Enhance AssistantPage and ComponentsModelsPage with API key management improvements
- Update AssistantPage to handle API key input more securely by removing placeholder values and allowing empty submissions to retain existing keys. - Introduce a new SecretInputField component for API key entry, improving user experience with visibility toggling and contextual hints. - Modify ComponentsModelsPage to reflect similar API key handling, ensuring users can manage keys effectively while providing feedback on existing configurations. - Add EditorBackButton for better navigation within the AssistantPage.
This commit is contained in:
@@ -7,6 +7,8 @@ import {
|
||||
Check,
|
||||
Copy,
|
||||
Database,
|
||||
Eye,
|
||||
EyeOff,
|
||||
MessageSquareText,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
@@ -525,7 +527,8 @@ export function AssistantPage() {
|
||||
setDifyForm({
|
||||
name: a.name,
|
||||
apiUrl: a.apiUrl,
|
||||
apiKey: a.apiKey, // 编辑时为打码值,不改则原样回传(后端哨兵保留旧 key)
|
||||
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||
apiKey: "",
|
||||
asr: a.asrCredentialId ?? "",
|
||||
voice: a.ttsCredentialId ?? "",
|
||||
enableInterrupt: a.enableInterrupt,
|
||||
@@ -552,7 +555,8 @@ export function AssistantPage() {
|
||||
name: a.name,
|
||||
appId: a.appId,
|
||||
apiUrl: a.apiUrl,
|
||||
apiKey: a.apiKey,
|
||||
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||
apiKey: "",
|
||||
asr: a.asrCredentialId ?? "",
|
||||
voice: a.ttsCredentialId ?? "",
|
||||
enableInterrupt: a.enableInterrupt,
|
||||
@@ -580,6 +584,9 @@ export function AssistantPage() {
|
||||
type: typeToLabel[a.type],
|
||||
updatedAt: formatTimestamp(a.updatedAt),
|
||||
}));
|
||||
const hasStoredApiKey = Boolean(
|
||||
editingId && assistants.find((assistant) => assistant.id === editingId)?.apiKey,
|
||||
);
|
||||
|
||||
const filteredAssistants = listItems.filter((assistant) => {
|
||||
if (typeFilter !== "全部" && assistant.type !== typeFilter) {
|
||||
@@ -1082,6 +1089,7 @@ export function AssistantPage() {
|
||||
<div className="-mt-6 flex h-full flex-col gap-4">
|
||||
<div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<EditorBackButton onClick={() => setView("list")} />
|
||||
<EditableTitle
|
||||
value={difyForm.name}
|
||||
onChange={(value) => updateDifyForm("name", value)}
|
||||
@@ -1089,15 +1097,6 @@ export function AssistantPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setView("list")}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
返回
|
||||
</Button>
|
||||
|
||||
{saveError && (
|
||||
<span className="self-center text-xs text-destructive">
|
||||
{saveError}
|
||||
@@ -1131,12 +1130,12 @@ export function AssistantPage() {
|
||||
onChange={(value) => updateDifyForm("apiUrl", value)}
|
||||
placeholder="https://api.dify.ai/v1/chat-messages"
|
||||
/>
|
||||
<InputField
|
||||
<SecretInputField
|
||||
label="API Key"
|
||||
value={difyForm.apiKey}
|
||||
onChange={(value) => updateDifyForm("apiKey", value)}
|
||||
placeholder="请输入 Dify API Key"
|
||||
type="password"
|
||||
hasStoredValue={hasStoredApiKey}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
@@ -1188,6 +1187,7 @@ export function AssistantPage() {
|
||||
<div className="-mt-6 flex h-full flex-col gap-4">
|
||||
<div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<EditorBackButton onClick={() => setView("list")} />
|
||||
<EditableTitle
|
||||
value={fastGptForm.name}
|
||||
onChange={(value) => updateFastGptForm("name", value)}
|
||||
@@ -1195,15 +1195,6 @@ export function AssistantPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setView("list")}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
返回
|
||||
</Button>
|
||||
|
||||
{saveError && (
|
||||
<span className="self-center text-xs text-destructive">
|
||||
{saveError}
|
||||
@@ -1243,12 +1234,12 @@ export function AssistantPage() {
|
||||
onChange={(value) => updateFastGptForm("apiUrl", value)}
|
||||
placeholder="https://api.fastgpt.in/api/v1/chat/completions"
|
||||
/>
|
||||
<InputField
|
||||
<SecretInputField
|
||||
label="API Key"
|
||||
value={fastGptForm.apiKey}
|
||||
onChange={(value) => updateFastGptForm("apiKey", value)}
|
||||
placeholder="请输入 FastGPT API Key"
|
||||
type="password"
|
||||
hasStoredValue={hasStoredApiKey}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
@@ -1300,6 +1291,7 @@ export function AssistantPage() {
|
||||
<div className="-mt-6 flex h-full flex-col gap-4">
|
||||
<div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<EditorBackButton onClick={() => setView("list")} />
|
||||
<EditableTitle
|
||||
value={openCodeForm.name}
|
||||
onChange={(value) => updateOpenCodeForm("name", value)}
|
||||
@@ -1307,15 +1299,6 @@ export function AssistantPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setView("list")}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
返回
|
||||
</Button>
|
||||
|
||||
<Button className="gap-2">
|
||||
<Save size={16} />
|
||||
保存
|
||||
@@ -1336,12 +1319,12 @@ export function AssistantPage() {
|
||||
onChange={(value) => updateOpenCodeForm("apiUrl", value)}
|
||||
placeholder="http://localhost:4096"
|
||||
/>
|
||||
<InputField
|
||||
<SecretInputField
|
||||
label="API Key"
|
||||
value={openCodeForm.apiKey}
|
||||
onChange={(value) => updateOpenCodeForm("apiKey", value)}
|
||||
placeholder="请输入 OpenCode API Key"
|
||||
type="password"
|
||||
hasStoredValue={false}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
@@ -1403,6 +1386,7 @@ export function AssistantPage() {
|
||||
<div className="-mt-6 flex h-full flex-col gap-4">
|
||||
<div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<EditorBackButton onClick={() => setView("list")} />
|
||||
<EditableTitle
|
||||
value={form.name}
|
||||
onChange={(value) => updateForm("name", value)}
|
||||
@@ -1410,15 +1394,6 @@ export function AssistantPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setView("list")}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
返回
|
||||
</Button>
|
||||
|
||||
{saveError && (
|
||||
<span className="self-center text-xs text-destructive">
|
||||
{saveError}
|
||||
@@ -1754,6 +1729,20 @@ function DebugVoicePanel() {
|
||||
);
|
||||
}
|
||||
|
||||
function EditorBackButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={onClick}
|
||||
aria-label="返回助手列表"
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function EditableTitle({
|
||||
value,
|
||||
onChange,
|
||||
@@ -1915,6 +1904,60 @@ function InputField({
|
||||
);
|
||||
}
|
||||
|
||||
function SecretInputField({
|
||||
label,
|
||||
value,
|
||||
placeholder,
|
||||
hasStoredValue,
|
||||
onChange,
|
||||
}: {
|
||||
label?: string;
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
hasStoredValue: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const [showValue, setShowValue] = useState(false);
|
||||
|
||||
return (
|
||||
<label className="block">
|
||||
{label && (
|
||||
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={value}
|
||||
type={showValue ? "text" : "password"}
|
||||
placeholder={
|
||||
hasStoredValue ? "已配置,留空则保持不变" : placeholder
|
||||
}
|
||||
autoComplete="new-password"
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
if (!nextValue) setShowValue(false);
|
||||
onChange(nextValue);
|
||||
}}
|
||||
className="border-hairline-strong bg-background pr-10 text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!value}
|
||||
onClick={() => setShowValue((current) => !current)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-soft transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
|
||||
aria-label={showValue ? "隐藏 API Key" : "显示 API Key"}
|
||||
>
|
||||
{showValue ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
{hasStoredValue && (
|
||||
<p className="mt-2 text-xs leading-5 text-muted-foreground">
|
||||
已有密钥不会返回浏览器。留空可保持原密钥,输入新值将覆盖原密钥。
|
||||
</p>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function TextAreaField({
|
||||
label,
|
||||
value,
|
||||
|
||||
@@ -145,7 +145,9 @@ export function ComponentsModelsPage() {
|
||||
try {
|
||||
// TODO: 接入真实疎通接口(按 form.interfaceType 区分调用方式)
|
||||
await new Promise((resolve) => setTimeout(resolve, 900));
|
||||
const reachable = Boolean(form.apiUrl.trim() && form.apiKey.trim());
|
||||
const reachable = Boolean(
|
||||
form.apiUrl.trim() && (form.apiKey.trim() || editingModel?.apiKey),
|
||||
);
|
||||
setTestResult(reachable ? "ok" : "fail");
|
||||
} catch {
|
||||
setTestResult("fail");
|
||||
@@ -186,8 +188,8 @@ export function ComponentsModelsPage() {
|
||||
type: model.type,
|
||||
interfaceType: model.interfaceType,
|
||||
apiUrl: model.apiUrl,
|
||||
// 后端回传已打码,原样带回 → 不修改则保留旧 key(写时哨兵)
|
||||
apiKey: model.apiKey,
|
||||
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||
apiKey: "",
|
||||
});
|
||||
setShowKey(false);
|
||||
setTestResult("idle");
|
||||
@@ -282,6 +284,7 @@ export function ComponentsModelsPage() {
|
||||
}
|
||||
|
||||
const interfaceOptions = interfaceOptionsByType[form.type];
|
||||
const hasStoredApiKey = Boolean(editingId && editingModel?.apiKey);
|
||||
const canSave =
|
||||
form.name.trim() && form.modelId.trim() && form.apiUrl.trim();
|
||||
|
||||
@@ -699,18 +702,29 @@ export function ComponentsModelsPage() {
|
||||
value={form.apiKey}
|
||||
type={showKey ? "text" : "password"}
|
||||
onChange={(event) => updateForm("apiKey", event.target.value)}
|
||||
placeholder="请输入 API Key"
|
||||
placeholder={
|
||||
hasStoredApiKey
|
||||
? "已配置,留空则保持不变"
|
||||
: "请输入 API Key"
|
||||
}
|
||||
autoComplete="new-password"
|
||||
className="border-hairline-strong bg-background pr-10 text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowKey((value) => !value)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-soft transition-colors hover:text-foreground"
|
||||
disabled={!form.apiKey}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-soft transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
|
||||
aria-label={showKey ? "隐藏 API Key" : "显示 API Key"}
|
||||
>
|
||||
{showKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
{hasStoredApiKey && (
|
||||
<p className="mt-2 text-xs leading-5 text-muted-foreground">
|
||||
已有密钥不会返回浏览器。留空可保持原密钥,输入新值将覆盖原密钥。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user