Add OpenCode assistant type and configuration form in AssistantPage

This commit is contained in:
Xin Wang
2026-06-08 13:28:06 +08:00
parent 048c274bd1
commit f25782b99f

View File

@@ -25,6 +25,7 @@ import {
Waypoints,
AudioLines,
Square,
Terminal,
} from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -93,9 +94,25 @@ type DifyForm = {
enableInterrupt: boolean;
};
type AssistantType = "提示词" | "工作流" | "Dify" | "FastGPT";
type OpenCodeForm = {
name: string;
prompt: string;
apiUrl: string;
apiKey: string;
asr: string;
voice: string;
enableInterrupt: boolean;
};
const assistantTypes: AssistantType[] = ["提示词", "工作流", "Dify", "FastGPT"];
type AssistantType = "提示词" | "工作流" | "Dify" | "FastGPT" | "OpenCode";
const assistantTypes: AssistantType[] = [
"提示词",
"工作流",
"Dify",
"FastGPT",
"OpenCode",
];
type AssistantTypeOption = {
type: AssistantType;
@@ -135,6 +152,13 @@ const assistantTypeOptions: AssistantTypeOption[] = [
icon: <Database size={20} />,
available: true,
},
{
type: "OpenCode",
label: "使用 OpenCode 构建",
description: "对接 OpenCode 服务,通过提示词驱动代码助手并支持实时语音对话。",
icon: <Terminal size={20} />,
available: true,
},
];
type AssistantListItem = {
@@ -217,6 +241,12 @@ const mockAssistants: AssistantListItem[] = [
type: "FastGPT",
updatedAt: "2026-04-28 19:34",
},
{
id: "asst_013",
name: "OpenCode 代码助手",
type: "OpenCode",
updatedAt: "2026-06-07 14:22",
},
];
type DebugMode = "text" | "voice";
@@ -256,8 +286,24 @@ export function AssistantPage() {
voice: "晓宁",
enableInterrupt: true,
});
const [openCodeForm, setOpenCodeForm] = useState<OpenCodeForm>({
name: "OpenCode 代码助手",
prompt:
"你是一个代码助手的语音交互界面,请用简洁、口语化的方式回答用户关于代码与工程的问题。",
apiUrl: "http://localhost:4096",
apiKey: "",
asr: "SenseVoice",
voice: "晓宁",
enableInterrupt: true,
});
const [view, setView] = useState<
"list" | "choose" | "create" | "create-dify" | "create-fastgpt" | "placeholder"
| "list"
| "choose"
| "create"
| "create-dify"
| "create-fastgpt"
| "create-opencode"
| "placeholder"
>("list");
const [searchQuery, setSearchQuery] = useState("");
const [typeFilter, setTypeFilter] = useState<TypeFilter>("全部");
@@ -285,6 +331,9 @@ export function AssistantPage() {
} else if (assistant.type === "Dify") {
updateDifyForm("name", assistant.name);
setView("create-dify");
} else if (assistant.type === "OpenCode") {
updateOpenCodeForm("name", assistant.name);
setView("create-opencode");
} else {
// 工作流:暂时显示占位页
setDraftName(assistant.name);
@@ -310,6 +359,10 @@ export function AssistantPage() {
// Dify 类型:进入 Dify 构建表单,并把已填的名称带过去
updateDifyForm("name", draftName.trim());
setView("create-dify");
} else if (draftType === "OpenCode") {
// OpenCode 类型:进入 OpenCode 构建表单,并把已填的名称带过去
updateOpenCodeForm("name", draftName.trim());
setView("create-opencode");
} else {
// 工作流:暂时显示占位页
setView("placeholder");
@@ -379,6 +432,16 @@ export function AssistantPage() {
[key]: value,
}));
}
function updateOpenCodeForm<K extends keyof OpenCodeForm>(
key: K,
value: OpenCodeForm[K],
) {
setOpenCodeForm((prev) => ({
...prev,
[key]: value,
}));
}
if (view === "list") {
return (
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8">
@@ -593,7 +656,7 @@ export function AssistantPage() {
);
return (
<div className="mx-auto flex w-full max-w-[920px] flex-col gap-8">
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-8">
<div className="flex items-start justify-between gap-6">
<div>
<h1 className="font-display display-lg text-ink"></h1>
@@ -631,7 +694,7 @@ export function AssistantPage() {
<section className="flex flex-col gap-4">
<div className="text-sm font-medium text-foreground"></div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{assistantTypeOptions.map((option) => {
const selected = draftType === option.type;
@@ -951,6 +1014,110 @@ export function AssistantPage() {
);
}
if (view === "create-opencode") {
return (
<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">
<EditableTitle
value={openCodeForm.name}
onChange={(value) => updateOpenCodeForm("name", value)}
/>
</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} />
</Button>
</div>
</div>
<div className="flex min-h-0 flex-1 gap-6">
<div className="min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
<SectionCard
icon={<Terminal size={18} />}
title="OpenCode 服务配置"
description="填写 OpenCode 服务地址与 API Key 以对接代码助手。"
>
<InputField
label="OpenCode URL"
value={openCodeForm.apiUrl}
onChange={(value) => updateOpenCodeForm("apiUrl", value)}
placeholder="http://localhost:4096"
/>
<InputField
label="API Key"
value={openCodeForm.apiKey}
onChange={(value) => updateOpenCodeForm("apiKey", value)}
placeholder="请输入 OpenCode API Key"
type="password"
/>
</SectionCard>
<SectionCard
icon={<MessageSquareText size={18} />}
title="提示词"
description="描述助手的角色、能力和回答要求"
>
<TextAreaField
value={openCodeForm.prompt}
onChange={(value) => updateOpenCodeForm("prompt", value)}
placeholder="请输入提示词,描述助手的角色、能力和回答要求"
rows={8}
/>
</SectionCard>
<SectionCard
icon={<Brain size={18} />}
title="语音配置"
description="配置本平台的语音识别与播报音色。"
>
<SelectField
label="语音识别"
value={openCodeForm.asr}
onChange={(value) => updateOpenCodeForm("asr", value)}
options={["SenseVoice", "Paraformer", "Whisper", "FunASR"]}
/>
<SelectField
label="播报声音"
value={openCodeForm.voice}
onChange={(value) => updateOpenCodeForm("voice", value)}
options={["晓宁", "晓美", "晓宇", "晓晨"]}
/>
</SectionCard>
<SectionCard
icon={<Sparkles size={18} />}
title="交互策略"
description="设置实时视频对话时的交互体验"
>
<ToggleRow
title="允许用户打断"
description="用户说话时,助手可以停止当前播报并重新理解用户输入"
checked={openCodeForm.enableInterrupt}
onChange={(checked) =>
updateOpenCodeForm("enableInterrupt", checked)
}
/>
</SectionCard>
</div>
<DebugDrawer mode={debugMode} onModeChange={setDebugMode} />
</div>
</div>
);
}
return (
<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">