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:
Xin Wang
2026-06-09 12:49:18 +08:00
parent 6acbac7d3b
commit 044411edc6
2 changed files with 106 additions and 49 deletions

View File

@@ -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,

View File

@@ -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>