2026-06-08 13:51:28 +08:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
Boxes,
|
|
|
|
|
|
Brain,
|
|
|
|
|
|
Check,
|
2026-06-09 08:31:19 +08:00
|
|
|
|
Copy,
|
2026-06-08 13:51:28 +08:00
|
|
|
|
MessageSquareText,
|
|
|
|
|
|
MoreHorizontal,
|
|
|
|
|
|
Pencil,
|
|
|
|
|
|
Plus,
|
|
|
|
|
|
Rocket,
|
|
|
|
|
|
Sparkles,
|
|
|
|
|
|
Trash2,
|
|
|
|
|
|
Workflow,
|
|
|
|
|
|
ChevronLeft,
|
2026-06-14 19:36:12 +08:00
|
|
|
|
ChevronUp,
|
|
|
|
|
|
ChevronDown,
|
2026-06-08 13:51:28 +08:00
|
|
|
|
Save,
|
|
|
|
|
|
Terminal,
|
2026-06-09 09:48:43 +08:00
|
|
|
|
Loader2,
|
2026-06-08 13:51:28 +08:00
|
|
|
|
} from "lucide-react";
|
|
|
|
|
|
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
2026-07-12 11:08:19 +08:00
|
|
|
|
import { TurnConfigEditor } from "@/components/turn-config-editor";
|
2026-06-08 13:51:28 +08:00
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
|
import {
|
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
2026-06-14 19:36:12 +08:00
|
|
|
|
import { DataList } from "@/components/ui/data-list";
|
|
|
|
|
|
import { PageHeader } from "@/components/ui/page-header";
|
|
|
|
|
|
import { FilterPills } from "@/components/ui/filter-pills";
|
|
|
|
|
|
import { SearchInput } from "@/components/ui/search-input";
|
|
|
|
|
|
import { ListToolbar } from "@/components/ui/list-toolbar";
|
2026-07-14 12:59:41 +08:00
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
2026-06-10 14:39:52 +08:00
|
|
|
|
import { useRouter } from "next/navigation";
|
2026-06-09 09:48:43 +08:00
|
|
|
|
import {
|
|
|
|
|
|
assistantsApi,
|
|
|
|
|
|
knowledgeBasesApi,
|
2026-06-14 19:36:12 +08:00
|
|
|
|
modelResourcesApi,
|
2026-07-10 10:05:41 +08:00
|
|
|
|
toolsApi,
|
2026-06-09 09:48:43 +08:00
|
|
|
|
type Assistant,
|
|
|
|
|
|
type AssistantType as ApiAssistantType,
|
|
|
|
|
|
type AssistantUpsert,
|
|
|
|
|
|
type KnowledgeBase,
|
2026-07-12 18:57:56 +08:00
|
|
|
|
type KnowledgeRetrievalConfig,
|
2026-06-14 19:36:12 +08:00
|
|
|
|
type ModelResource,
|
2026-07-10 10:05:41 +08:00
|
|
|
|
type Tool,
|
2026-07-12 11:08:19 +08:00
|
|
|
|
type TurnConfig,
|
2026-06-09 09:48:43 +08:00
|
|
|
|
} from "@/lib/api";
|
2026-07-14 12:59:41 +08:00
|
|
|
|
import type { WorkflowSettings } from "@/components/workflow/types";
|
|
|
|
|
|
import { SectionCard } from "@/components/editor/section-card";
|
2026-06-15 10:12:41 +08:00
|
|
|
|
import {
|
|
|
|
|
|
defaultGraph,
|
|
|
|
|
|
type WorkflowGraph,
|
|
|
|
|
|
} from "@/components/workflow/specs";
|
2026-07-14 11:08:11 +08:00
|
|
|
|
import {
|
|
|
|
|
|
defaultTurnConfig,
|
|
|
|
|
|
normalizeTurnConfig,
|
|
|
|
|
|
} from "@/lib/turn-config";
|
2026-07-14 12:59:41 +08:00
|
|
|
|
import {
|
|
|
|
|
|
activeDynamicVariableDefinitions,
|
|
|
|
|
|
activeWorkflowDynamicVariableDefinitions,
|
|
|
|
|
|
} from "@/components/assistant-editor/dynamic-variables";
|
|
|
|
|
|
import { useWorkflowEditorState } from "@/components/assistant-editor/use-workflow-editor-state";
|
|
|
|
|
|
import { DebugDrawer } from "@/components/assistant-editor/debug-preview";
|
|
|
|
|
|
import {
|
|
|
|
|
|
AssistantIdentity,
|
|
|
|
|
|
EditableTitle,
|
|
|
|
|
|
EditorBackButton,
|
|
|
|
|
|
ResourceSelectField,
|
|
|
|
|
|
TextAreaField,
|
|
|
|
|
|
ToggleRow,
|
|
|
|
|
|
} from "@/components/assistant-editor/editor-controls";
|
|
|
|
|
|
import type { AssistantForm } from "@/components/assistant-editor/types";
|
|
|
|
|
|
import { PromptEditor } from "@/components/assistant-editor/prompt-editor";
|
|
|
|
|
|
import { WorkflowPage } from "@/components/assistant-editor/workflow-page";
|
2026-06-08 13:51:28 +08:00
|
|
|
|
|
2026-07-17 20:03:01 +08:00
|
|
|
|
type AgentPlatform = "dify" | "fastgpt";
|
2026-06-08 13:51:28 +08:00
|
|
|
|
|
2026-07-17 20:03:01 +08:00
|
|
|
|
type AgentPlatformForm = {
|
2026-06-08 13:51:28 +08:00
|
|
|
|
name: string;
|
2026-07-17 20:03:01 +08:00
|
|
|
|
platform: AgentPlatform;
|
2026-07-09 15:29:25 +08:00
|
|
|
|
agent: string;
|
2026-06-08 13:51:28 +08:00
|
|
|
|
asr: string;
|
|
|
|
|
|
voice: string;
|
|
|
|
|
|
enableInterrupt: boolean;
|
2026-07-12 11:08:19 +08:00
|
|
|
|
turnConfig: TurnConfig;
|
2026-06-08 13:51:28 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type OpenCodeForm = {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
prompt: string;
|
2026-07-09 15:29:25 +08:00
|
|
|
|
agent: string;
|
2026-06-09 12:59:07 +08:00
|
|
|
|
model: string;
|
2026-06-08 13:51:28 +08:00
|
|
|
|
asr: string;
|
|
|
|
|
|
voice: string;
|
|
|
|
|
|
enableInterrupt: boolean;
|
2026-07-12 11:08:19 +08:00
|
|
|
|
turnConfig: TurnConfig;
|
2026-07-07 21:50:15 +08:00
|
|
|
|
visionEnabled: boolean;
|
|
|
|
|
|
visionModelResourceId: string;
|
2026-06-08 13:51:28 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type AssistantType = "提示词" | "工作流" | "Dify" | "FastGPT" | "OpenCode";
|
2026-07-17 20:03:01 +08:00
|
|
|
|
type BuildMethod = "提示词" | "工作流" | "智能体平台" | "OpenCode";
|
2026-06-08 13:51:28 +08:00
|
|
|
|
|
|
|
|
|
|
const assistantTypes: AssistantType[] = [
|
|
|
|
|
|
"提示词",
|
|
|
|
|
|
"工作流",
|
|
|
|
|
|
"Dify",
|
|
|
|
|
|
"FastGPT",
|
|
|
|
|
|
"OpenCode",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-07-12 18:57:56 +08:00
|
|
|
|
function defaultKnowledgeRetrievalConfig(): KnowledgeRetrievalConfig {
|
|
|
|
|
|
return {
|
|
|
|
|
|
mode: "automatic",
|
|
|
|
|
|
topN: 5,
|
|
|
|
|
|
scoreThreshold: 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 09:48:43 +08:00
|
|
|
|
// 后端 type(英文) ↔ 列表展示标签(中文)
|
|
|
|
|
|
const typeToLabel: Record<ApiAssistantType, AssistantType> = {
|
|
|
|
|
|
prompt: "提示词",
|
|
|
|
|
|
workflow: "工作流",
|
|
|
|
|
|
dify: "Dify",
|
|
|
|
|
|
fastgpt: "FastGPT",
|
|
|
|
|
|
opencode: "OpenCode",
|
|
|
|
|
|
};
|
2026-07-17 20:03:01 +08:00
|
|
|
|
const typeFromBuildMethod: Record<
|
|
|
|
|
|
Exclude<BuildMethod, "智能体平台">,
|
|
|
|
|
|
ApiAssistantType
|
|
|
|
|
|
> = {
|
2026-06-10 14:39:52 +08:00
|
|
|
|
提示词: "prompt",
|
|
|
|
|
|
工作流: "workflow",
|
|
|
|
|
|
OpenCode: "opencode",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 后端 type → 编辑器视图(工作流暂为占位页)
|
|
|
|
|
|
const typeToView = {
|
|
|
|
|
|
prompt: "create",
|
2026-07-17 20:03:01 +08:00
|
|
|
|
dify: "create-agent-platform",
|
|
|
|
|
|
fastgpt: "create-agent-platform",
|
2026-06-10 14:39:52 +08:00
|
|
|
|
opencode: "create-opencode",
|
2026-06-15 10:12:41 +08:00
|
|
|
|
workflow: "workflow",
|
2026-06-10 14:39:52 +08:00
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
|
|
type View = "list" | "choose" | "loading" | (typeof typeToView)[ApiAssistantType];
|
|
|
|
|
|
|
|
|
|
|
|
// 路由驱动的页面模式:
|
|
|
|
|
|
// /assistants → list | /assistants/new → choose(引导,确认即建库) | /assistants/[id] → edit
|
|
|
|
|
|
export type AssistantPageProps =
|
|
|
|
|
|
| { mode: "list" }
|
|
|
|
|
|
| { mode: "choose" }
|
|
|
|
|
|
| { mode: "edit"; assistantId: string };
|
|
|
|
|
|
|
|
|
|
|
|
// 各类型的空白表单模板(新建用)
|
|
|
|
|
|
function blankPromptForm(name: string): AssistantForm {
|
|
|
|
|
|
return {
|
|
|
|
|
|
name,
|
|
|
|
|
|
greeting: "",
|
|
|
|
|
|
prompt: "",
|
2026-07-12 23:42:56 +08:00
|
|
|
|
dynamicVariableDefinitions: {},
|
2026-06-10 14:39:52 +08:00
|
|
|
|
runtimeMode: "pipeline",
|
|
|
|
|
|
realtimeModel: "",
|
|
|
|
|
|
model: "",
|
|
|
|
|
|
asr: "",
|
|
|
|
|
|
voice: "",
|
|
|
|
|
|
knowledgeBase: "",
|
2026-07-12 18:57:56 +08:00
|
|
|
|
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
2026-06-10 14:39:52 +08:00
|
|
|
|
enableInterrupt: true,
|
2026-07-12 11:08:19 +08:00
|
|
|
|
turnConfig: defaultTurnConfig(),
|
2026-07-07 21:50:15 +08:00
|
|
|
|
visionEnabled: false,
|
|
|
|
|
|
visionModelResourceId: "",
|
2026-07-10 10:05:41 +08:00
|
|
|
|
toolIds: [],
|
2026-06-10 14:39:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 20:03:01 +08:00
|
|
|
|
function blankAgentPlatformForm(name: string): AgentPlatformForm {
|
2026-06-10 14:39:52 +08:00
|
|
|
|
return {
|
|
|
|
|
|
name,
|
2026-07-17 20:03:01 +08:00
|
|
|
|
platform: "dify",
|
2026-07-09 15:29:25 +08:00
|
|
|
|
agent: "",
|
2026-06-10 14:39:52 +08:00
|
|
|
|
asr: "",
|
|
|
|
|
|
voice: "",
|
|
|
|
|
|
enableInterrupt: true,
|
2026-07-12 11:08:19 +08:00
|
|
|
|
turnConfig: defaultTurnConfig(),
|
2026-06-10 14:39:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function blankOpenCodeForm(name: string): OpenCodeForm {
|
|
|
|
|
|
return {
|
|
|
|
|
|
name,
|
|
|
|
|
|
prompt: "",
|
2026-07-09 15:29:25 +08:00
|
|
|
|
agent: "",
|
2026-06-10 14:39:52 +08:00
|
|
|
|
model: "",
|
|
|
|
|
|
asr: "",
|
|
|
|
|
|
voice: "",
|
|
|
|
|
|
enableInterrupt: true,
|
2026-07-12 11:08:19 +08:00
|
|
|
|
turnConfig: defaultTurnConfig(),
|
2026-07-07 21:50:15 +08:00
|
|
|
|
visionEnabled: false,
|
|
|
|
|
|
visionModelResourceId: "",
|
2026-06-10 14:39:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-06-09 09:48:43 +08:00
|
|
|
|
function formatTimestamp(iso?: string | null): string {
|
|
|
|
|
|
if (!iso) return "";
|
|
|
|
|
|
const d = new Date(iso);
|
|
|
|
|
|
if (Number.isNaN(d.getTime())) return "";
|
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
|
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(
|
|
|
|
|
|
d.getHours(),
|
|
|
|
|
|
)}:${pad(d.getMinutes())}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 13:51:28 +08:00
|
|
|
|
type AssistantTypeOption = {
|
2026-07-17 20:03:01 +08:00
|
|
|
|
type: BuildMethod;
|
2026-06-08 13:51:28 +08:00
|
|
|
|
label: string;
|
|
|
|
|
|
description: string;
|
|
|
|
|
|
icon: React.ReactNode;
|
2026-07-17 20:03:01 +08:00
|
|
|
|
/** 提示词、工作流、智能体平台已落地;OpenCode 暂时显示即将上线 */
|
2026-06-08 13:51:28 +08:00
|
|
|
|
available: boolean;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const assistantTypeOptions: AssistantTypeOption[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "提示词",
|
|
|
|
|
|
label: "使用提示词构建",
|
|
|
|
|
|
description: "通过提示词、模型与语音快速搭建对话助手,适合大多数场景。",
|
|
|
|
|
|
icon: <MessageSquareText size={20} />,
|
|
|
|
|
|
available: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "工作流",
|
|
|
|
|
|
label: "使用工作流构建",
|
|
|
|
|
|
description: "用可视化编排串联多个节点,适合多步骤、带分支的复杂流程。",
|
|
|
|
|
|
icon: <Workflow size={20} />,
|
2026-07-14 09:36:28 +08:00
|
|
|
|
available: true,
|
2026-06-08 13:51:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-07-17 20:03:01 +08:00
|
|
|
|
type: "智能体平台",
|
|
|
|
|
|
label: "从智能体平台创建",
|
|
|
|
|
|
description:
|
|
|
|
|
|
"接入 Dify、FastGPT 等智能体平台,复用平台中的提示词、知识库和工作流配置。",
|
2026-06-08 13:51:28 +08:00
|
|
|
|
icon: <Boxes size={20} />,
|
|
|
|
|
|
available: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "OpenCode",
|
|
|
|
|
|
label: "使用 OpenCode 构建",
|
|
|
|
|
|
description: "对接 OpenCode 服务,通过提示词驱动代码助手并支持实时语音对话。",
|
|
|
|
|
|
icon: <Terminal size={20} />,
|
2026-07-14 09:36:28 +08:00
|
|
|
|
available: false,
|
2026-06-08 13:51:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
type AssistantListItem = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
type: AssistantType;
|
|
|
|
|
|
updatedAt: string;
|
2026-06-14 19:36:12 +08:00
|
|
|
|
/** 原始 ISO 时间戳,用于按时间排序(updatedAt 为展示用整形字符串) */
|
|
|
|
|
|
updatedAtRaw: string | null | undefined;
|
2026-06-08 13:51:28 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type TypeFilter = "全部" | AssistantType;
|
|
|
|
|
|
|
|
|
|
|
|
const typeFilters: TypeFilter[] = ["全部", ...assistantTypes];
|
|
|
|
|
|
|
2026-06-14 19:36:12 +08:00
|
|
|
|
// 列表按更新时间排序:newest=最近更新在前(倒叙,默认) / oldest=最早更新在前
|
|
|
|
|
|
type SortOrder = "newest" | "oldest";
|
|
|
|
|
|
|
2026-06-10 14:39:52 +08:00
|
|
|
|
export function AssistantPage(props: AssistantPageProps) {
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
// 编辑中的助手 id(来自路由)
|
|
|
|
|
|
const editingId = props.mode === "edit" ? props.assistantId : null;
|
|
|
|
|
|
|
|
|
|
|
|
const [form, setForm] = useState<AssistantForm>(() => blankPromptForm(""));
|
2026-07-17 20:03:01 +08:00
|
|
|
|
const [agentPlatformForm, setAgentPlatformForm] =
|
|
|
|
|
|
useState<AgentPlatformForm>(() =>
|
|
|
|
|
|
blankAgentPlatformForm(""),
|
2026-06-10 14:39:52 +08:00
|
|
|
|
);
|
|
|
|
|
|
const [openCodeForm, setOpenCodeForm] = useState<OpenCodeForm>(() =>
|
|
|
|
|
|
blankOpenCodeForm(""),
|
|
|
|
|
|
);
|
2026-06-09 09:48:43 +08:00
|
|
|
|
const [assistants, setAssistants] = useState<Assistant[]>([]);
|
|
|
|
|
|
const [listLoading, setListLoading] = useState(true);
|
|
|
|
|
|
const [listError, setListError] = useState<string | null>(null);
|
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
|
const [saveError, setSaveError] = useState<string | null>(null);
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// 编辑模式:加载指定 id 助手失败时的错误
|
|
|
|
|
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
|
|
|
|
// 编辑模式:后端返回的打码 API Key(用于编辑页展示"当前密钥")
|
2026-06-14 19:36:12 +08:00
|
|
|
|
// 下拉数据源:模型资源 + 知识库
|
|
|
|
|
|
const [modelResources, setModelResources] = useState<ModelResource[]>([]);
|
2026-06-09 09:48:43 +08:00
|
|
|
|
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
|
2026-07-10 10:05:41 +08:00
|
|
|
|
const [tools, setTools] = useState<Tool[]>([]);
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// 视图由路由模式决定;仅编辑模式需要先 loading,等拿到助手类型后切换
|
|
|
|
|
|
const [view, setView] = useState<View>(() => {
|
|
|
|
|
|
if (props.mode === "choose") return "choose";
|
|
|
|
|
|
if (props.mode === "edit") return "loading";
|
|
|
|
|
|
return "list";
|
|
|
|
|
|
});
|
2026-06-08 13:51:28 +08:00
|
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
|
|
const [typeFilter, setTypeFilter] = useState<TypeFilter>("全部");
|
2026-06-14 19:36:12 +08:00
|
|
|
|
const [sortOrder, setSortOrder] = useState<SortOrder>("newest");
|
2026-06-08 13:51:28 +08:00
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// choose 步骤的草稿:名称与已选类型,确认后直接建库并进入编辑页
|
|
|
|
|
|
// (工作流占位页也用它展示名称与类型)
|
2026-06-08 13:51:28 +08:00
|
|
|
|
const [draftName, setDraftName] = useState("");
|
2026-07-17 20:03:01 +08:00
|
|
|
|
const [draftType, setDraftType] = useState<BuildMethod | null>(null);
|
|
|
|
|
|
const [draftAgentPlatform, setDraftAgentPlatform] =
|
|
|
|
|
|
useState<AgentPlatform | null>(null);
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// 引导页:创建请求进行中 / 创建失败
|
|
|
|
|
|
const [creating, setCreating] = useState(false);
|
|
|
|
|
|
const [createError, setCreateError] = useState<string | null>(null);
|
|
|
|
|
|
// 已保存基线(当前类型表单的 JSON);与表单不一致时保存按钮才可点击
|
|
|
|
|
|
const [savedSnapshot, setSavedSnapshot] = useState<string | null>(null);
|
2026-06-08 13:51:28 +08:00
|
|
|
|
|
2026-07-14 12:59:41 +08:00
|
|
|
|
const {
|
|
|
|
|
|
workflowName,
|
|
|
|
|
|
setWorkflowName,
|
|
|
|
|
|
workflowGraph,
|
|
|
|
|
|
setWorkflowGraph,
|
|
|
|
|
|
workflowSettings,
|
|
|
|
|
|
setWorkflowSettings,
|
|
|
|
|
|
workflowDynamicVariableDefinitions,
|
|
|
|
|
|
setWorkflowDynamicVariableDefinitions,
|
|
|
|
|
|
workflowDebugOpen,
|
|
|
|
|
|
setWorkflowDebugOpen,
|
|
|
|
|
|
workflowSettingsOpen,
|
|
|
|
|
|
setWorkflowSettingsOpen,
|
|
|
|
|
|
workflowEditingNodeId,
|
|
|
|
|
|
setWorkflowEditingNodeId,
|
|
|
|
|
|
workflowEditingEdgeId,
|
|
|
|
|
|
setWorkflowEditingEdgeId,
|
|
|
|
|
|
activeNodeId,
|
|
|
|
|
|
setActiveNodeId,
|
|
|
|
|
|
dynamicVariablesOpen,
|
|
|
|
|
|
setDynamicVariablesOpen,
|
|
|
|
|
|
} = useWorkflowEditorState();
|
2026-07-12 23:42:56 +08:00
|
|
|
|
|
|
|
|
|
|
const effectiveDynamicVariableDefinitions = activeDynamicVariableDefinitions(
|
|
|
|
|
|
[form.prompt, form.greeting],
|
|
|
|
|
|
form.dynamicVariableDefinitions,
|
|
|
|
|
|
);
|
2026-07-14 11:08:11 +08:00
|
|
|
|
const effectiveWorkflowDynamicVariableDefinitions =
|
|
|
|
|
|
activeWorkflowDynamicVariableDefinitions(
|
|
|
|
|
|
workflowGraph,
|
|
|
|
|
|
workflowDynamicVariableDefinitions,
|
|
|
|
|
|
);
|
2026-06-15 10:12:41 +08:00
|
|
|
|
|
2026-06-09 09:48:43 +08:00
|
|
|
|
const loadAssistants = useCallback(async () => {
|
|
|
|
|
|
setListLoading(true);
|
|
|
|
|
|
setListError(null);
|
|
|
|
|
|
try {
|
|
|
|
|
|
setAssistants(await assistantsApi.list());
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setListError(error instanceof Error ? error.message : "加载失败");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setListLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// 列表页挂载时拉取助手列表(与后端同步)
|
|
|
|
|
|
if (props.mode !== "list") return;
|
2026-06-09 09:48:43 +08:00
|
|
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
|
|
|
|
void loadAssistants();
|
2026-06-10 14:39:52 +08:00
|
|
|
|
}, [props.mode, loadAssistants]);
|
2026-06-09 09:48:43 +08:00
|
|
|
|
|
2026-06-14 19:36:12 +08:00
|
|
|
|
// 进入创建/编辑前加载下拉数据源(模型资源 + 知识库)
|
2026-06-09 09:48:43 +08:00
|
|
|
|
const loadResources = useCallback(async () => {
|
|
|
|
|
|
try {
|
2026-07-10 10:05:41 +08:00
|
|
|
|
const [creds, kbs, toolRows] = await Promise.all([
|
2026-06-14 19:36:12 +08:00
|
|
|
|
modelResourcesApi.list(),
|
2026-06-09 09:48:43 +08:00
|
|
|
|
knowledgeBasesApi.list(),
|
2026-07-10 10:05:41 +08:00
|
|
|
|
toolsApi.list(),
|
2026-06-09 09:48:43 +08:00
|
|
|
|
]);
|
2026-06-14 19:36:12 +08:00
|
|
|
|
setModelResources(creds);
|
2026-06-09 09:48:43 +08:00
|
|
|
|
setKnowledgeBases(kbs);
|
2026-07-10 10:05:41 +08:00
|
|
|
|
setTools(toolRows);
|
2026-06-09 09:48:43 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
// 拉取失败时下拉为空,不阻塞表单
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// 编辑页挂载时加载下拉数据源
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (props.mode !== "edit") return;
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
|
|
|
|
void loadResources();
|
|
|
|
|
|
}, [props.mode, loadResources]);
|
|
|
|
|
|
|
2026-06-09 09:48:43 +08:00
|
|
|
|
// 按资源类型生成 {value:id, label:name} 选项
|
2026-06-14 19:36:12 +08:00
|
|
|
|
const credOptions = (type: ModelResource["capability"]) =>
|
|
|
|
|
|
modelResources
|
2026-07-07 21:50:15 +08:00
|
|
|
|
.filter((c) => c.capability === type)
|
2026-06-09 09:48:43 +08:00
|
|
|
|
.map((c) => ({ value: c.id, label: c.name }));
|
2026-07-09 15:29:25 +08:00
|
|
|
|
const agentOptions = (interfaceType: "dify" | "fastgpt" | "opencode") =>
|
|
|
|
|
|
modelResources
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
(resource) =>
|
|
|
|
|
|
resource.capability === "Agent" &&
|
|
|
|
|
|
resource.interfaceType === interfaceType,
|
|
|
|
|
|
)
|
|
|
|
|
|
.map((resource) => ({ value: resource.id, label: resource.name }));
|
2026-07-07 21:50:15 +08:00
|
|
|
|
const visionModelOptionsFor = (currentModelId: string) => modelResources
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
(c) =>
|
|
|
|
|
|
c.capability === "LLM" &&
|
|
|
|
|
|
c.supportImageInput &&
|
|
|
|
|
|
c.id !== currentModelId,
|
|
|
|
|
|
)
|
|
|
|
|
|
.map((c) => ({ value: c.id, label: c.name }));
|
2026-06-09 09:48:43 +08:00
|
|
|
|
const kbOptions = knowledgeBases.map((k) => ({ value: k.id, label: k.name }));
|
|
|
|
|
|
|
2026-07-07 21:50:15 +08:00
|
|
|
|
function handlePromptVisionEnabledChange(enabled: boolean) {
|
|
|
|
|
|
updateForm("visionEnabled", enabled);
|
|
|
|
|
|
if (!enabled) updateForm("visionModelResourceId", "");
|
2026-07-07 19:41:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 21:50:15 +08:00
|
|
|
|
function handlePromptModelChange(value: string) {
|
|
|
|
|
|
updateForm("model", value);
|
|
|
|
|
|
if (form.visionModelResourceId === value) {
|
|
|
|
|
|
updateForm("visionModelResourceId", "");
|
2026-07-07 19:41:45 +08:00
|
|
|
|
}
|
2026-07-07 21:50:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleOpenCodeVisionEnabledChange(enabled: boolean) {
|
|
|
|
|
|
updateOpenCodeForm("visionEnabled", enabled);
|
|
|
|
|
|
if (!enabled) updateOpenCodeForm("visionModelResourceId", "");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleOpenCodeModelChange(value: string) {
|
|
|
|
|
|
updateOpenCodeForm("model", value);
|
|
|
|
|
|
if (openCodeForm.visionModelResourceId === value) {
|
|
|
|
|
|
updateOpenCodeForm("visionModelResourceId", "");
|
2026-07-07 19:41:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 13:51:28 +08:00
|
|
|
|
function startCreate() {
|
2026-06-10 14:39:52 +08:00
|
|
|
|
router.push("/assistants/new");
|
2026-06-09 09:48:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 19:36:12 +08:00
|
|
|
|
// 把后端 Assistant 回填进提示词表单(model/asr/voice 等存模型资源 id)
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// 返回回填后的表单,供调用方记录"已保存基线"
|
|
|
|
|
|
function fillPromptForm(a: Assistant): AssistantForm {
|
|
|
|
|
|
const next: AssistantForm = {
|
2026-06-09 09:48:43 +08:00
|
|
|
|
name: a.name,
|
|
|
|
|
|
greeting: a.greeting,
|
2026-06-09 10:37:29 +08:00
|
|
|
|
prompt: a.prompt,
|
2026-07-12 23:42:56 +08:00
|
|
|
|
dynamicVariableDefinitions: a.dynamicVariableDefinitions ?? {},
|
2026-06-09 09:48:43 +08:00
|
|
|
|
runtimeMode: a.runtimeMode,
|
2026-06-14 19:36:12 +08:00
|
|
|
|
realtimeModel: a.modelResourceIds.Realtime ?? "",
|
|
|
|
|
|
model: a.modelResourceIds.LLM ?? "",
|
|
|
|
|
|
asr: a.modelResourceIds.ASR ?? "",
|
|
|
|
|
|
voice: a.modelResourceIds.TTS ?? "",
|
2026-06-09 09:48:43 +08:00
|
|
|
|
knowledgeBase: a.knowledgeBaseId ?? "",
|
2026-07-12 18:57:56 +08:00
|
|
|
|
knowledgeRetrievalConfig:
|
|
|
|
|
|
a.knowledgeRetrievalConfig ?? defaultKnowledgeRetrievalConfig(),
|
2026-06-09 09:48:43 +08:00
|
|
|
|
enableInterrupt: a.enableInterrupt,
|
2026-07-14 11:08:11 +08:00
|
|
|
|
turnConfig: normalizeTurnConfig(a.turnConfig),
|
2026-07-07 21:50:15 +08:00
|
|
|
|
visionEnabled: a.visionEnabled,
|
|
|
|
|
|
visionModelResourceId: a.visionModelResourceId ?? "",
|
2026-07-10 10:05:41 +08:00
|
|
|
|
toolIds: a.toolIds ?? [],
|
2026-06-10 14:39:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
setForm(next);
|
|
|
|
|
|
return next;
|
2026-06-09 09:48:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// 编辑:跳转到 /assistants/[id],由编辑页按助手类型回填表单
|
|
|
|
|
|
function handleEdit(assistant: AssistantListItem) {
|
|
|
|
|
|
router.push(`/assistants/${assistant.id}`);
|
2026-06-08 13:51:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// 引导页确认:直接创建到数据库拿到 id,再进入该助手的编辑页
|
|
|
|
|
|
async function confirmType() {
|
2026-07-17 20:03:01 +08:00
|
|
|
|
if (
|
|
|
|
|
|
!draftName.trim() ||
|
|
|
|
|
|
!draftType ||
|
|
|
|
|
|
(draftType === "智能体平台" && !draftAgentPlatform) ||
|
|
|
|
|
|
creating
|
|
|
|
|
|
) {
|
2026-06-08 13:51:28 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 14:39:52 +08:00
|
|
|
|
setCreating(true);
|
|
|
|
|
|
setCreateError(null);
|
|
|
|
|
|
try {
|
2026-07-17 20:03:01 +08:00
|
|
|
|
let assistantType: ApiAssistantType;
|
|
|
|
|
|
if (draftType === "智能体平台") {
|
|
|
|
|
|
if (!draftAgentPlatform) return;
|
|
|
|
|
|
assistantType = draftAgentPlatform;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
assistantType = typeFromBuildMethod[draftType];
|
|
|
|
|
|
}
|
2026-06-10 14:39:52 +08:00
|
|
|
|
const created = await assistantsApi.create(
|
|
|
|
|
|
baseUpsert({
|
|
|
|
|
|
name: draftName.trim(),
|
2026-07-17 20:03:01 +08:00
|
|
|
|
type: assistantType,
|
2026-06-10 14:39:52 +08:00
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
router.push(`/assistants/${created.id}`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setCreateError(error instanceof Error ? error.message : "创建失败");
|
|
|
|
|
|
setCreating(false);
|
2026-06-08 13:51:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 09:48:43 +08:00
|
|
|
|
// 复制助手:服务端整行复制(含真 key,密钥不经浏览器)
|
|
|
|
|
|
async function handleDuplicate(assistant: AssistantListItem) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await assistantsApi.duplicate(assistant.id);
|
|
|
|
|
|
await loadAssistants();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setListError(error instanceof Error ? error.message : "复制失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-09 08:31:19 +08:00
|
|
|
|
|
2026-06-09 09:48:43 +08:00
|
|
|
|
// 删除助手
|
|
|
|
|
|
async function handleDelete(id: string) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await assistantsApi.remove(id);
|
|
|
|
|
|
await loadAssistants();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setListError(error instanceof Error ? error.message : "删除失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-09 08:31:19 +08:00
|
|
|
|
|
2026-06-09 10:37:29 +08:00
|
|
|
|
// 平铺字段的空白 upsert,各类型按需覆盖(后端会按 type 清掉无关字段)
|
|
|
|
|
|
function baseUpsert(over: Partial<AssistantUpsert>): AssistantUpsert {
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: "",
|
2026-06-09 09:48:43 +08:00
|
|
|
|
type: "prompt",
|
|
|
|
|
|
runtimeMode: "pipeline",
|
|
|
|
|
|
greeting: "",
|
2026-06-09 10:37:29 +08:00
|
|
|
|
enableInterrupt: true,
|
2026-07-12 11:08:19 +08:00
|
|
|
|
turnConfig: defaultTurnConfig(),
|
2026-07-07 21:50:15 +08:00
|
|
|
|
visionEnabled: false,
|
|
|
|
|
|
visionModelResourceId: null,
|
2026-06-14 19:36:12 +08:00
|
|
|
|
modelResourceIds: {},
|
2026-06-09 09:48:43 +08:00
|
|
|
|
knowledgeBaseId: null,
|
2026-07-12 18:57:56 +08:00
|
|
|
|
knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
|
2026-07-10 10:05:41 +08:00
|
|
|
|
toolIds: [],
|
2026-06-09 10:37:29 +08:00
|
|
|
|
prompt: "",
|
2026-07-12 23:42:56 +08:00
|
|
|
|
dynamicVariableDefinitions: {},
|
2026-06-09 10:37:29 +08:00
|
|
|
|
apiUrl: "",
|
|
|
|
|
|
apiKey: "",
|
|
|
|
|
|
appId: "",
|
|
|
|
|
|
graph: {},
|
|
|
|
|
|
...over,
|
2026-06-09 09:48:43 +08:00
|
|
|
|
};
|
2026-06-09 10:37:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-12 11:08:19 +08:00
|
|
|
|
// 保存后停留在编辑页,并把当前表单记为已保存基线(按钮随之置灰)。
|
2026-06-09 10:37:29 +08:00
|
|
|
|
async function save(payload: AssistantUpsert) {
|
2026-06-10 14:39:52 +08:00
|
|
|
|
if (!editingId) return;
|
2026-06-09 10:37:29 +08:00
|
|
|
|
setSaving(true);
|
|
|
|
|
|
setSaveError(null);
|
2026-06-09 09:48:43 +08:00
|
|
|
|
try {
|
2026-07-12 11:08:19 +08:00
|
|
|
|
await assistantsApi.update(editingId, payload);
|
2026-06-10 14:39:52 +08:00
|
|
|
|
if (view === "create") {
|
|
|
|
|
|
setSavedSnapshot(JSON.stringify(form));
|
2026-07-17 20:03:01 +08:00
|
|
|
|
} else if (view === "create-agent-platform") {
|
|
|
|
|
|
const next = { ...agentPlatformForm };
|
|
|
|
|
|
setAgentPlatformForm(next);
|
2026-06-10 14:39:52 +08:00
|
|
|
|
setSavedSnapshot(JSON.stringify(next));
|
|
|
|
|
|
} else if (view === "create-opencode") {
|
2026-07-09 15:29:25 +08:00
|
|
|
|
const next = { ...openCodeForm };
|
2026-06-10 14:39:52 +08:00
|
|
|
|
setOpenCodeForm(next);
|
|
|
|
|
|
setSavedSnapshot(JSON.stringify(next));
|
2026-06-15 10:12:41 +08:00
|
|
|
|
} else if (view === "workflow") {
|
|
|
|
|
|
setSavedSnapshot(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
name: workflowName,
|
|
|
|
|
|
graph: workflowGraph,
|
|
|
|
|
|
settings: workflowSettings,
|
2026-07-14 11:08:11 +08:00
|
|
|
|
dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions,
|
2026-06-15 10:12:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
);
|
2026-06-10 14:39:52 +08:00
|
|
|
|
}
|
2026-06-09 09:48:43 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setSaveError(error instanceof Error ? error.message : "保存失败");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 10:37:29 +08:00
|
|
|
|
function handleSavePrompt() {
|
|
|
|
|
|
void save(
|
|
|
|
|
|
baseUpsert({
|
|
|
|
|
|
name: form.name.trim(),
|
|
|
|
|
|
type: "prompt",
|
|
|
|
|
|
runtimeMode: form.runtimeMode,
|
|
|
|
|
|
greeting: form.greeting,
|
|
|
|
|
|
enableInterrupt: form.enableInterrupt,
|
2026-07-12 11:08:19 +08:00
|
|
|
|
turnConfig: form.turnConfig,
|
2026-07-07 21:50:15 +08:00
|
|
|
|
visionEnabled: form.visionEnabled,
|
|
|
|
|
|
visionModelResourceId: form.visionModelResourceId || null,
|
2026-06-14 19:36:12 +08:00
|
|
|
|
modelResourceIds: {
|
|
|
|
|
|
...(form.model ? { LLM: form.model } : {}),
|
|
|
|
|
|
...(form.asr ? { ASR: form.asr } : {}),
|
|
|
|
|
|
...(form.voice ? { TTS: form.voice } : {}),
|
|
|
|
|
|
...(form.realtimeModel ? { Realtime: form.realtimeModel } : {}),
|
|
|
|
|
|
},
|
2026-07-12 13:58:47 +08:00
|
|
|
|
knowledgeBaseId: form.runtimeMode === "pipeline" ? form.knowledgeBase || null : null,
|
2026-07-12 18:57:56 +08:00
|
|
|
|
knowledgeRetrievalConfig: form.knowledgeRetrievalConfig,
|
2026-07-10 10:05:41 +08:00
|
|
|
|
toolIds: form.toolIds,
|
2026-06-09 10:37:29 +08:00
|
|
|
|
prompt: form.prompt,
|
2026-07-12 23:42:56 +08:00
|
|
|
|
dynamicVariableDefinitions: effectiveDynamicVariableDefinitions,
|
2026-06-09 10:37:29 +08:00
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 20:03:01 +08:00
|
|
|
|
// ---- 外部智能体平台(Dify / FastGPT)----
|
|
|
|
|
|
function fillAgentPlatformForm(a: Assistant): AgentPlatformForm {
|
|
|
|
|
|
const next: AgentPlatformForm = {
|
2026-06-09 10:37:29 +08:00
|
|
|
|
name: a.name,
|
2026-07-17 20:03:01 +08:00
|
|
|
|
platform: a.type === "fastgpt" ? "fastgpt" : "dify",
|
2026-07-09 15:29:25 +08:00
|
|
|
|
agent: a.modelResourceIds.Agent ?? "",
|
2026-06-14 19:36:12 +08:00
|
|
|
|
asr: a.modelResourceIds.ASR ?? "",
|
|
|
|
|
|
voice: a.modelResourceIds.TTS ?? "",
|
2026-06-09 10:37:29 +08:00
|
|
|
|
enableInterrupt: a.enableInterrupt,
|
2026-07-14 11:08:11 +08:00
|
|
|
|
turnConfig: normalizeTurnConfig(a.turnConfig),
|
2026-06-10 14:39:52 +08:00
|
|
|
|
};
|
2026-07-17 20:03:01 +08:00
|
|
|
|
setAgentPlatformForm(next);
|
2026-06-10 14:39:52 +08:00
|
|
|
|
return next;
|
2026-06-09 10:37:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 20:03:01 +08:00
|
|
|
|
function handleSaveAgentPlatform() {
|
2026-06-09 10:37:29 +08:00
|
|
|
|
void save(
|
|
|
|
|
|
baseUpsert({
|
2026-07-17 20:03:01 +08:00
|
|
|
|
name: agentPlatformForm.name.trim(),
|
|
|
|
|
|
type: agentPlatformForm.platform,
|
|
|
|
|
|
enableInterrupt: agentPlatformForm.enableInterrupt,
|
|
|
|
|
|
turnConfig: agentPlatformForm.turnConfig,
|
2026-06-14 19:36:12 +08:00
|
|
|
|
modelResourceIds: {
|
2026-07-17 20:03:01 +08:00
|
|
|
|
...(agentPlatformForm.agent
|
|
|
|
|
|
? { Agent: agentPlatformForm.agent }
|
|
|
|
|
|
: {}),
|
|
|
|
|
|
...(agentPlatformForm.asr ? { ASR: agentPlatformForm.asr } : {}),
|
|
|
|
|
|
...(agentPlatformForm.voice
|
|
|
|
|
|
? { TTS: agentPlatformForm.voice }
|
|
|
|
|
|
: {}),
|
2026-06-14 19:36:12 +08:00
|
|
|
|
},
|
2026-06-09 10:37:29 +08:00
|
|
|
|
}),
|
|
|
|
|
|
);
|
2026-06-09 09:48:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 12:59:07 +08:00
|
|
|
|
// ---- OpenCode ----
|
2026-06-10 14:39:52 +08:00
|
|
|
|
function fillOpenCodeForm(a: Assistant): OpenCodeForm {
|
|
|
|
|
|
const next: OpenCodeForm = {
|
2026-06-09 12:59:07 +08:00
|
|
|
|
name: a.name,
|
|
|
|
|
|
prompt: a.prompt,
|
2026-07-09 15:29:25 +08:00
|
|
|
|
agent: a.modelResourceIds.Agent ?? "",
|
2026-06-14 19:36:12 +08:00
|
|
|
|
model: a.modelResourceIds.LLM ?? "",
|
|
|
|
|
|
asr: a.modelResourceIds.ASR ?? "",
|
|
|
|
|
|
voice: a.modelResourceIds.TTS ?? "",
|
2026-06-09 12:59:07 +08:00
|
|
|
|
enableInterrupt: a.enableInterrupt,
|
2026-07-14 11:08:11 +08:00
|
|
|
|
turnConfig: normalizeTurnConfig(a.turnConfig),
|
2026-07-07 21:50:15 +08:00
|
|
|
|
visionEnabled: a.visionEnabled,
|
|
|
|
|
|
visionModelResourceId: a.visionModelResourceId ?? "",
|
2026-06-10 14:39:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
setOpenCodeForm(next);
|
|
|
|
|
|
return next;
|
2026-06-09 12:59:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// 编辑模式:按路由中的 id 拉取助手,回填对应类型的表单后再切换视图
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (props.mode !== "edit" || !editingId) return;
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
|
|
|
|
|
|
void (async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const assistant = await assistantsApi.get(editingId);
|
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
|
if (assistant.type === "prompt") {
|
|
|
|
|
|
setSavedSnapshot(JSON.stringify(fillPromptForm(assistant)));
|
2026-07-17 20:03:01 +08:00
|
|
|
|
} else if (
|
|
|
|
|
|
assistant.type === "fastgpt" ||
|
|
|
|
|
|
assistant.type === "dify"
|
|
|
|
|
|
) {
|
|
|
|
|
|
setSavedSnapshot(JSON.stringify(fillAgentPlatformForm(assistant)));
|
2026-06-10 14:39:52 +08:00
|
|
|
|
} else if (assistant.type === "opencode") {
|
|
|
|
|
|
setSavedSnapshot(JSON.stringify(fillOpenCodeForm(assistant)));
|
|
|
|
|
|
} else {
|
2026-06-15 10:12:41 +08:00
|
|
|
|
// 工作流:回填名称与图(空图回落到默认 开始→智能体→结束)
|
|
|
|
|
|
const graph =
|
|
|
|
|
|
assistant.graph &&
|
|
|
|
|
|
Array.isArray((assistant.graph as WorkflowGraph).nodes) &&
|
|
|
|
|
|
(assistant.graph as WorkflowGraph).nodes.length > 0
|
|
|
|
|
|
? (assistant.graph as WorkflowGraph)
|
|
|
|
|
|
: defaultGraph();
|
|
|
|
|
|
const wfSettings: WorkflowSettings = {
|
2026-07-14 09:36:28 +08:00
|
|
|
|
llm:
|
|
|
|
|
|
graph.settings?.defaultLlmResourceId ||
|
|
|
|
|
|
assistant.modelResourceIds.LLM,
|
2026-07-13 16:13:27 +08:00
|
|
|
|
asr: graph.settings?.defaultAsrResourceId || assistant.modelResourceIds.ASR,
|
|
|
|
|
|
tts: graph.settings?.defaultTtsResourceId || assistant.modelResourceIds.TTS,
|
2026-07-14 09:36:28 +08:00
|
|
|
|
toolIds: graph.settings?.toolIds ?? [],
|
|
|
|
|
|
knowledgeBaseId:
|
|
|
|
|
|
graph.settings?.knowledgeBaseId || assistant.knowledgeBaseId || "",
|
|
|
|
|
|
knowledgeRetrievalConfig: {
|
|
|
|
|
|
mode:
|
|
|
|
|
|
graph.settings?.knowledgeMode ||
|
|
|
|
|
|
assistant.knowledgeRetrievalConfig.mode,
|
|
|
|
|
|
topN:
|
|
|
|
|
|
graph.settings?.knowledgeTopN ??
|
|
|
|
|
|
assistant.knowledgeRetrievalConfig.topN,
|
|
|
|
|
|
scoreThreshold:
|
|
|
|
|
|
graph.settings?.knowledgeScoreThreshold ??
|
|
|
|
|
|
assistant.knowledgeRetrievalConfig.scoreThreshold,
|
|
|
|
|
|
},
|
2026-07-13 16:13:27 +08:00
|
|
|
|
globalPrompt: graph.settings?.globalPrompt ?? "",
|
2026-07-14 11:08:11 +08:00
|
|
|
|
allowInterrupt:
|
|
|
|
|
|
graph.settings?.enableInterrupt ?? assistant.enableInterrupt,
|
|
|
|
|
|
turnConfig: normalizeTurnConfig(
|
|
|
|
|
|
graph.settings?.turnConfig ?? assistant.turnConfig,
|
|
|
|
|
|
),
|
2026-06-15 10:12:41 +08:00
|
|
|
|
};
|
|
|
|
|
|
setWorkflowName(assistant.name);
|
|
|
|
|
|
setWorkflowGraph(graph);
|
|
|
|
|
|
setWorkflowSettings(wfSettings);
|
2026-07-14 11:08:11 +08:00
|
|
|
|
const dynamicVariableDefinitions =
|
|
|
|
|
|
activeWorkflowDynamicVariableDefinitions(
|
|
|
|
|
|
graph,
|
|
|
|
|
|
assistant.dynamicVariableDefinitions ?? {},
|
|
|
|
|
|
);
|
2026-07-13 16:13:27 +08:00
|
|
|
|
setWorkflowDynamicVariableDefinitions(
|
2026-07-14 11:08:11 +08:00
|
|
|
|
dynamicVariableDefinitions,
|
2026-07-13 16:13:27 +08:00
|
|
|
|
);
|
2026-06-15 10:12:41 +08:00
|
|
|
|
setSavedSnapshot(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
name: assistant.name,
|
|
|
|
|
|
graph,
|
|
|
|
|
|
settings: wfSettings,
|
2026-07-14 11:08:11 +08:00
|
|
|
|
dynamicVariableDefinitions,
|
2026-06-15 10:12:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
);
|
2026-06-10 14:39:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
setView(typeToView[assistant.type]);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
setLoadError(
|
|
|
|
|
|
error instanceof Error ? error.message : "加载助手失败",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true;
|
|
|
|
|
|
};
|
2026-07-14 12:59:41 +08:00
|
|
|
|
}, [
|
|
|
|
|
|
props.mode,
|
|
|
|
|
|
editingId,
|
|
|
|
|
|
setWorkflowDynamicVariableDefinitions,
|
|
|
|
|
|
setWorkflowGraph,
|
|
|
|
|
|
setWorkflowName,
|
|
|
|
|
|
setWorkflowSettings,
|
|
|
|
|
|
]);
|
2026-06-10 14:39:52 +08:00
|
|
|
|
|
2026-06-09 12:59:07 +08:00
|
|
|
|
function handleSaveOpenCode() {
|
|
|
|
|
|
void save(
|
|
|
|
|
|
baseUpsert({
|
|
|
|
|
|
name: openCodeForm.name.trim(),
|
|
|
|
|
|
type: "opencode",
|
|
|
|
|
|
enableInterrupt: openCodeForm.enableInterrupt,
|
2026-07-12 11:08:19 +08:00
|
|
|
|
turnConfig: openCodeForm.turnConfig,
|
2026-07-07 21:50:15 +08:00
|
|
|
|
visionEnabled: openCodeForm.visionEnabled,
|
|
|
|
|
|
visionModelResourceId: openCodeForm.visionModelResourceId || null,
|
2026-06-14 19:36:12 +08:00
|
|
|
|
modelResourceIds: {
|
2026-07-09 15:29:25 +08:00
|
|
|
|
...(openCodeForm.agent ? { Agent: openCodeForm.agent } : {}),
|
2026-06-14 19:36:12 +08:00
|
|
|
|
...(openCodeForm.model ? { LLM: openCodeForm.model } : {}),
|
|
|
|
|
|
...(openCodeForm.asr ? { ASR: openCodeForm.asr } : {}),
|
|
|
|
|
|
...(openCodeForm.voice ? { TTS: openCodeForm.voice } : {}),
|
|
|
|
|
|
},
|
2026-06-09 12:59:07 +08:00
|
|
|
|
prompt: openCodeForm.prompt,
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:12:41 +08:00
|
|
|
|
function handleSaveWorkflow() {
|
|
|
|
|
|
void save(
|
|
|
|
|
|
baseUpsert({
|
|
|
|
|
|
name: workflowName.trim(),
|
|
|
|
|
|
type: "workflow",
|
|
|
|
|
|
enableInterrupt: workflowSettings.allowInterrupt,
|
2026-07-12 11:08:19 +08:00
|
|
|
|
turnConfig: workflowSettings.turnConfig,
|
2026-06-15 10:12:41 +08:00
|
|
|
|
modelResourceIds: {
|
|
|
|
|
|
...(workflowSettings.llm ? { LLM: workflowSettings.llm } : {}),
|
|
|
|
|
|
...(workflowSettings.asr ? { ASR: workflowSettings.asr } : {}),
|
|
|
|
|
|
...(workflowSettings.tts ? { TTS: workflowSettings.tts } : {}),
|
|
|
|
|
|
},
|
2026-07-14 09:36:28 +08:00
|
|
|
|
knowledgeBaseId: workflowSettings.knowledgeBaseId || null,
|
|
|
|
|
|
knowledgeRetrievalConfig: workflowSettings.knowledgeRetrievalConfig,
|
|
|
|
|
|
toolIds: workflowSettings.toolIds,
|
2026-06-15 10:12:41 +08:00
|
|
|
|
graph: workflowGraph as unknown as Record<string, unknown>,
|
2026-07-14 11:08:11 +08:00
|
|
|
|
dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions,
|
2026-06-15 10:12:41 +08:00
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 14:39:52 +08:00
|
|
|
|
// 当前编辑器表单相对已保存基线是否有改动(决定保存按钮是否可点)
|
|
|
|
|
|
const activeFormJson =
|
|
|
|
|
|
view === "create"
|
|
|
|
|
|
? JSON.stringify(form)
|
2026-07-17 20:03:01 +08:00
|
|
|
|
: view === "create-agent-platform"
|
|
|
|
|
|
? JSON.stringify(agentPlatformForm)
|
|
|
|
|
|
: view === "create-opencode"
|
|
|
|
|
|
? JSON.stringify(openCodeForm)
|
|
|
|
|
|
: view === "workflow"
|
|
|
|
|
|
? JSON.stringify({
|
|
|
|
|
|
name: workflowName,
|
|
|
|
|
|
graph: workflowGraph,
|
|
|
|
|
|
settings: workflowSettings,
|
|
|
|
|
|
dynamicVariableDefinitions:
|
|
|
|
|
|
effectiveWorkflowDynamicVariableDefinitions,
|
|
|
|
|
|
})
|
|
|
|
|
|
: null;
|
2026-06-10 14:39:52 +08:00
|
|
|
|
const dirty =
|
|
|
|
|
|
activeFormJson !== null &&
|
|
|
|
|
|
savedSnapshot !== null &&
|
|
|
|
|
|
activeFormJson !== savedSnapshot;
|
|
|
|
|
|
|
2026-06-09 09:48:43 +08:00
|
|
|
|
const listItems: AssistantListItem[] = assistants.map((a) => ({
|
|
|
|
|
|
id: a.id,
|
|
|
|
|
|
name: a.name,
|
|
|
|
|
|
type: typeToLabel[a.type],
|
|
|
|
|
|
updatedAt: formatTimestamp(a.updatedAt),
|
2026-06-14 19:36:12 +08:00
|
|
|
|
updatedAtRaw: a.updatedAt,
|
2026-06-09 09:48:43 +08:00
|
|
|
|
}));
|
|
|
|
|
|
const filteredAssistants = listItems.filter((assistant) => {
|
2026-06-08 13:51:28 +08:00
|
|
|
|
if (typeFilter !== "全部" && assistant.type !== typeFilter) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const keyword = searchQuery.trim().toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
if (!keyword) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [assistant.name, assistant.id, assistant.type, assistant.updatedAt]
|
|
|
|
|
|
.join(" ")
|
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
|
.includes(keyword);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-14 19:36:12 +08:00
|
|
|
|
// 按更新时间排序(以原始 ISO 时间戳为准);缺失时间戳视为最早,id 作为稳定的次级排序键
|
|
|
|
|
|
const timeValue = (iso: string | null | undefined) => {
|
|
|
|
|
|
const t = iso ? new Date(iso).getTime() : NaN;
|
|
|
|
|
|
return Number.isNaN(t) ? 0 : t;
|
|
|
|
|
|
};
|
|
|
|
|
|
const sortedAssistants = [...filteredAssistants].sort((a, b) => {
|
|
|
|
|
|
const diff = timeValue(b.updatedAtRaw) - timeValue(a.updatedAtRaw);
|
|
|
|
|
|
if (diff !== 0) return sortOrder === "newest" ? diff : -diff;
|
|
|
|
|
|
return a.id.localeCompare(b.id);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-08 13:51:28 +08:00
|
|
|
|
const pageSize = 5;
|
2026-06-14 19:36:12 +08:00
|
|
|
|
const totalPages = Math.max(1, Math.ceil(sortedAssistants.length / pageSize));
|
2026-06-08 13:51:28 +08:00
|
|
|
|
const safeCurrentPage = Math.min(currentPage, totalPages);
|
|
|
|
|
|
const pageStart = (safeCurrentPage - 1) * pageSize;
|
|
|
|
|
|
const pageEnd = pageStart + pageSize;
|
2026-06-14 19:36:12 +08:00
|
|
|
|
const paginatedAssistants = sortedAssistants.slice(pageStart, pageEnd);
|
2026-06-08 13:51:28 +08:00
|
|
|
|
|
|
|
|
|
|
function handleSearchChange(value: string) {
|
|
|
|
|
|
setSearchQuery(value);
|
|
|
|
|
|
setCurrentPage(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleFilterChange(filter: TypeFilter) {
|
|
|
|
|
|
setTypeFilter(filter);
|
|
|
|
|
|
setCurrentPage(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 19:36:12 +08:00
|
|
|
|
function handleSortChange(order: SortOrder) {
|
|
|
|
|
|
setSortOrder(order);
|
|
|
|
|
|
setCurrentPage(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleSortOrder() {
|
|
|
|
|
|
handleSortChange(sortOrder === "newest" ? "oldest" : "newest");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 13:51:28 +08:00
|
|
|
|
function updateForm<K extends keyof AssistantForm>(
|
|
|
|
|
|
key: K,
|
|
|
|
|
|
value: AssistantForm[K],
|
|
|
|
|
|
) {
|
|
|
|
|
|
setForm((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[key]: value,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 20:03:01 +08:00
|
|
|
|
function updateAgentPlatformForm<K extends keyof AgentPlatformForm>(
|
2026-06-08 13:51:28 +08:00
|
|
|
|
key: K,
|
2026-07-17 20:03:01 +08:00
|
|
|
|
value: AgentPlatformForm[K],
|
2026-06-08 13:51:28 +08:00
|
|
|
|
) {
|
2026-07-17 20:03:01 +08:00
|
|
|
|
setAgentPlatformForm((prev) => ({
|
2026-06-08 13:51:28 +08:00
|
|
|
|
...prev,
|
|
|
|
|
|
[key]: value,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 20:03:01 +08:00
|
|
|
|
function handleAgentPlatformChange(platform: AgentPlatform) {
|
|
|
|
|
|
setAgentPlatformForm((prev) => ({
|
2026-06-08 13:51:28 +08:00
|
|
|
|
...prev,
|
2026-07-17 20:03:01 +08:00
|
|
|
|
platform,
|
|
|
|
|
|
agent: prev.platform === platform ? prev.agent : "",
|
2026-06-08 13:51:28 +08:00
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateOpenCodeForm<K extends keyof OpenCodeForm>(
|
|
|
|
|
|
key: K,
|
|
|
|
|
|
value: OpenCodeForm[K],
|
|
|
|
|
|
) {
|
|
|
|
|
|
setOpenCodeForm((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[key]: value,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
2026-06-10 14:39:52 +08:00
|
|
|
|
if (view === "loading") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full items-center justify-center">
|
|
|
|
|
|
{loadError ? (
|
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
|
<div className="font-medium text-destructive">加载助手失败</div>
|
|
|
|
|
|
<div className="mt-2 text-sm text-muted-foreground">
|
|
|
|
|
|
{loadError}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="mt-4 border-hairline-strong text-muted-foreground hover:text-foreground"
|
|
|
|
|
|
onClick={() => router.push("/assistants")}
|
|
|
|
|
|
>
|
|
|
|
|
|
返回列表
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
|
<Loader2 size={16} className="animate-spin" />
|
|
|
|
|
|
正在加载助手…
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 13:51:28 +08:00
|
|
|
|
if (view === "list") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8">
|
2026-06-14 19:36:12 +08:00
|
|
|
|
<PageHeader
|
|
|
|
|
|
title="助手列表"
|
|
|
|
|
|
description="管理已有的视频助手,支持提示词、工作流、Dify 和 FastGPT 类型。"
|
|
|
|
|
|
action={
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="lg"
|
|
|
|
|
|
className="w-full gap-2 sm:w-auto"
|
|
|
|
|
|
onClick={startCreate}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus size={16} />
|
|
|
|
|
|
创建助手
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
2026-06-08 13:51:28 +08:00
|
|
|
|
|
|
|
|
|
|
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
|
2026-06-14 19:36:12 +08:00
|
|
|
|
<ListToolbar
|
|
|
|
|
|
filters={
|
|
|
|
|
|
<FilterPills
|
|
|
|
|
|
options={typeFilters}
|
|
|
|
|
|
value={typeFilter}
|
|
|
|
|
|
onChange={handleFilterChange}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
2026-06-14 19:36:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
search={
|
|
|
|
|
|
<SearchInput
|
2026-06-08 13:51:28 +08:00
|
|
|
|
value={searchQuery}
|
2026-06-14 19:36:12 +08:00
|
|
|
|
onChange={handleSearchChange}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
placeholder="搜索助手名称、类型或 ID..."
|
2026-06-14 19:36:12 +08:00
|
|
|
|
className="lg:w-[320px]"
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
2026-06-14 19:36:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
/>
|
2026-06-08 13:51:28 +08:00
|
|
|
|
|
2026-06-14 19:36:12 +08:00
|
|
|
|
<DataList<AssistantListItem>
|
|
|
|
|
|
rows={paginatedAssistants}
|
|
|
|
|
|
rowKey={(assistant) => assistant.id}
|
|
|
|
|
|
loading={listLoading}
|
|
|
|
|
|
loadingText="正在加载助手列表…"
|
|
|
|
|
|
error={listError}
|
|
|
|
|
|
onRetry={() => void loadAssistants()}
|
|
|
|
|
|
empty={{
|
|
|
|
|
|
title: listItems.length === 0 ? "暂无助手" : "未找到匹配的助手",
|
|
|
|
|
|
description:
|
|
|
|
|
|
listItems.length === 0
|
|
|
|
|
|
? "点击右上角「创建助手」开始。"
|
|
|
|
|
|
: "请调整关键词或筛选条件后再试。",
|
|
|
|
|
|
}}
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
page: safeCurrentPage,
|
|
|
|
|
|
totalPages,
|
|
|
|
|
|
onPageChange: setCurrentPage,
|
|
|
|
|
|
summary:
|
|
|
|
|
|
filteredAssistants.length === 0
|
|
|
|
|
|
? "没有数据"
|
|
|
|
|
|
: `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredAssistants.length)} / 共 ${filteredAssistants.length} 个助手`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
columns={[
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "name",
|
|
|
|
|
|
header: "助手名称",
|
|
|
|
|
|
width: "md:w-[360px]",
|
|
|
|
|
|
cell: (assistant) => (
|
|
|
|
|
|
<>
|
2026-06-08 13:51:28 +08:00
|
|
|
|
<div className="truncate font-medium text-foreground">
|
|
|
|
|
|
{assistant.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-1 text-xs text-muted-soft">
|
|
|
|
|
|
{assistant.id}
|
|
|
|
|
|
</div>
|
2026-06-14 19:36:12 +08:00
|
|
|
|
</>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "type",
|
|
|
|
|
|
header: "助手类型",
|
|
|
|
|
|
width: "md:w-[128px]",
|
|
|
|
|
|
cell: (assistant) => (
|
|
|
|
|
|
<Badge
|
|
|
|
|
|
variant="secondary"
|
|
|
|
|
|
className="h-6 bg-surface-strong px-3 text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
{assistant.type}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "updatedAt",
|
|
|
|
|
|
width: "md:w-[176px]",
|
|
|
|
|
|
header: (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={toggleSortOrder}
|
|
|
|
|
|
className="caption-label -mx-2 inline-flex items-center gap-1 rounded-md px-2 py-1 text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
|
|
|
|
|
|
aria-label={
|
|
|
|
|
|
sortOrder === "newest"
|
|
|
|
|
|
? "当前按最近更新排序,点击切换为最早更新"
|
|
|
|
|
|
: "当前按最早更新排序,点击切换为最近更新"
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
更新时间
|
|
|
|
|
|
{sortOrder === "newest" ? (
|
|
|
|
|
|
<ChevronDown size={13} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronUp size={13} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
),
|
|
|
|
|
|
cellClassName:
|
|
|
|
|
|
"whitespace-nowrap tabular-nums text-muted-foreground",
|
|
|
|
|
|
cell: (assistant) => assistant.updatedAt,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "actions",
|
|
|
|
|
|
header: "操作",
|
|
|
|
|
|
align: "right",
|
|
|
|
|
|
cellClassName: "flex justify-end gap-2",
|
|
|
|
|
|
cell: (assistant) => (
|
|
|
|
|
|
<>
|
2026-06-08 13:51:28 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="gap-1.5 border-hairline-strong text-xs text-muted-foreground hover:text-foreground"
|
|
|
|
|
|
onClick={() => handleEdit(assistant)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Pencil size={14} />
|
|
|
|
|
|
编辑
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
|
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
|
|
|
|
|
aria-label={`${assistant.name} 更多操作`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MoreHorizontal size={15} />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
|
<DropdownMenuContent
|
|
|
|
|
|
align="end"
|
|
|
|
|
|
className="w-32 min-w-32 rounded-xl border border-hairline bg-popover p-1"
|
|
|
|
|
|
>
|
2026-06-09 08:31:19 +08:00
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
|
className="rounded-lg"
|
|
|
|
|
|
onSelect={() => handleDuplicate(assistant)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Copy size={14} />
|
|
|
|
|
|
复制
|
|
|
|
|
|
</DropdownMenuItem>
|
2026-06-08 13:51:28 +08:00
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
|
variant="destructive"
|
|
|
|
|
|
className="rounded-lg"
|
2026-06-09 09:48:43 +08:00
|
|
|
|
onSelect={(event) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
void handleDelete(assistant.id);
|
|
|
|
|
|
}}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Trash2 size={14} />
|
|
|
|
|
|
删除
|
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
|
</DropdownMenu>
|
2026-06-14 19:36:12 +08:00
|
|
|
|
</>
|
2026-06-08 13:51:28 +08:00
|
|
|
|
),
|
2026-06-14 19:36:12 +08:00
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
2026-06-08 13:51:28 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (view === "choose") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
|
2026-06-10 14:39:52 +08:00
|
|
|
|
先为助手取个名字,再选择构建方式。确认后将立即创建助手并进入编辑页。
|
2026-06-08 13:51:28 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="lg"
|
|
|
|
|
|
className="shrink-0 gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
|
2026-06-10 14:39:52 +08:00
|
|
|
|
onClick={() => router.push("/assistants")}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
>
|
|
|
|
|
|
<ChevronLeft size={16} />
|
|
|
|
|
|
返回列表
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="flex flex-col gap-4">
|
|
|
|
|
|
<div className="text-sm font-medium text-foreground">构建方式</div>
|
|
|
|
|
|
|
2026-07-17 20:03:01 +08:00
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
2026-06-08 13:51:28 +08:00
|
|
|
|
{assistantTypeOptions.map((option) => {
|
|
|
|
|
|
const selected = draftType === option.type;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={option.type}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setDraftType(option.type)}
|
|
|
|
|
|
className={`group relative flex flex-col gap-4 rounded-2xl border bg-card p-5 text-left transition-colors ${
|
|
|
|
|
|
selected
|
|
|
|
|
|
? "border-primary ring-1 ring-primary"
|
|
|
|
|
|
: "border-hairline hover:border-hairline-strong"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
|
|
|
|
|
{option.icon}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{selected ? (
|
|
|
|
|
|
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
|
|
|
|
|
<Check size={14} />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
!option.available && (
|
|
|
|
|
|
<Badge
|
|
|
|
|
|
variant="secondary"
|
|
|
|
|
|
className="h-6 bg-surface-strong px-3 text-xs text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
即将上线
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="text-base font-medium text-foreground">
|
|
|
|
|
|
{option.label}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="mt-1.5 text-sm leading-6 text-muted-foreground">
|
|
|
|
|
|
{option.description}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
2026-07-17 20:03:01 +08:00
|
|
|
|
|
|
|
|
|
|
{draftType === "智能体平台" && (
|
|
|
|
|
|
<div className="rounded-2xl border border-hairline bg-card p-5">
|
|
|
|
|
|
<div className="text-sm font-medium text-foreground">
|
|
|
|
|
|
选择智能体平台
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="mt-1.5 text-sm leading-6 text-muted-foreground">
|
|
|
|
|
|
平台类型用于匹配对应的应用资源和运行方式。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="mt-4 flex flex-wrap gap-3">
|
|
|
|
|
|
{(["dify", "fastgpt"] as const).map((platform) => {
|
|
|
|
|
|
const selected = draftAgentPlatform === platform;
|
|
|
|
|
|
const label = platform === "dify" ? "Dify" : "FastGPT";
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key={platform}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant={selected ? "default" : "outline"}
|
|
|
|
|
|
className={
|
|
|
|
|
|
selected
|
|
|
|
|
|
? "gap-2"
|
|
|
|
|
|
: "gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
|
|
|
|
|
|
}
|
|
|
|
|
|
onClick={() => setDraftAgentPlatform(platform)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{selected && <Check size={14} />}
|
|
|
|
|
|
{label}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-end gap-3">
|
2026-06-10 14:39:52 +08:00
|
|
|
|
{createError && (
|
|
|
|
|
|
<span className="text-xs text-destructive">{createError}</span>
|
|
|
|
|
|
)}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="lg"
|
|
|
|
|
|
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
2026-06-10 14:39:52 +08:00
|
|
|
|
disabled={creating}
|
|
|
|
|
|
onClick={() => router.push("/assistants")}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="lg"
|
|
|
|
|
|
className="gap-2"
|
2026-07-17 20:03:01 +08:00
|
|
|
|
disabled={
|
|
|
|
|
|
!draftName.trim() ||
|
|
|
|
|
|
!draftType ||
|
|
|
|
|
|
(draftType === "智能体平台" && !draftAgentPlatform) ||
|
|
|
|
|
|
creating
|
|
|
|
|
|
}
|
2026-06-10 14:39:52 +08:00
|
|
|
|
onClick={() => void confirmType()}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
>
|
2026-06-10 14:39:52 +08:00
|
|
|
|
{creating ? (
|
|
|
|
|
|
<Loader2 size={16} className="animate-spin" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Rocket size={16} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
创建助手
|
2026-06-08 13:51:28 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:12:41 +08:00
|
|
|
|
if (view === "workflow") {
|
2026-06-08 13:51:28 +08:00
|
|
|
|
return (
|
2026-07-14 12:59:41 +08:00
|
|
|
|
<WorkflowPage
|
|
|
|
|
|
assistantId={editingId}
|
|
|
|
|
|
workflowName={workflowName}
|
|
|
|
|
|
workflowGraph={workflowGraph}
|
|
|
|
|
|
workflowSettings={workflowSettings}
|
|
|
|
|
|
dynamicVariableDefinitions={
|
|
|
|
|
|
effectiveWorkflowDynamicVariableDefinitions
|
|
|
|
|
|
}
|
|
|
|
|
|
workflowDebugOpen={workflowDebugOpen}
|
|
|
|
|
|
workflowSettingsOpen={workflowSettingsOpen}
|
|
|
|
|
|
workflowEditingNodeId={workflowEditingNodeId}
|
|
|
|
|
|
workflowEditingEdgeId={workflowEditingEdgeId}
|
|
|
|
|
|
activeNodeId={activeNodeId}
|
|
|
|
|
|
dynamicVariablesOpen={dynamicVariablesOpen}
|
|
|
|
|
|
saving={saving}
|
|
|
|
|
|
dirty={dirty}
|
|
|
|
|
|
saveError={saveError}
|
|
|
|
|
|
modelOptions={{
|
|
|
|
|
|
llm: credOptions("LLM"),
|
|
|
|
|
|
asr: credOptions("ASR"),
|
|
|
|
|
|
tts: credOptions("TTS"),
|
|
|
|
|
|
}}
|
|
|
|
|
|
toolOptions={tools
|
|
|
|
|
|
.filter((tool) => tool.status === "active")
|
|
|
|
|
|
.map((tool) => ({ value: tool.id, label: tool.name }))}
|
|
|
|
|
|
knowledgeOptions={kbOptions}
|
|
|
|
|
|
onBack={() => router.push("/assistants")}
|
|
|
|
|
|
onSave={() => void handleSaveWorkflow()}
|
|
|
|
|
|
setWorkflowName={setWorkflowName}
|
|
|
|
|
|
setWorkflowGraph={setWorkflowGraph}
|
|
|
|
|
|
setWorkflowSettings={setWorkflowSettings}
|
|
|
|
|
|
setWorkflowDynamicVariableDefinitions={
|
|
|
|
|
|
setWorkflowDynamicVariableDefinitions
|
|
|
|
|
|
}
|
|
|
|
|
|
setWorkflowDebugOpen={setWorkflowDebugOpen}
|
|
|
|
|
|
setWorkflowSettingsOpen={setWorkflowSettingsOpen}
|
|
|
|
|
|
setWorkflowEditingNodeId={setWorkflowEditingNodeId}
|
|
|
|
|
|
setWorkflowEditingEdgeId={setWorkflowEditingEdgeId}
|
|
|
|
|
|
setActiveNodeId={setActiveNodeId}
|
|
|
|
|
|
setDynamicVariablesOpen={setDynamicVariablesOpen}
|
|
|
|
|
|
/>
|
2026-06-08 13:51:28 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-07-17 20:03:01 +08:00
|
|
|
|
if (view === "create-agent-platform") {
|
|
|
|
|
|
const platformLabel =
|
|
|
|
|
|
agentPlatformForm.platform === "dify" ? "Dify" : "FastGPT";
|
|
|
|
|
|
|
2026-06-08 13:51:28 +08:00
|
|
|
|
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">
|
2026-06-10 14:39:52 +08:00
|
|
|
|
<EditorBackButton onClick={() => router.push("/assistants")} />
|
2026-06-08 13:51:28 +08:00
|
|
|
|
<EditableTitle
|
2026-07-17 20:03:01 +08:00
|
|
|
|
value={agentPlatformForm.name}
|
|
|
|
|
|
onChange={(value) => updateAgentPlatformForm("name", value)}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
2026-06-09 16:48:40 +08:00
|
|
|
|
<AssistantIdentity assistantId={editingId} />
|
2026-06-08 13:51:28 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex shrink-0 gap-2">
|
2026-06-09 09:48:43 +08:00
|
|
|
|
{saveError && (
|
|
|
|
|
|
<span className="self-center text-xs text-destructive">
|
|
|
|
|
|
{saveError}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
className="gap-2"
|
2026-07-09 15:29:25 +08:00
|
|
|
|
disabled={
|
|
|
|
|
|
saving ||
|
|
|
|
|
|
!dirty ||
|
2026-07-17 20:03:01 +08:00
|
|
|
|
!agentPlatformForm.name.trim() ||
|
|
|
|
|
|
!agentPlatformForm.agent
|
2026-07-09 15:29:25 +08:00
|
|
|
|
}
|
2026-07-17 20:03:01 +08:00
|
|
|
|
onClick={() => void handleSaveAgentPlatform()}
|
2026-06-09 09:48:43 +08:00
|
|
|
|
>
|
|
|
|
|
|
{saving ? (
|
|
|
|
|
|
<Loader2 size={16} className="animate-spin" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Save size={16} />
|
|
|
|
|
|
)}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
保存
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-14 09:36:28 +08:00
|
|
|
|
<div className="flex min-h-0 flex-1 gap-4">
|
|
|
|
|
|
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
2026-06-08 13:51:28 +08:00
|
|
|
|
<SectionCard
|
2026-07-17 20:03:01 +08:00
|
|
|
|
icon={<Boxes size={15} />}
|
|
|
|
|
|
title="智能体平台配置"
|
|
|
|
|
|
description={`选择智能体平台及对应应用。开场白、知识库、提示词等对话编排请在 ${platformLabel} 平台配置,本页不重复设置。`}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
>
|
2026-07-17 20:03:01 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<div className="mb-2 text-sm font-medium text-foreground">
|
|
|
|
|
|
智能体平台
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{(["dify", "fastgpt"] as const).map((platform) => {
|
|
|
|
|
|
const selected = agentPlatformForm.platform === platform;
|
|
|
|
|
|
const label = platform === "dify" ? "Dify" : "FastGPT";
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key={platform}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant={selected ? "default" : "outline"}
|
|
|
|
|
|
className={
|
|
|
|
|
|
selected
|
|
|
|
|
|
? "gap-2"
|
|
|
|
|
|
: "gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
|
|
|
|
|
|
}
|
|
|
|
|
|
onClick={() => handleAgentPlatformChange(platform)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{selected && <Check size={14} />}
|
|
|
|
|
|
{label}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-07-09 15:29:25 +08:00
|
|
|
|
<ResourceSelectField
|
2026-07-17 20:03:01 +08:00
|
|
|
|
label={`${platformLabel} 应用`}
|
|
|
|
|
|
value={agentPlatformForm.agent}
|
|
|
|
|
|
onChange={(value) => updateAgentPlatformForm("agent", value)}
|
|
|
|
|
|
options={agentOptions(agentPlatformForm.platform)}
|
2026-07-09 15:29:25 +08:00
|
|
|
|
noneLabel="请选择"
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</SectionCard>
|
|
|
|
|
|
|
|
|
|
|
|
<SectionCard
|
2026-07-14 09:36:28 +08:00
|
|
|
|
icon={<Brain size={15} />}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
title="语音配置"
|
2026-07-17 20:03:01 +08:00
|
|
|
|
description={`从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 ${platformLabel} 应用提供,请前往 ${platformLabel} 平台配置。`}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
>
|
2026-06-09 09:48:43 +08:00
|
|
|
|
<ResourceSelectField
|
2026-06-08 13:51:28 +08:00
|
|
|
|
label="语音识别"
|
2026-07-17 20:03:01 +08:00
|
|
|
|
value={agentPlatformForm.asr}
|
|
|
|
|
|
onChange={(value) => updateAgentPlatformForm("asr", value)}
|
2026-06-09 09:48:43 +08:00
|
|
|
|
options={credOptions("ASR")}
|
|
|
|
|
|
noneLabel="无"
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
2026-06-09 09:48:43 +08:00
|
|
|
|
<ResourceSelectField
|
|
|
|
|
|
label="语音合成"
|
2026-07-17 20:03:01 +08:00
|
|
|
|
value={agentPlatformForm.voice}
|
|
|
|
|
|
onChange={(value) => updateAgentPlatformForm("voice", value)}
|
2026-06-09 09:48:43 +08:00
|
|
|
|
options={credOptions("TTS")}
|
|
|
|
|
|
noneLabel="无"
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</SectionCard>
|
|
|
|
|
|
|
|
|
|
|
|
<SectionCard
|
2026-07-14 09:36:28 +08:00
|
|
|
|
icon={<Sparkles size={15} />}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
title="交互策略"
|
|
|
|
|
|
description="设置实时视频对话时的交互体验"
|
|
|
|
|
|
>
|
2026-07-12 11:08:19 +08:00
|
|
|
|
<TurnConfigEditor
|
2026-07-17 20:03:01 +08:00
|
|
|
|
enabled={agentPlatformForm.enableInterrupt}
|
|
|
|
|
|
config={agentPlatformForm.turnConfig}
|
|
|
|
|
|
onEnabledChange={(checked) =>
|
|
|
|
|
|
updateAgentPlatformForm("enableInterrupt", checked)
|
|
|
|
|
|
}
|
|
|
|
|
|
onConfigChange={(config) =>
|
|
|
|
|
|
updateAgentPlatformForm("turnConfig", config)
|
|
|
|
|
|
}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</SectionCard>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-17 20:03:01 +08:00
|
|
|
|
<DebugDrawer assistantId={editingId} hasUnsavedChanges={dirty} />
|
2026-06-08 13:51:28 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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">
|
2026-06-10 14:39:52 +08:00
|
|
|
|
<EditorBackButton onClick={() => router.push("/assistants")} />
|
2026-06-08 13:51:28 +08:00
|
|
|
|
<EditableTitle
|
|
|
|
|
|
value={openCodeForm.name}
|
|
|
|
|
|
onChange={(value) => updateOpenCodeForm("name", value)}
|
|
|
|
|
|
/>
|
2026-06-09 16:48:40 +08:00
|
|
|
|
<AssistantIdentity assistantId={editingId} />
|
2026-06-08 13:51:28 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex shrink-0 gap-2">
|
2026-06-09 12:59:07 +08:00
|
|
|
|
{saveError && (
|
|
|
|
|
|
<span className="self-center text-xs text-destructive">
|
|
|
|
|
|
{saveError}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
className="gap-2"
|
2026-07-09 15:29:25 +08:00
|
|
|
|
disabled={
|
|
|
|
|
|
saving ||
|
|
|
|
|
|
!dirty ||
|
|
|
|
|
|
!openCodeForm.name.trim() ||
|
|
|
|
|
|
!openCodeForm.agent
|
|
|
|
|
|
}
|
2026-06-09 12:59:07 +08:00
|
|
|
|
onClick={() => void handleSaveOpenCode()}
|
|
|
|
|
|
>
|
|
|
|
|
|
{saving ? (
|
|
|
|
|
|
<Loader2 size={16} className="animate-spin" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Save size={16} />
|
|
|
|
|
|
)}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
保存
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-14 09:36:28 +08:00
|
|
|
|
<div className="flex min-h-0 flex-1 gap-4">
|
|
|
|
|
|
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
2026-06-08 13:51:28 +08:00
|
|
|
|
<SectionCard
|
2026-07-14 09:36:28 +08:00
|
|
|
|
icon={<Terminal size={15} />}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
title="OpenCode 服务配置"
|
2026-07-09 15:29:25 +08:00
|
|
|
|
description="从「模型资源」中选择 OpenCode 服务资源。"
|
2026-06-08 13:51:28 +08:00
|
|
|
|
>
|
2026-07-09 15:29:25 +08:00
|
|
|
|
<ResourceSelectField
|
|
|
|
|
|
label="OpenCode 服务"
|
|
|
|
|
|
value={openCodeForm.agent}
|
|
|
|
|
|
onChange={(value) => updateOpenCodeForm("agent", value)}
|
|
|
|
|
|
options={agentOptions("opencode")}
|
|
|
|
|
|
noneLabel="请选择"
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</SectionCard>
|
|
|
|
|
|
|
|
|
|
|
|
<SectionCard
|
2026-07-14 09:36:28 +08:00
|
|
|
|
icon={<MessageSquareText size={15} />}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
title="提示词"
|
|
|
|
|
|
description="描述助手的角色、能力和回答要求"
|
|
|
|
|
|
>
|
|
|
|
|
|
<TextAreaField
|
|
|
|
|
|
value={openCodeForm.prompt}
|
|
|
|
|
|
onChange={(value) => updateOpenCodeForm("prompt", value)}
|
|
|
|
|
|
placeholder="请输入提示词,描述助手的角色、能力和回答要求"
|
|
|
|
|
|
rows={8}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</SectionCard>
|
|
|
|
|
|
|
|
|
|
|
|
<SectionCard
|
2026-07-14 09:36:28 +08:00
|
|
|
|
icon={<Brain size={15} />}
|
2026-06-09 12:59:07 +08:00
|
|
|
|
title="模型与语音配置"
|
|
|
|
|
|
description="配置 OpenCode 使用的大语言模型、语音识别与语音合成资源。"
|
2026-06-08 13:51:28 +08:00
|
|
|
|
>
|
2026-07-07 19:41:45 +08:00
|
|
|
|
<ToggleRow
|
|
|
|
|
|
title="视觉理解"
|
2026-07-07 21:50:15 +08:00
|
|
|
|
hint="开启后,开始对话时会允许助手按需理解当前视频画面。视觉模型选「模型自己」时,大语言模型本身必须支持图片输入。"
|
|
|
|
|
|
checked={openCodeForm.visionEnabled}
|
|
|
|
|
|
onChange={handleOpenCodeVisionEnabledChange}
|
2026-07-07 19:41:45 +08:00
|
|
|
|
/>
|
2026-07-07 21:50:15 +08:00
|
|
|
|
{openCodeForm.visionEnabled && (
|
|
|
|
|
|
<ResourceSelectField
|
|
|
|
|
|
label="视觉模型"
|
|
|
|
|
|
value={openCodeForm.visionModelResourceId}
|
|
|
|
|
|
onChange={(value) =>
|
|
|
|
|
|
updateOpenCodeForm("visionModelResourceId", value)
|
|
|
|
|
|
}
|
|
|
|
|
|
options={visionModelOptionsFor(openCodeForm.model)}
|
|
|
|
|
|
noneLabel="模型自己"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-06-09 12:59:07 +08:00
|
|
|
|
<ResourceSelectField
|
|
|
|
|
|
label="大语言模型"
|
|
|
|
|
|
value={openCodeForm.model}
|
2026-07-07 21:50:15 +08:00
|
|
|
|
onChange={handleOpenCodeModelChange}
|
2026-06-09 12:59:07 +08:00
|
|
|
|
options={credOptions("LLM")}
|
|
|
|
|
|
noneLabel="无"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ResourceSelectField
|
2026-06-08 13:51:28 +08:00
|
|
|
|
label="语音识别"
|
|
|
|
|
|
value={openCodeForm.asr}
|
|
|
|
|
|
onChange={(value) => updateOpenCodeForm("asr", value)}
|
2026-06-09 12:59:07 +08:00
|
|
|
|
options={credOptions("ASR")}
|
|
|
|
|
|
noneLabel="无"
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
2026-06-09 12:59:07 +08:00
|
|
|
|
<ResourceSelectField
|
|
|
|
|
|
label="语音合成"
|
2026-06-08 13:51:28 +08:00
|
|
|
|
value={openCodeForm.voice}
|
|
|
|
|
|
onChange={(value) => updateOpenCodeForm("voice", value)}
|
2026-06-09 12:59:07 +08:00
|
|
|
|
options={credOptions("TTS")}
|
|
|
|
|
|
noneLabel="无"
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</SectionCard>
|
|
|
|
|
|
|
|
|
|
|
|
<SectionCard
|
2026-07-14 09:36:28 +08:00
|
|
|
|
icon={<Sparkles size={15} />}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
title="交互策略"
|
|
|
|
|
|
description="设置实时视频对话时的交互体验"
|
|
|
|
|
|
>
|
2026-07-12 11:08:19 +08:00
|
|
|
|
<TurnConfigEditor
|
|
|
|
|
|
enabled={openCodeForm.enableInterrupt}
|
|
|
|
|
|
config={openCodeForm.turnConfig}
|
|
|
|
|
|
onEnabledChange={(checked) => updateOpenCodeForm("enableInterrupt", checked)}
|
|
|
|
|
|
onConfigChange={(config) => updateOpenCodeForm("turnConfig", config)}
|
2026-06-08 13:51:28 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</SectionCard>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-07 20:16:30 +08:00
|
|
|
|
<DebugDrawer assistantId={editingId} hasUnsavedChanges={dirty} />
|
2026-06-08 13:51:28 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-14 12:59:41 +08:00
|
|
|
|
<PromptEditor
|
|
|
|
|
|
assistantId={editingId}
|
|
|
|
|
|
form={form}
|
|
|
|
|
|
dynamicVariablesOpen={dynamicVariablesOpen}
|
|
|
|
|
|
dynamicVariableDefinitions={effectiveDynamicVariableDefinitions}
|
|
|
|
|
|
saving={saving}
|
|
|
|
|
|
dirty={dirty}
|
|
|
|
|
|
saveError={saveError}
|
|
|
|
|
|
llmOptions={credOptions("LLM")}
|
|
|
|
|
|
asrOptions={credOptions("ASR")}
|
|
|
|
|
|
ttsOptions={credOptions("TTS")}
|
|
|
|
|
|
realtimeOptions={credOptions("Realtime")}
|
|
|
|
|
|
visionModelOptions={visionModelOptionsFor(form.model)}
|
|
|
|
|
|
knowledgeOptions={kbOptions}
|
|
|
|
|
|
tools={tools}
|
|
|
|
|
|
onBack={() => router.push("/assistants")}
|
|
|
|
|
|
onSave={() => void handleSavePrompt()}
|
|
|
|
|
|
setDynamicVariablesOpen={setDynamicVariablesOpen}
|
|
|
|
|
|
updateForm={updateForm}
|
|
|
|
|
|
handlePromptVisionEnabledChange={handlePromptVisionEnabledChange}
|
|
|
|
|
|
handlePromptModelChange={handlePromptModelChange}
|
2026-07-07 10:07:36 +08:00
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|