Enhance OpenCode form handling in AssistantPage
- Introduce a new model field in the OpenCode form to manage language model selection. - Refactor the form handling logic to improve data loading and error management for OpenCode assistants. - Update UI components to utilize ResourceSelectField for model and voice configuration, enhancing user experience. - Clear form fields when creating new OpenCode entries to ensure a fresh start for users.
This commit is contained in:
@@ -113,6 +113,7 @@ type OpenCodeForm = {
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
asr: string;
|
asr: string;
|
||||||
voice: string;
|
voice: string;
|
||||||
enableInterrupt: boolean;
|
enableInterrupt: boolean;
|
||||||
@@ -243,6 +244,7 @@ export function AssistantPage() {
|
|||||||
"你是一个代码助手的语音交互界面,请用简洁、口语化的方式回答用户关于代码与工程的问题。",
|
"你是一个代码助手的语音交互界面,请用简洁、口语化的方式回答用户关于代码与工程的问题。",
|
||||||
apiUrl: "http://localhost:4096",
|
apiUrl: "http://localhost:4096",
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
|
model: "",
|
||||||
asr: "",
|
asr: "",
|
||||||
voice: "",
|
voice: "",
|
||||||
enableInterrupt: true,
|
enableInterrupt: true,
|
||||||
@@ -385,8 +387,15 @@ export function AssistantPage() {
|
|||||||
setListError(error instanceof Error ? error.message : "加载助手失败");
|
setListError(error instanceof Error ? error.message : "加载助手失败");
|
||||||
}
|
}
|
||||||
} else if (assistant.type === "OpenCode") {
|
} else if (assistant.type === "OpenCode") {
|
||||||
updateOpenCodeForm("name", assistant.name);
|
void loadResources();
|
||||||
setView("create-opencode");
|
setSaveError(null);
|
||||||
|
setEditingId(assistant.id);
|
||||||
|
try {
|
||||||
|
fillOpenCodeForm(await assistantsApi.get(assistant.id));
|
||||||
|
setView("create-opencode");
|
||||||
|
} catch (error) {
|
||||||
|
setListError(error instanceof Error ? error.message : "加载助手失败");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 工作流:暂时显示占位页
|
// 工作流:暂时显示占位页
|
||||||
setDraftName(assistant.name);
|
setDraftName(assistant.name);
|
||||||
@@ -417,6 +426,7 @@ export function AssistantPage() {
|
|||||||
appId: "",
|
appId: "",
|
||||||
apiUrl: "",
|
apiUrl: "",
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
|
model: "",
|
||||||
asr: "",
|
asr: "",
|
||||||
voice: "",
|
voice: "",
|
||||||
enableInterrupt: true,
|
enableInterrupt: true,
|
||||||
@@ -437,8 +447,19 @@ export function AssistantPage() {
|
|||||||
});
|
});
|
||||||
setView("create-dify");
|
setView("create-dify");
|
||||||
} else if (draftType === "OpenCode") {
|
} else if (draftType === "OpenCode") {
|
||||||
// OpenCode 类型:进入 OpenCode 构建表单,并把已填的名称带过去
|
// OpenCode 类型:新建,清空表单 + 带入名称
|
||||||
updateOpenCodeForm("name", draftName.trim());
|
void loadResources();
|
||||||
|
setEditingId(null);
|
||||||
|
setSaveError(null);
|
||||||
|
setOpenCodeForm({
|
||||||
|
name: draftName.trim(),
|
||||||
|
prompt: "",
|
||||||
|
apiUrl: "",
|
||||||
|
apiKey: "",
|
||||||
|
asr: "",
|
||||||
|
voice: "",
|
||||||
|
enableInterrupt: true,
|
||||||
|
});
|
||||||
setView("create-opencode");
|
setView("create-opencode");
|
||||||
} else {
|
} else {
|
||||||
// 工作流:暂时显示占位页
|
// 工作流:暂时显示占位页
|
||||||
@@ -529,6 +550,7 @@ export function AssistantPage() {
|
|||||||
apiUrl: a.apiUrl,
|
apiUrl: a.apiUrl,
|
||||||
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
|
model: a.llmCredentialId ?? "",
|
||||||
asr: a.asrCredentialId ?? "",
|
asr: a.asrCredentialId ?? "",
|
||||||
voice: a.ttsCredentialId ?? "",
|
voice: a.ttsCredentialId ?? "",
|
||||||
enableInterrupt: a.enableInterrupt,
|
enableInterrupt: a.enableInterrupt,
|
||||||
@@ -578,6 +600,36 @@ export function AssistantPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- OpenCode ----
|
||||||
|
function fillOpenCodeForm(a: Assistant) {
|
||||||
|
setOpenCodeForm({
|
||||||
|
name: a.name,
|
||||||
|
prompt: a.prompt,
|
||||||
|
apiUrl: a.apiUrl,
|
||||||
|
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||||
|
apiKey: "",
|
||||||
|
asr: a.asrCredentialId ?? "",
|
||||||
|
voice: a.ttsCredentialId ?? "",
|
||||||
|
enableInterrupt: a.enableInterrupt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSaveOpenCode() {
|
||||||
|
void save(
|
||||||
|
baseUpsert({
|
||||||
|
name: openCodeForm.name.trim(),
|
||||||
|
type: "opencode",
|
||||||
|
enableInterrupt: openCodeForm.enableInterrupt,
|
||||||
|
llmCredentialId: openCodeForm.model || null,
|
||||||
|
asrCredentialId: openCodeForm.asr || null,
|
||||||
|
ttsCredentialId: openCodeForm.voice || null,
|
||||||
|
prompt: openCodeForm.prompt,
|
||||||
|
apiUrl: openCodeForm.apiUrl,
|
||||||
|
apiKey: openCodeForm.apiKey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const listItems: AssistantListItem[] = assistants.map((a) => ({
|
const listItems: AssistantListItem[] = assistants.map((a) => ({
|
||||||
id: a.id,
|
id: a.id,
|
||||||
name: a.name,
|
name: a.name,
|
||||||
@@ -1300,8 +1352,21 @@ export function AssistantPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 gap-2">
|
<div className="flex shrink-0 gap-2">
|
||||||
<Button className="gap-2">
|
{saveError && (
|
||||||
<Save size={16} />
|
<span className="self-center text-xs text-destructive">
|
||||||
|
{saveError}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className="gap-2"
|
||||||
|
disabled={saving || !openCodeForm.name.trim()}
|
||||||
|
onClick={() => void handleSaveOpenCode()}
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<Loader2 size={16} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save size={16} />
|
||||||
|
)}
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1325,7 +1390,7 @@ export function AssistantPage() {
|
|||||||
value={openCodeForm.apiKey}
|
value={openCodeForm.apiKey}
|
||||||
onChange={(value) => updateOpenCodeForm("apiKey", value)}
|
onChange={(value) => updateOpenCodeForm("apiKey", value)}
|
||||||
placeholder="请输入 OpenCode API Key"
|
placeholder="请输入 OpenCode API Key"
|
||||||
storedValueMask=""
|
storedValueMask={storedApiKeyMask}
|
||||||
/>
|
/>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
@@ -1344,20 +1409,29 @@ export function AssistantPage() {
|
|||||||
|
|
||||||
<SectionCard
|
<SectionCard
|
||||||
icon={<Brain size={18} />}
|
icon={<Brain size={18} />}
|
||||||
title="语音配置"
|
title="模型与语音配置"
|
||||||
description="配置本平台的语音识别与播报音色。"
|
description="配置 OpenCode 使用的大语言模型、语音识别与语音合成资源。"
|
||||||
>
|
>
|
||||||
<SelectField
|
<ResourceSelectField
|
||||||
|
label="大语言模型"
|
||||||
|
value={openCodeForm.model}
|
||||||
|
onChange={(value) => updateOpenCodeForm("model", value)}
|
||||||
|
options={credOptions("LLM")}
|
||||||
|
noneLabel="无"
|
||||||
|
/>
|
||||||
|
<ResourceSelectField
|
||||||
label="语音识别"
|
label="语音识别"
|
||||||
value={openCodeForm.asr}
|
value={openCodeForm.asr}
|
||||||
onChange={(value) => updateOpenCodeForm("asr", value)}
|
onChange={(value) => updateOpenCodeForm("asr", value)}
|
||||||
options={["SenseVoice", "Paraformer", "Whisper", "FunASR"]}
|
options={credOptions("ASR")}
|
||||||
|
noneLabel="无"
|
||||||
/>
|
/>
|
||||||
<SelectField
|
<ResourceSelectField
|
||||||
label="播报声音"
|
label="语音合成"
|
||||||
value={openCodeForm.voice}
|
value={openCodeForm.voice}
|
||||||
onChange={(value) => updateOpenCodeForm("voice", value)}
|
onChange={(value) => updateOpenCodeForm("voice", value)}
|
||||||
options={["晓宁", "晓美", "晓宇", "晓晨"]}
|
options={credOptions("TTS")}
|
||||||
|
noneLabel="无"
|
||||||
/>
|
/>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
@@ -1997,40 +2071,6 @@ function TextAreaField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectField({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
options,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
label?: string;
|
|
||||||
value: string;
|
|
||||||
options: string[];
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="block">
|
|
||||||
{label && (
|
|
||||||
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Select value={value} onValueChange={onChange}>
|
|
||||||
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
|
|
||||||
<SelectValue placeholder={label ? `请选择${label}` : "请选择"} />
|
|
||||||
</SelectTrigger>
|
|
||||||
|
|
||||||
<SelectContent className="border-hairline bg-popover text-popover-foreground">
|
|
||||||
{options.map((item) => (
|
|
||||||
<SelectItem key={item} value={item}>
|
|
||||||
{item}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Radix Select 不允许空字符串 value,用哨兵表示"未选/无"
|
// Radix Select 不允许空字符串 value,用哨兵表示"未选/无"
|
||||||
const NONE_VALUE = "__none__";
|
const NONE_VALUE = "__none__";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user