Add workflow editor and node types support in frontend and backend
- Introduce a new workflow editor component for visualizing and managing workflows, allowing users to add nodes and define connections. - Implement backend support for node types, including validation and constraints for workflow graphs. - Add new API endpoints for retrieving node types and their specifications. - Enhance the AssistantPage to integrate the workflow editor, enabling users to create and edit workflows directly. - Update frontend components to support new workflow functionalities, including condition edges and generic nodes. - Refactor existing code to accommodate the new workflow features and improve overall structure.
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
PhoneOff,
|
||||
Orbit,
|
||||
Waves,
|
||||
Bug,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -38,6 +39,12 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -89,6 +96,14 @@ import {
|
||||
type ChatMessage,
|
||||
type VoicePreviewStatus,
|
||||
} from "@/hooks/use-voice-preview";
|
||||
import {
|
||||
WorkflowEditor,
|
||||
type WorkflowSettings,
|
||||
} from "@/components/workflow/WorkflowEditor";
|
||||
import {
|
||||
defaultGraph,
|
||||
type WorkflowGraph,
|
||||
} from "@/components/workflow/specs";
|
||||
|
||||
type RuntimeMode = "pipeline" | "realtime";
|
||||
|
||||
@@ -167,7 +182,7 @@ const typeToView = {
|
||||
dify: "create-dify",
|
||||
fastgpt: "create-fastgpt",
|
||||
opencode: "create-opencode",
|
||||
workflow: "placeholder",
|
||||
workflow: "workflow",
|
||||
} as const;
|
||||
|
||||
type View = "list" | "choose" | "loading" | (typeof typeToView)[ApiAssistantType];
|
||||
@@ -348,6 +363,16 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
// 已保存基线(当前类型表单的 JSON);与表单不一致时保存按钮才可点击
|
||||
const [savedSnapshot, setSavedSnapshot] = useState<string | null>(null);
|
||||
|
||||
// workflow 编辑器:名称 + 图(nodes/edges)。graph 实时由画布回传。
|
||||
const [workflowName, setWorkflowName] = useState("");
|
||||
const [workflowGraph, setWorkflowGraph] = useState<WorkflowGraph>(() =>
|
||||
defaultGraph(),
|
||||
);
|
||||
const [workflowSettings, setWorkflowSettings] = useState<WorkflowSettings>({
|
||||
allowInterrupt: true,
|
||||
});
|
||||
const [debugOpen, setDebugOpen] = useState(false);
|
||||
|
||||
const loadAssistants = useCallback(async () => {
|
||||
setListLoading(true);
|
||||
setListError(null);
|
||||
@@ -507,6 +532,14 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
const next = { ...openCodeForm, apiKey: "" };
|
||||
setOpenCodeForm(next);
|
||||
setSavedSnapshot(JSON.stringify(next));
|
||||
} else if (view === "workflow") {
|
||||
setSavedSnapshot(
|
||||
JSON.stringify({
|
||||
name: workflowName,
|
||||
graph: workflowGraph,
|
||||
settings: workflowSettings,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setSaveError(error instanceof Error ? error.message : "保存失败");
|
||||
@@ -635,9 +668,29 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
} else if (assistant.type === "opencode") {
|
||||
setSavedSnapshot(JSON.stringify(fillOpenCodeForm(assistant)));
|
||||
} else {
|
||||
// 工作流:暂时显示占位页
|
||||
setDraftName(assistant.name);
|
||||
setDraftType(typeToLabel[assistant.type]);
|
||||
// 工作流:回填名称与图(空图回落到默认 开始→智能体→结束)
|
||||
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 = {
|
||||
llm: assistant.modelResourceIds.LLM,
|
||||
asr: assistant.modelResourceIds.ASR,
|
||||
tts: assistant.modelResourceIds.TTS,
|
||||
allowInterrupt: assistant.enableInterrupt,
|
||||
};
|
||||
setWorkflowName(assistant.name);
|
||||
setWorkflowGraph(graph);
|
||||
setWorkflowSettings(wfSettings);
|
||||
setSavedSnapshot(
|
||||
JSON.stringify({
|
||||
name: assistant.name,
|
||||
graph,
|
||||
settings: wfSettings,
|
||||
}),
|
||||
);
|
||||
}
|
||||
setView(typeToView[assistant.type]);
|
||||
} catch (error) {
|
||||
@@ -672,6 +725,22 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function handleSaveWorkflow() {
|
||||
void save(
|
||||
baseUpsert({
|
||||
name: workflowName.trim(),
|
||||
type: "workflow",
|
||||
enableInterrupt: workflowSettings.allowInterrupt,
|
||||
modelResourceIds: {
|
||||
...(workflowSettings.llm ? { LLM: workflowSettings.llm } : {}),
|
||||
...(workflowSettings.asr ? { ASR: workflowSettings.asr } : {}),
|
||||
...(workflowSettings.tts ? { TTS: workflowSettings.tts } : {}),
|
||||
},
|
||||
graph: workflowGraph as unknown as Record<string, unknown>,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 当前编辑器表单相对已保存基线是否有改动(决定保存按钮是否可点)
|
||||
const activeFormJson =
|
||||
view === "create"
|
||||
@@ -682,7 +751,13 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
? JSON.stringify(fastGptForm)
|
||||
: view === "create-opencode"
|
||||
? JSON.stringify(openCodeForm)
|
||||
: null;
|
||||
: view === "workflow"
|
||||
? JSON.stringify({
|
||||
name: workflowName,
|
||||
graph: workflowGraph,
|
||||
settings: workflowSettings,
|
||||
})
|
||||
: null;
|
||||
const dirty =
|
||||
activeFormJson !== null &&
|
||||
savedSnapshot !== null &&
|
||||
@@ -1114,59 +1189,73 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (view === "placeholder") {
|
||||
if (view === "workflow") {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-6">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div>
|
||||
<h1 className="font-display display-lg text-ink">
|
||||
{draftName.trim() || "创建助手"}
|
||||
</h1>
|
||||
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
|
||||
{draftType} 构建方式正在开发中,敬请期待。
|
||||
</p>
|
||||
<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">
|
||||
<EditorBackButton onClick={() => router.push("/assistants")} />
|
||||
<EditableTitle value={workflowName} onChange={setWorkflowName} />
|
||||
<AssistantIdentity assistantId={editingId} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="shrink-0 gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
onClick={() => router.push("/assistants")}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
返回列表
|
||||
</Button>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
{saveError && (
|
||||
<span className="self-center text-xs text-destructive">
|
||||
{saveError}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
|
||||
disabled={!editingId}
|
||||
onClick={() => setDebugOpen(true)}
|
||||
>
|
||||
<Bug size={16} />
|
||||
调试
|
||||
</Button>
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={saving || !dirty || !workflowName.trim()}
|
||||
onClick={() => handleSaveWorkflow()}
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
<Save size={16} />
|
||||
)}
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="relative overflow-hidden rounded-3xl border border-hairline bg-canvas-soft px-10 py-20 text-center">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -right-24 top-0 h-72 w-72 rounded-full opacity-50 blur-3xl"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(circle, color-mix(in srgb, var(--gradient-sky) 50%, transparent), transparent 70%)",
|
||||
<div className="min-h-0 flex-1">
|
||||
<WorkflowEditor
|
||||
value={workflowGraph}
|
||||
onChange={setWorkflowGraph}
|
||||
settings={workflowSettings}
|
||||
onSettingsChange={setWorkflowSettings}
|
||||
modelOptions={{
|
||||
llm: credOptions("LLM"),
|
||||
asr: credOptions("ASR"),
|
||||
tts: credOptions("TTS"),
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -left-20 bottom-0 h-64 w-64 rounded-full opacity-45 blur-3xl"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(circle, color-mix(in srgb, var(--gradient-lavender) 50%, transparent), transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="caption-label inline-flex rounded-full bg-surface-strong px-3 py-1 text-muted-foreground">
|
||||
建设中
|
||||
</div>
|
||||
<p className="font-display display-sm mx-auto mt-5 max-w-md text-ink">
|
||||
{draftType} 构建页面正在编写中
|
||||
</p>
|
||||
<p className="mx-auto mt-3 max-w-md text-sm leading-7 text-body">
|
||||
页面骨架与设计语言已就绪,后续将填充具体构建流程与数据。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Sheet open={debugOpen} onOpenChange={setDebugOpen} modal={false}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
showOverlay={false}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
className="w-[440px] gap-0 border-l border-hairline bg-card p-0 sm:max-w-[440px]"
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>语音调试</SheetTitle>
|
||||
</SheetHeader>
|
||||
<DebugDrawer assistantId={editingId} asSheet />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1767,12 +1856,23 @@ function SegmentedIconButton({
|
||||
);
|
||||
}
|
||||
|
||||
function DebugDrawer({ assistantId }: { assistantId: string | null }) {
|
||||
function DebugDrawer({
|
||||
assistantId,
|
||||
asSheet = false,
|
||||
}: {
|
||||
assistantId: string | null;
|
||||
asSheet?: boolean;
|
||||
}) {
|
||||
const [showTranscript, setShowTranscript] = useState(false);
|
||||
const [vizStyle, setVizStyle] = useState<VizStyle>("aura");
|
||||
|
||||
// 内联(prompt 编辑器右栏)用 aside + 圆角边框;抽屉模式占满 Sheet。
|
||||
const containerClass = asSheet
|
||||
? "flex h-full min-w-0 flex-1 flex-col overflow-hidden"
|
||||
: "hidden min-w-0 flex-1 flex-col overflow-hidden rounded-2xl border border-hairline bg-card shadow-sm lg:flex";
|
||||
|
||||
return (
|
||||
<aside className="hidden min-w-0 flex-1 flex-col overflow-hidden rounded-2xl border border-hairline bg-card shadow-sm lg:flex">
|
||||
<aside className={containerClass}>
|
||||
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-hairline px-5 py-3">
|
||||
<div className="text-sm font-medium text-foreground">调试与预览</div>
|
||||
{SHOW_VOICE_VIZ && (
|
||||
|
||||
Reference in New Issue
Block a user