Skip to content

Commit b7fecfc

Browse files
authored
Merge pull request #226 from Crokily/feat/Crokily/StorageConfirm
2 parents ad291db + 21680dc commit b7fecfc

File tree

5 files changed

+4480
-8035
lines changed

5 files changed

+4480
-8035
lines changed

app/components/assistant-ui/SettingsDialog.tsx

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { Label } from "@/components/ui/label";
1212
import { Input } from "@/components/ui/input";
1313
import { RadioGroup, RadioGroupItem } from "@/app/components/ui/radio-group";
1414
import { Button } from "@/components/ui/button";
15+
import { Checkbox } from "@/app/components/ui/checkbox";
16+
import { AlertCircle } from "lucide-react";
1517

1618
interface SettingsDialogProps {
1719
isOpen: boolean;
@@ -29,6 +31,8 @@ export const SettingsDialog = ({
2931
setOpenaiApiKey,
3032
geminiApiKey,
3133
setGeminiApiKey,
34+
saveToLocalStorage,
35+
setSaveToLocalStorage,
3236
refreshFromStorage,
3337
} = useAssistantSettings();
3438

@@ -70,28 +74,106 @@ export const SettingsDialog = ({
7074
</div>
7175

7276
{provider === "openai" && (
73-
<div className="space-y-2">
74-
<Label htmlFor="openai-key">OpenAI API Key</Label>
75-
<Input
76-
id="openai-key"
77-
type="password"
78-
placeholder="sk-..."
79-
value={openaiApiKey}
80-
onChange={(e) => setOpenaiApiKey(e.target.value)}
81-
/>
77+
<div className="space-y-4">
78+
<div className="space-y-2">
79+
<Label htmlFor="openai-key">OpenAI API Key</Label>
80+
<Input
81+
id="openai-key"
82+
type="password"
83+
placeholder="sk-..."
84+
value={openaiApiKey}
85+
onChange={(e) => setOpenaiApiKey(e.target.value)}
86+
/>
87+
</div>
88+
89+
<div className="space-y-3">
90+
<div className="flex items-center space-x-2">
91+
<Checkbox
92+
id="save-to-storage"
93+
checked={saveToLocalStorage}
94+
onCheckedChange={(checked) =>
95+
setSaveToLocalStorage(checked === true)
96+
}
97+
/>
98+
<Label
99+
htmlFor="save-to-storage"
100+
className="text-sm font-normal cursor-pointer"
101+
>
102+
保存 API Key 到本地存储
103+
</Label>
104+
</div>
105+
106+
{saveToLocalStorage && (
107+
<div className="rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-950/20 p-3">
108+
<div className="flex gap-2">
109+
<AlertCircle className="size-4 text-amber-600 dark:text-amber-500 shrink-0 mt-0.5" />
110+
<div className="space-y-1 text-xs text-amber-900 dark:text-amber-200">
111+
<p className="font-medium">安全提示</p>
112+
<ul className="space-y-0.5 list-disc list-inside">
113+
<li>
114+
API Key 将以明文形式存储在浏览器的 localStorage 中
115+
</li>
116+
<li>任何能访问此浏览器的人都可能获取您的 API Key</li>
117+
<li>请勿在公用电脑或共享设备上勾选此选项</li>
118+
<li>建议定期更换 API Key 以提高安全性</li>
119+
</ul>
120+
</div>
121+
</div>
122+
</div>
123+
)}
124+
</div>
82125
</div>
83126
)}
84127

85128
{provider === "gemini" && (
86-
<div className="space-y-2">
87-
<Label htmlFor="gemini-key">Gemini API Key</Label>
88-
<Input
89-
id="gemini-key"
90-
type="password"
91-
placeholder="AIzaSy..."
92-
value={geminiApiKey}
93-
onChange={(e) => setGeminiApiKey(e.target.value)}
94-
/>
129+
<div className="space-y-4">
130+
<div className="space-y-2">
131+
<Label htmlFor="gemini-key">Gemini API Key</Label>
132+
<Input
133+
id="gemini-key"
134+
type="password"
135+
placeholder="AIzaSy..."
136+
value={geminiApiKey}
137+
onChange={(e) => setGeminiApiKey(e.target.value)}
138+
/>
139+
</div>
140+
141+
<div className="space-y-3">
142+
<div className="flex items-center space-x-2">
143+
<Checkbox
144+
id="save-to-storage"
145+
checked={saveToLocalStorage}
146+
onCheckedChange={(checked) =>
147+
setSaveToLocalStorage(checked === true)
148+
}
149+
/>
150+
<Label
151+
htmlFor="save-to-storage"
152+
className="text-sm font-normal cursor-pointer"
153+
>
154+
保存 API Key 到本地存储
155+
</Label>
156+
</div>
157+
158+
{saveToLocalStorage && (
159+
<div className="rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-950/20 p-3">
160+
<div className="flex gap-2">
161+
<AlertCircle className="size-4 text-amber-600 dark:text-amber-500 shrink-0 mt-0.5" />
162+
<div className="space-y-1 text-xs text-amber-900 dark:text-amber-200">
163+
<p className="font-medium">安全提示</p>
164+
<ul className="space-y-0.5 list-disc list-inside">
165+
<li>
166+
API Key 将以明文形式存储在浏览器的 localStorage 中
167+
</li>
168+
<li>任何能访问此浏览器的人都可能获取您的 API Key</li>
169+
<li>请勿在公用电脑或共享设备上勾选此选项</li>
170+
<li>建议定期更换 API Key 以提高安全性</li>
171+
</ul>
172+
</div>
173+
</div>
174+
</div>
175+
)}
176+
</div>
95177
</div>
96178
)}
97179

app/components/ui/checkbox.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5+
import { CheckIcon } from "lucide-react";
6+
7+
import { cn } from "@/lib/utils";
8+
9+
const Checkbox = React.forwardRef<
10+
React.ElementRef<typeof CheckboxPrimitive.Root>,
11+
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
12+
>(({ className, ...props }, ref) => (
13+
<CheckboxPrimitive.Root
14+
ref={ref}
15+
className={cn(
16+
"peer size-4 shrink-0 rounded border border-input shadow-xs transition-[color,box-shadow] outline-none",
17+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
18+
"disabled:cursor-not-allowed disabled:opacity-50",
19+
"data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary",
20+
"dark:bg-input/30",
21+
className,
22+
)}
23+
{...props}
24+
>
25+
<CheckboxPrimitive.Indicator
26+
className={cn("flex items-center justify-center text-current")}
27+
>
28+
<CheckIcon className="size-3.5" />
29+
</CheckboxPrimitive.Indicator>
30+
</CheckboxPrimitive.Root>
31+
));
32+
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
33+
34+
export { Checkbox };

app/hooks/useAssistantSettings.tsx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ interface AssistantSettingsState {
1616
provider: Provider;
1717
openaiApiKey: string;
1818
geminiApiKey: string;
19+
saveToLocalStorage: boolean; // 是否将API Key保存到localStorage
1920
}
2021

2122
interface AssistantSettingsContextValue extends AssistantSettingsState {
2223
setProvider: (provider: Provider) => void;
2324
setOpenaiApiKey: (key: string) => void;
2425
setGeminiApiKey: (key: string) => void;
26+
setSaveToLocalStorage: (save: boolean) => void;
2527
refreshFromStorage: () => void;
2628
}
2729

@@ -31,6 +33,7 @@ const defaultSettings: AssistantSettingsState = {
3133
provider: "openai",
3234
openaiApiKey: "",
3335
geminiApiKey: "",
36+
saveToLocalStorage: false,
3437
};
3538

3639
const AssistantSettingsContext = createContext<
@@ -44,17 +47,25 @@ const parseStoredSettings = (raw: string | null): AssistantSettingsState => {
4447

4548
try {
4649
const parsed = JSON.parse(raw) as Partial<AssistantSettingsState>;
50+
const saveToLocalStorage = parsed.saveToLocalStorage === true;
51+
4752
return {
4853
provider:
4954
parsed.provider === "gemini"
5055
? "gemini"
5156
: parsed.provider === "intern"
5257
? "intern"
5358
: "openai",
59+
// 只有在saveToLocalStorage为true时才使用存储的key
5460
openaiApiKey:
55-
typeof parsed.openaiApiKey === "string" ? parsed.openaiApiKey : "",
61+
saveToLocalStorage && typeof parsed.openaiApiKey === "string"
62+
? parsed.openaiApiKey
63+
: "",
5664
geminiApiKey:
57-
typeof parsed.geminiApiKey === "string" ? parsed.geminiApiKey : "",
65+
saveToLocalStorage && typeof parsed.geminiApiKey === "string"
66+
? parsed.geminiApiKey
67+
: "",
68+
saveToLocalStorage,
5869
};
5970
} catch (error) {
6071
console.error(
@@ -89,7 +100,16 @@ export const AssistantSettingsProvider = ({
89100
}
90101

91102
try {
92-
window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
103+
// 根据saveToLocalStorage决定是否保存API key
104+
const dataToSave = settings.saveToLocalStorage
105+
? settings
106+
: {
107+
...settings,
108+
openaiApiKey: "",
109+
geminiApiKey: "",
110+
};
111+
112+
window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(dataToSave));
93113
} catch (error) {
94114
console.error("Failed to save assistant settings to localStorage", error);
95115
}
@@ -113,8 +133,20 @@ export const AssistantSettingsProvider = ({
113133
}, []);
114134

115135
const refreshFromStorage = useCallback(() => {
116-
const latestSettings = readStoredSettings();
117-
setSettings(latestSettings);
136+
setSettings((prev) => {
137+
const storedSettings = readStoredSettings();
138+
139+
if (storedSettings.saveToLocalStorage) {
140+
return storedSettings;
141+
}
142+
143+
return {
144+
...prev,
145+
...storedSettings,
146+
openaiApiKey: prev.openaiApiKey,
147+
geminiApiKey: prev.geminiApiKey,
148+
};
149+
});
118150
}, []);
119151

120152
const value = useMemo(
@@ -129,6 +161,9 @@ export const AssistantSettingsProvider = ({
129161
setGeminiApiKey: (key: string) => {
130162
setSettings((prev) => ({ ...prev, geminiApiKey: key }));
131163
},
164+
setSaveToLocalStorage: (save: boolean) => {
165+
setSettings((prev) => ({ ...prev, saveToLocalStorage: save }));
166+
},
132167
refreshFromStorage,
133168
}),
134169
[settings, refreshFromStorage],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@orama/tokenizers": "^3.1.14",
3434
"@prisma/client": "^6.16.2",
3535
"@radix-ui/react-avatar": "^1.1.10",
36+
"@radix-ui/react-checkbox": "^1.3.3",
3637
"@radix-ui/react-dialog": "^1.1.15",
3738
"@radix-ui/react-label": "^2.1.7",
3839
"@radix-ui/react-radio-group": "^1.3.8",

0 commit comments

Comments
 (0)