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:
Xin Wang
2026-06-09 12:59:07 +08:00
parent a8b6c09920
commit be0da3449c

View File

@@ -113,6 +113,7 @@ type OpenCodeForm = {
prompt: string;
apiUrl: string;
apiKey: string;
model: string;
asr: string;
voice: string;
enableInterrupt: boolean;
@@ -243,6 +244,7 @@ export function AssistantPage() {
"你是一个代码助手的语音交互界面,请用简洁、口语化的方式回答用户关于代码与工程的问题。",
apiUrl: "http://localhost:4096",
apiKey: "",
model: "",
asr: "",
voice: "",
enableInterrupt: true,
@@ -385,8 +387,15 @@ export function AssistantPage() {
setListError(error instanceof Error ? error.message : "加载助手失败");
}
} else if (assistant.type === "OpenCode") {
updateOpenCodeForm("name", assistant.name);
setView("create-opencode");
void loadResources();
setSaveError(null);
setEditingId(assistant.id);
try {
fillOpenCodeForm(await assistantsApi.get(assistant.id));
setView("create-opencode");
} catch (error) {
setListError(error instanceof Error ? error.message : "加载助手失败");
}
} else {
// 工作流:暂时显示占位页
setDraftName(assistant.name);
@@ -417,6 +426,7 @@ export function AssistantPage() {
appId: "",
apiUrl: "",
apiKey: "",
model: "",
asr: "",
voice: "",
enableInterrupt: true,
@@ -437,8 +447,19 @@ export function AssistantPage() {
});
setView("create-dify");
} else if (draftType === "OpenCode") {
// OpenCode 类型:进入 OpenCode 构建表单,并把已填的名称带过去
updateOpenCodeForm("name", draftName.trim());
// OpenCode 类型:新建,清空表单 + 带入名称
void loadResources();
setEditingId(null);
setSaveError(null);
setOpenCodeForm({
name: draftName.trim(),
prompt: "",
apiUrl: "",
apiKey: "",
asr: "",
voice: "",
enableInterrupt: true,
});
setView("create-opencode");
} else {
// 工作流:暂时显示占位页
@@ -529,6 +550,7 @@ export function AssistantPage() {
apiUrl: a.apiUrl,
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
apiKey: "",
model: a.llmCredentialId ?? "",
asr: a.asrCredentialId ?? "",
voice: a.ttsCredentialId ?? "",
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) => ({
id: a.id,
name: a.name,
@@ -1300,8 +1352,21 @@ export function AssistantPage() {
</div>
<div className="flex shrink-0 gap-2">
<Button className="gap-2">
<Save size={16} />
{saveError && (
<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>
</div>
@@ -1325,7 +1390,7 @@ export function AssistantPage() {
value={openCodeForm.apiKey}
onChange={(value) => updateOpenCodeForm("apiKey", value)}
placeholder="请输入 OpenCode API Key"
storedValueMask=""
storedValueMask={storedApiKeyMask}
/>
</SectionCard>
@@ -1344,20 +1409,29 @@ export function AssistantPage() {
<SectionCard
icon={<Brain size={18} />}
title="语音配置"
description="配置本平台的语音识别与播报音色。"
title="模型与语音配置"
description="配置 OpenCode 使用的大语言模型、语音识别与语音合成资源。"
>
<SelectField
<ResourceSelectField
label="大语言模型"
value={openCodeForm.model}
onChange={(value) => updateOpenCodeForm("model", value)}
options={credOptions("LLM")}
noneLabel="无"
/>
<ResourceSelectField
label="语音识别"
value={openCodeForm.asr}
onChange={(value) => updateOpenCodeForm("asr", value)}
options={["SenseVoice", "Paraformer", "Whisper", "FunASR"]}
options={credOptions("ASR")}
noneLabel="无"
/>
<SelectField
label="播报声音"
<ResourceSelectField
label="语音合成"
value={openCodeForm.voice}
onChange={(value) => updateOpenCodeForm("voice", value)}
options={["晓宁", "晓美", "晓宇", "晓晨"]}
options={credOptions("TTS")}
noneLabel="无"
/>
</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,用哨兵表示"未选/无"
const NONE_VALUE = "__none__";