返回 DeepSeek-Reasonix
OnboardingOverlay.tsx
根目录 / desktop / frontend / src / components / OnboardingOverlay.tsx
1 import { useCallback, useRef, useState } from "react";
2 import logo from "../assets/logo.svg";
3 import { useT } from "../lib/i18n";
4 import { app, openExternal } from "../lib/bridge";
5
6 // Full-window first-run guide: DeepSeek stays the fastest path, while users can
7 // open the provider settings or defer setup without being trapped in the gate.
8 export function OnboardingOverlay({
9 onComplete,
10 onChooseProvider,
11 onSkip,
12 }: {
13 onComplete: () => void;
14 onChooseProvider: () => void;
15 onSkip: () => void;
16 }) {
17 const t = useT();
18 const [value, setValue] = useState("");
19 const [state, setState] = useState<"idle" | "validating" | "error">("idle");
20 const [error, setError] = useState<string | null>(null);
21 const inputRef = useRef<HTMLInputElement>(null);
22
23 const submit = useCallback(async () => {
24 const key = value.trim();
25 if (!key) {
26 setError(t("onboarding.error.empty"));
27 setState("error");
28 inputRef.current?.focus();
29 return;
30 }
31 setState("validating");
32 setError(null);
33 try {
34 await app.ConnectKey(key);
35 onComplete();
36 } catch (e) {
37 const msg = e instanceof Error ? e.message : String(e);
38 if (/status\s*401|status\s*403|invalid/i.test(msg)) {
39 setError(t("onboarding.error.invalid"));
40 } else if (/network|unreachable|timeout|dial/i.test(msg)) {
41 setError(t("onboarding.error.network"));
42 } else {
43 setError(msg || t("onboarding.error.unknown"));
44 }
45 setState("error");
46 inputRef.current?.focus();
47 inputRef.current?.select();
48 }
49 }, [t, value, onComplete]);
50
51 return (
52 <div className="onboarding" role="dialog" aria-modal="true" aria-labelledby="onboarding-title">
53 <div className="onboarding__card">
54 <img src={logo} className="onboarding__logo" alt="Reasonix" draggable={false} />
55 <div id="onboarding-title" className="onboarding__title">{t("onboarding.title")}</div>
56 <div className="onboarding__tag">{t("onboarding.tagline")}</div>
57
58 <label className="onboarding__label" htmlFor="onboarding-key">
59 {t("onboarding.inputLabel")}
60 </label>
61 <input
62 id="onboarding-key"
63 ref={inputRef}
64 className="onboarding__input"
65 type="password"
66 autoComplete="off"
67 spellCheck={false}
68 autoFocus
69 placeholder={t("onboarding.inputPlaceholder")}
70 value={value}
71 onChange={(e) => {
72 setValue(e.target.value);
73 if (state === "error") setState("idle");
74 }}
75 onKeyDown={(e) => {
76 if (e.key === "Enter" && state !== "validating") {
77 e.preventDefault();
78 void submit();
79 }
80 }}
81 disabled={state === "validating"}
82 />
83
84 {state === "error" && error && (
85 <div className="onboarding__error" role="alert">
86 {error}
87 </div>
88 )}
89
90 <button
91 className="onboarding__submit"
92 onClick={() => void submit()}
93 disabled={state === "validating"}
94 >
95 {state === "validating" ? (
96 <>
97 <span className="onboarding__spinner" />
98 {t("onboarding.validating")}
99 </>
100 ) : (
101 t("onboarding.submit")
102 )}
103 </button>
104
105 <button
106 type="button"
107 className="onboarding__provider"
108 onClick={onChooseProvider}
109 disabled={state === "validating"}
110 >
111 {t("onboarding.chooseProvider")}
112 </button>
113
114 <div className="onboarding__links">
115 <button
116 type="button"
117 className="onboarding__link"
118 onClick={() => openExternal("https://platform.deepseek.com/api_keys")}
119 >
120 {t("onboarding.getKey")}
121 </button>
122 <span className="onboarding__sep">·</span>
123 <span className="onboarding__privacy">{t("onboarding.privacy")}</span>
124 </div>
125
126 <button
127 type="button"
128 className="onboarding__skip"
129 onClick={onSkip}
130 disabled={state === "validating"}
131 >
132 {t("onboarding.skip")}
133 </button>
134 </div>
135 </div>
136 );
137 }
138
138 lines Plain Text