Files
ai-video-fullstack/frontend/src/components/pages/CreateAssistantPage.tsx
T

248 lines
6.9 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import {
Boxes,
ChevronLeft,
Loader2,
MessageSquareText,
Rocket,
Terminal,
Workflow,
} from "lucide-react";
import {
ListPageLayout,
ListPageSection,
} from "@/components/layout/list-page-layout";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
SelectionOptionCard,
selectionOptionGridClassName,
} from "@/components/ui/selection-option-card";
import {
assistantsApi,
type AssistantType as ApiAssistantType,
type AssistantUpsert,
type KnowledgeRetrievalConfig,
} from "@/lib/api";
import { defaultTurnConfig } from "@/lib/turn-config";
type BuildMethod = "提示词" | "工作流" | "智能体平台" | "OpenCode";
const typeFromBuildMethod: Record<
Exclude<BuildMethod, "智能体平台">,
ApiAssistantType
> = {
提示词: "prompt",
工作流: "workflow",
OpenCode: "opencode",
};
type AssistantTypeOption = {
type: BuildMethod;
label: string;
description: string;
icon: React.ReactNode;
/** 提示词、工作流、智能体平台已落地;OpenCode 暂时显示即将上线 */
available: boolean;
};
const assistantTypeOptions: AssistantTypeOption[] = [
{
type: "提示词",
label: "使用提示词构建",
description: "通过提示词、模型与语音快速搭建对话助手,适合大多数场景。",
icon: <MessageSquareText size={20} />,
available: true,
},
{
type: "工作流",
label: "使用工作流构建",
description: "用可视化编排串联多个节点,适合多步骤、带分支的复杂流程。",
icon: <Workflow size={20} />,
available: true,
},
{
type: "智能体平台",
label: "从智能体平台创建",
description:
"接入 Dify、FastGPT 等智能体平台,复用平台中的提示词、知识库和工作流配置。",
icon: <Boxes size={20} />,
available: true,
},
{
type: "OpenCode",
label: "使用 OpenCode 构建",
description: "对接 OpenCode 服务,通过提示词驱动代码助手并支持实时语音对话。",
icon: <Terminal size={20} />,
available: false,
},
];
function defaultKnowledgeRetrievalConfig(): KnowledgeRetrievalConfig {
return {
mode: "automatic",
topN: 5,
scoreThreshold: 0,
};
}
function baseUpsert(over: Partial<AssistantUpsert>): AssistantUpsert {
return {
name: "",
type: "prompt",
runtimeMode: "pipeline",
greeting: "",
enableInterrupt: true,
turnConfig: defaultTurnConfig(),
startup: {
executionMode: "sequential",
openingMode: "interruptible",
entryMode: "wait_user",
actions: [],
openingMessage: null,
},
visionEnabled: false,
visionModelResourceId: null,
visionPromptMode: "runtime",
visionPrompt: "",
modelResourceIds: {},
knowledgeBaseId: null,
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
toolIds: [],
prompt: "",
dynamicVariableDefinitions: {},
analysisConfig: {
enabled: false,
modelResourceId: "",
fields: [],
},
apiUrl: "",
apiKey: "",
appId: "",
graph: {},
...over,
};
}
export function CreateAssistantPage() {
const router = useRouter();
const [draftName, setDraftName] = useState("");
const [draftType, setDraftType] = useState<BuildMethod | null>(null);
const [creating, setCreating] = useState(false);
const [createError, setCreateError] = useState<string | null>(null);
async function handleCreate() {
if (!draftName.trim() || !draftType || creating) {
return;
}
setCreating(true);
setCreateError(null);
try {
const assistantType: ApiAssistantType =
draftType === "智能体平台"
? "dify"
: typeFromBuildMethod[draftType];
const created = await assistantsApi.create(
baseUpsert({
name: draftName.trim(),
type: assistantType,
}),
);
router.push(`/assistants/${created.id}`);
} catch (error) {
setCreateError(error instanceof Error ? error.message : "创建失败");
setCreating(false);
}
}
return (
<ListPageLayout
title="创建助手"
className="max-w-[1180px]"
description="先为助手取个名字,再选择构建方式。确认后将立即创建助手并进入编辑页。"
action={
<Button
variant="outline"
className="w-full shrink-0 gap-2 border-hairline-strong text-muted-foreground hover:text-foreground sm:w-auto"
onClick={() => router.push("/assistants")}
>
<ChevronLeft size={16} />
返回列表
</Button>
}
>
<ListPageSection>
<label className="block">
<div className="mb-2 text-sm font-medium text-foreground">助手名称</div>
<Input
value={draftName}
autoFocus
onChange={(event) => setDraftName(event.target.value)}
placeholder="请输入助手名称"
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
</label>
</ListPageSection>
<section className="flex flex-col gap-3">
<div className="text-sm font-medium text-foreground">构建方式</div>
<div className={selectionOptionGridClassName}>
{assistantTypeOptions.map((option) => (
<SelectionOptionCard
key={option.type}
selected={draftType === option.type}
icon={option.icon}
title={option.label}
description={option.description}
onClick={() => setDraftType(option.type)}
unselectedTrailing={
!option.available ? (
<Badge
variant="secondary"
className="h-6 bg-surface-strong px-3 text-xs text-muted-foreground"
>
即将上线
</Badge>
) : undefined
}
/>
))}
</div>
</section>
<div className="flex items-center justify-end gap-3">
{createError && (
<span className="text-xs text-destructive">{createError}</span>
)}
<Button
variant="outline"
className="border-hairline-strong text-muted-foreground hover:text-foreground"
disabled={creating}
onClick={() => router.push("/assistants")}
>
取消
</Button>
<Button
className="gap-2"
disabled={!draftName.trim() || !draftType || creating}
onClick={() => void handleCreate()}
>
{creating ? (
<Loader2 size={16} className="animate-spin" />
) : (
<Rocket size={16} />
)}
创建助手
</Button>
</div>
</ListPageLayout>
);
}