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,
|
Check,
|
||||||
Copy,
|
Copy,
|
||||||
Database,
|
Database,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
MessageSquareText,
|
MessageSquareText,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Pencil,
|
Pencil,
|
||||||
@@ -525,7 +527,8 @@ export function AssistantPage() {
|
|||||||
setDifyForm({
|
setDifyForm({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
apiUrl: a.apiUrl,
|
apiUrl: a.apiUrl,
|
||||||
apiKey: a.apiKey, // 编辑时为打码值,不改则原样回传(后端哨兵保留旧 key)
|
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||||
|
apiKey: "",
|
||||||
asr: a.asrCredentialId ?? "",
|
asr: a.asrCredentialId ?? "",
|
||||||
voice: a.ttsCredentialId ?? "",
|
voice: a.ttsCredentialId ?? "",
|
||||||
enableInterrupt: a.enableInterrupt,
|
enableInterrupt: a.enableInterrupt,
|
||||||
@@ -552,7 +555,8 @@ export function AssistantPage() {
|
|||||||
name: a.name,
|
name: a.name,
|
||||||
appId: a.appId,
|
appId: a.appId,
|
||||||
apiUrl: a.apiUrl,
|
apiUrl: a.apiUrl,
|
||||||
apiKey: a.apiKey,
|
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||||
|
apiKey: "",
|
||||||
asr: a.asrCredentialId ?? "",
|
asr: a.asrCredentialId ?? "",
|
||||||
voice: a.ttsCredentialId ?? "",
|
voice: a.ttsCredentialId ?? "",
|
||||||
enableInterrupt: a.enableInterrupt,
|
enableInterrupt: a.enableInterrupt,
|
||||||
@@ -580,6 +584,9 @@ export function AssistantPage() {
|
|||||||
type: typeToLabel[a.type],
|
type: typeToLabel[a.type],
|
||||||
updatedAt: formatTimestamp(a.updatedAt),
|
updatedAt: formatTimestamp(a.updatedAt),
|
||||||
}));
|
}));
|
||||||
|
const hasStoredApiKey = Boolean(
|
||||||
|
editingId && assistants.find((assistant) => assistant.id === editingId)?.apiKey,
|
||||||
|
);
|
||||||
|
|
||||||
const filteredAssistants = listItems.filter((assistant) => {
|
const filteredAssistants = listItems.filter((assistant) => {
|
||||||
if (typeFilter !== "全部" && assistant.type !== typeFilter) {
|
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="-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 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">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
<EditorBackButton onClick={() => setView("list")} />
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
value={difyForm.name}
|
value={difyForm.name}
|
||||||
onChange={(value) => updateDifyForm("name", value)}
|
onChange={(value) => updateDifyForm("name", value)}
|
||||||
@@ -1089,15 +1097,6 @@ export function AssistantPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 gap-2">
|
<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 && (
|
{saveError && (
|
||||||
<span className="self-center text-xs text-destructive">
|
<span className="self-center text-xs text-destructive">
|
||||||
{saveError}
|
{saveError}
|
||||||
@@ -1131,12 +1130,12 @@ export function AssistantPage() {
|
|||||||
onChange={(value) => updateDifyForm("apiUrl", value)}
|
onChange={(value) => updateDifyForm("apiUrl", value)}
|
||||||
placeholder="https://api.dify.ai/v1/chat-messages"
|
placeholder="https://api.dify.ai/v1/chat-messages"
|
||||||
/>
|
/>
|
||||||
<InputField
|
<SecretInputField
|
||||||
label="API Key"
|
label="API Key"
|
||||||
value={difyForm.apiKey}
|
value={difyForm.apiKey}
|
||||||
onChange={(value) => updateDifyForm("apiKey", value)}
|
onChange={(value) => updateDifyForm("apiKey", value)}
|
||||||
placeholder="请输入 Dify API Key"
|
placeholder="请输入 Dify API Key"
|
||||||
type="password"
|
hasStoredValue={hasStoredApiKey}
|
||||||
/>
|
/>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
@@ -1188,6 +1187,7 @@ export function AssistantPage() {
|
|||||||
<div className="-mt-6 flex h-full flex-col gap-4">
|
<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 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">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
<EditorBackButton onClick={() => setView("list")} />
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
value={fastGptForm.name}
|
value={fastGptForm.name}
|
||||||
onChange={(value) => updateFastGptForm("name", value)}
|
onChange={(value) => updateFastGptForm("name", value)}
|
||||||
@@ -1195,15 +1195,6 @@ export function AssistantPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 gap-2">
|
<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 && (
|
{saveError && (
|
||||||
<span className="self-center text-xs text-destructive">
|
<span className="self-center text-xs text-destructive">
|
||||||
{saveError}
|
{saveError}
|
||||||
@@ -1243,12 +1234,12 @@ export function AssistantPage() {
|
|||||||
onChange={(value) => updateFastGptForm("apiUrl", value)}
|
onChange={(value) => updateFastGptForm("apiUrl", value)}
|
||||||
placeholder="https://api.fastgpt.in/api/v1/chat/completions"
|
placeholder="https://api.fastgpt.in/api/v1/chat/completions"
|
||||||
/>
|
/>
|
||||||
<InputField
|
<SecretInputField
|
||||||
label="API Key"
|
label="API Key"
|
||||||
value={fastGptForm.apiKey}
|
value={fastGptForm.apiKey}
|
||||||
onChange={(value) => updateFastGptForm("apiKey", value)}
|
onChange={(value) => updateFastGptForm("apiKey", value)}
|
||||||
placeholder="请输入 FastGPT API Key"
|
placeholder="请输入 FastGPT API Key"
|
||||||
type="password"
|
hasStoredValue={hasStoredApiKey}
|
||||||
/>
|
/>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
@@ -1300,6 +1291,7 @@ export function AssistantPage() {
|
|||||||
<div className="-mt-6 flex h-full flex-col gap-4">
|
<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 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">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
<EditorBackButton onClick={() => setView("list")} />
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
value={openCodeForm.name}
|
value={openCodeForm.name}
|
||||||
onChange={(value) => updateOpenCodeForm("name", value)}
|
onChange={(value) => updateOpenCodeForm("name", value)}
|
||||||
@@ -1307,15 +1299,6 @@ export function AssistantPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 gap-2">
|
<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">
|
<Button className="gap-2">
|
||||||
<Save size={16} />
|
<Save size={16} />
|
||||||
保存
|
保存
|
||||||
@@ -1336,12 +1319,12 @@ export function AssistantPage() {
|
|||||||
onChange={(value) => updateOpenCodeForm("apiUrl", value)}
|
onChange={(value) => updateOpenCodeForm("apiUrl", value)}
|
||||||
placeholder="http://localhost:4096"
|
placeholder="http://localhost:4096"
|
||||||
/>
|
/>
|
||||||
<InputField
|
<SecretInputField
|
||||||
label="API Key"
|
label="API Key"
|
||||||
value={openCodeForm.apiKey}
|
value={openCodeForm.apiKey}
|
||||||
onChange={(value) => updateOpenCodeForm("apiKey", value)}
|
onChange={(value) => updateOpenCodeForm("apiKey", value)}
|
||||||
placeholder="请输入 OpenCode API Key"
|
placeholder="请输入 OpenCode API Key"
|
||||||
type="password"
|
hasStoredValue={false}
|
||||||
/>
|
/>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
@@ -1403,6 +1386,7 @@ export function AssistantPage() {
|
|||||||
<div className="-mt-6 flex h-full flex-col gap-4">
|
<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 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">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
<EditorBackButton onClick={() => setView("list")} />
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(value) => updateForm("name", value)}
|
onChange={(value) => updateForm("name", value)}
|
||||||
@@ -1410,15 +1394,6 @@ export function AssistantPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 gap-2">
|
<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 && (
|
{saveError && (
|
||||||
<span className="self-center text-xs text-destructive">
|
<span className="self-center text-xs text-destructive">
|
||||||
{saveError}
|
{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({
|
function EditableTitle({
|
||||||
value,
|
value,
|
||||||
onChange,
|
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({
|
function TextAreaField({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -145,7 +145,9 @@ export function ComponentsModelsPage() {
|
|||||||
try {
|
try {
|
||||||
// TODO: 接入真实疎通接口(按 form.interfaceType 区分调用方式)
|
// TODO: 接入真实疎通接口(按 form.interfaceType 区分调用方式)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 900));
|
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");
|
setTestResult(reachable ? "ok" : "fail");
|
||||||
} catch {
|
} catch {
|
||||||
setTestResult("fail");
|
setTestResult("fail");
|
||||||
@@ -186,8 +188,8 @@ export function ComponentsModelsPage() {
|
|||||||
type: model.type,
|
type: model.type,
|
||||||
interfaceType: model.interfaceType,
|
interfaceType: model.interfaceType,
|
||||||
apiUrl: model.apiUrl,
|
apiUrl: model.apiUrl,
|
||||||
// 后端回传已打码,原样带回 → 不修改则保留旧 key(写时哨兵)
|
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||||
apiKey: model.apiKey,
|
apiKey: "",
|
||||||
});
|
});
|
||||||
setShowKey(false);
|
setShowKey(false);
|
||||||
setTestResult("idle");
|
setTestResult("idle");
|
||||||
@@ -282,6 +284,7 @@ export function ComponentsModelsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const interfaceOptions = interfaceOptionsByType[form.type];
|
const interfaceOptions = interfaceOptionsByType[form.type];
|
||||||
|
const hasStoredApiKey = Boolean(editingId && editingModel?.apiKey);
|
||||||
const canSave =
|
const canSave =
|
||||||
form.name.trim() && form.modelId.trim() && form.apiUrl.trim();
|
form.name.trim() && form.modelId.trim() && form.apiUrl.trim();
|
||||||
|
|
||||||
@@ -699,18 +702,29 @@ export function ComponentsModelsPage() {
|
|||||||
value={form.apiKey}
|
value={form.apiKey}
|
||||||
type={showKey ? "text" : "password"}
|
type={showKey ? "text" : "password"}
|
||||||
onChange={(event) => updateForm("apiKey", event.target.value)}
|
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"
|
className="border-hairline-strong bg-background pr-10 text-foreground placeholder:text-muted-soft"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowKey((value) => !value)}
|
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"}
|
aria-label={showKey ? "隐藏 API Key" : "显示 API Key"}
|
||||||
>
|
>
|
||||||
{showKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
{showKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{hasStoredApiKey && (
|
||||||
|
<p className="mt-2 text-xs leading-5 text-muted-foreground">
|
||||||
|
已有密钥不会返回浏览器。留空可保持原密钥,输入新值将覆盖原密钥。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user