Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 100 additions & 18 deletions app/components/assistant-ui/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/app/components/ui/radio-group";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/app/components/ui/checkbox";
import { AlertCircle } from "lucide-react";

interface SettingsDialogProps {
isOpen: boolean;
Expand All @@ -29,6 +31,8 @@ export const SettingsDialog = ({
setOpenaiApiKey,
geminiApiKey,
setGeminiApiKey,
saveToLocalStorage,
setSaveToLocalStorage,
refreshFromStorage,
} = useAssistantSettings();

Expand Down Expand Up @@ -70,28 +74,106 @@ export const SettingsDialog = ({
</div>

{provider === "openai" && (
<div className="space-y-2">
<Label htmlFor="openai-key">OpenAI API Key</Label>
<Input
id="openai-key"
type="password"
placeholder="sk-..."
value={openaiApiKey}
onChange={(e) => setOpenaiApiKey(e.target.value)}
/>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="openai-key">OpenAI API Key</Label>
<Input
id="openai-key"
type="password"
placeholder="sk-..."
value={openaiApiKey}
onChange={(e) => setOpenaiApiKey(e.target.value)}
/>
</div>

<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="save-to-storage"
checked={saveToLocalStorage}
onCheckedChange={(checked) =>
setSaveToLocalStorage(checked === true)
}
/>
<Label
htmlFor="save-to-storage"
className="text-sm font-normal cursor-pointer"
>
保存 API Key 到本地存储
</Label>
</div>

{saveToLocalStorage && (
<div className="rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-950/20 p-3">
<div className="flex gap-2">
<AlertCircle className="size-4 text-amber-600 dark:text-amber-500 shrink-0 mt-0.5" />
<div className="space-y-1 text-xs text-amber-900 dark:text-amber-200">
<p className="font-medium">安全提示</p>
<ul className="space-y-0.5 list-disc list-inside">
<li>
API Key 将以明文形式存储在浏览器的 localStorage 中
</li>
<li>任何能访问此浏览器的人都可能获取您的 API Key</li>
<li>请勿在公用电脑或共享设备上勾选此选项</li>
<li>建议定期更换 API Key 以提高安全性</li>
</ul>
</div>
</div>
</div>
)}
</div>
</div>
)}

{provider === "gemini" && (
<div className="space-y-2">
<Label htmlFor="gemini-key">Gemini API Key</Label>
<Input
id="gemini-key"
type="password"
placeholder="AIzaSy..."
value={geminiApiKey}
onChange={(e) => setGeminiApiKey(e.target.value)}
/>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="gemini-key">Gemini API Key</Label>
<Input
id="gemini-key"
type="password"
placeholder="AIzaSy..."
value={geminiApiKey}
onChange={(e) => setGeminiApiKey(e.target.value)}
/>
</div>

<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="save-to-storage"
checked={saveToLocalStorage}
onCheckedChange={(checked) =>
setSaveToLocalStorage(checked === true)
}
/>
<Label
htmlFor="save-to-storage"
className="text-sm font-normal cursor-pointer"
>
保存 API Key 到本地存储
</Label>
</div>

{saveToLocalStorage && (
<div className="rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-950/20 p-3">
<div className="flex gap-2">
<AlertCircle className="size-4 text-amber-600 dark:text-amber-500 shrink-0 mt-0.5" />
<div className="space-y-1 text-xs text-amber-900 dark:text-amber-200">
<p className="font-medium">安全提示</p>
<ul className="space-y-0.5 list-disc list-inside">
<li>
API Key 将以明文形式存储在浏览器的 localStorage 中
</li>
<li>任何能访问此浏览器的人都可能获取您的 API Key</li>
<li>请勿在公用电脑或共享设备上勾选此选项</li>
<li>建议定期更换 API Key 以提高安全性</li>
</ul>
</div>
</div>
</div>
)}
</div>
</div>
)}

Expand Down
34 changes: 34 additions & 0 deletions app/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";

import { cn } from "@/lib/utils";

const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer size-4 shrink-0 rounded border border-input shadow-xs transition-[color,box-shadow] outline-none",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"disabled:cursor-not-allowed disabled:opacity-50",
"data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary",
"dark:bg-input/30",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;

export { Checkbox };
45 changes: 40 additions & 5 deletions app/hooks/useAssistantSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ interface AssistantSettingsState {
provider: Provider;
openaiApiKey: string;
geminiApiKey: string;
saveToLocalStorage: boolean; // 是否将API Key保存到localStorage
}

interface AssistantSettingsContextValue extends AssistantSettingsState {
setProvider: (provider: Provider) => void;
setOpenaiApiKey: (key: string) => void;
setGeminiApiKey: (key: string) => void;
setSaveToLocalStorage: (save: boolean) => void;
refreshFromStorage: () => void;
}

Expand All @@ -31,6 +33,7 @@ const defaultSettings: AssistantSettingsState = {
provider: "openai",
openaiApiKey: "",
geminiApiKey: "",
saveToLocalStorage: false,
};

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

try {
const parsed = JSON.parse(raw) as Partial<AssistantSettingsState>;
const saveToLocalStorage = parsed.saveToLocalStorage === true;

return {
provider:
parsed.provider === "gemini"
? "gemini"
: parsed.provider === "intern"
? "intern"
: "openai",
// 只有在saveToLocalStorage为true时才使用存储的key
openaiApiKey:
typeof parsed.openaiApiKey === "string" ? parsed.openaiApiKey : "",
saveToLocalStorage && typeof parsed.openaiApiKey === "string"
? parsed.openaiApiKey
: "",
geminiApiKey:
typeof parsed.geminiApiKey === "string" ? parsed.geminiApiKey : "",
saveToLocalStorage && typeof parsed.geminiApiKey === "string"
? parsed.geminiApiKey
: "",
saveToLocalStorage,
};
} catch (error) {
console.error(
Expand Down Expand Up @@ -89,7 +100,16 @@ export const AssistantSettingsProvider = ({
}

try {
window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
// 根据saveToLocalStorage决定是否保存API key
const dataToSave = settings.saveToLocalStorage
? settings
: {
...settings,
openaiApiKey: "",
geminiApiKey: "",
};

window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(dataToSave));
} catch (error) {
console.error("Failed to save assistant settings to localStorage", error);
}
Expand All @@ -113,8 +133,20 @@ export const AssistantSettingsProvider = ({
}, []);

const refreshFromStorage = useCallback(() => {
const latestSettings = readStoredSettings();
setSettings(latestSettings);
setSettings((prev) => {
const storedSettings = readStoredSettings();

if (storedSettings.saveToLocalStorage) {
return storedSettings;
}

return {
...prev,
...storedSettings,
openaiApiKey: prev.openaiApiKey,
geminiApiKey: prev.geminiApiKey,
};
});
}, []);

const value = useMemo(
Expand All @@ -129,6 +161,9 @@ export const AssistantSettingsProvider = ({
setGeminiApiKey: (key: string) => {
setSettings((prev) => ({ ...prev, geminiApiKey: key }));
},
setSaveToLocalStorage: (save: boolean) => {
setSettings((prev) => ({ ...prev, saveToLocalStorage: save }));
},
refreshFromStorage,
}),
[settings, refreshFromStorage],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@orama/tokenizers": "^3.1.14",
"@prisma/client": "^6.16.2",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-radio-group": "^1.3.8",
Expand Down
Loading