feat(workflow): enhance message stages and image routing

This commit is contained in:
Xin Wang
2026-08-03 12:38:02 +08:00
parent b0991f239e
commit 4c43e167db
13 changed files with 593 additions and 157 deletions
@@ -1,13 +1,45 @@
"use client";
import { MessageSquareText } from "lucide-react";
import { ArrowRight, MessageSquareText } from "lucide-react";
import { SectionCard } from "@/components/editor/section-card";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import type { WorkflowNodeData } from "../specs";
import {
messageCompletionPolicy,
type MessageCompletionPolicy,
type WorkflowNodeData,
} from "../specs";
const COMPLETION_OPTIONS: Array<{
value: MessageCompletionPolicy;
label: string;
description: string;
}> = [
{
value: "interruptible",
label: "可打断",
description: "用户说话、发文字或图片时立即进入下一节点,并保留这次输入。",
},
{
value: "playback",
label: "播放完继续",
description: "播报期间关闭输入,实际播放完成后自动进入下一节点。",
},
{
value: "confirmation",
label: "确认后继续",
description: "关闭对话输入并显示弹窗,只有用户在客户端确认后才能继续。",
},
];
export function MessageNodePanel({
draft,
@@ -18,111 +50,126 @@ export function MessageNodePanel({
set: (key: string, value: unknown) => void;
setPatch: (patch: Partial<WorkflowNodeData>) => void;
}) {
const showMessage = Boolean(draft.showMessage);
const requireConfirmation = Boolean(draft.requireConfirmation);
const completionPolicy = messageCompletionPolicy(draft);
const completionDescription = COMPLETION_OPTIONS.find(
(option) => option.value === completionPolicy,
)?.description;
const selectCompletionPolicy = (value: MessageCompletionPolicy) => {
setPatch({
completionPolicy: value,
requireConfirmation: undefined,
showMessage: undefined,
});
};
return (
<SectionCard
icon={<MessageSquareText size={15} />}
title="播报与确认"
description="播放固定话术,并可同时显示平台内置的客户端消息"
>
<label className="block">
<div className="mb-1.5 text-sm font-medium text-foreground">
固定播报(可选)
</div>
<Textarea
rows={4}
value={draft.speech ?? ""}
onChange={(event) => set("speech", event.target.value)}
placeholder="例如:您好,在开始服务前请确认以下重要信息。"
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
<span className="mt-1.5 block text-xs leading-5 text-muted-foreground">
支持 {"{{variable}}"} 动态变量。仅播报时,播放完成后进入下一节点。
</span>
</label>
<label className="flex items-start justify-between gap-4 rounded-xl border border-hairline bg-canvas-soft px-4 py-3">
<span>
<span className="block text-sm font-medium text-foreground">
显示客户端消息
<>
<SectionCard
icon={<MessageSquareText size={15} />}
title="固定播报"
description="配置进入 Message 节点后播放的固定话术"
>
<label className="block">
<div className="mb-1.5 text-sm font-medium text-foreground">
播报内容 <span className="text-destructive">*</span>
</div>
<Textarea
rows={4}
value={draft.speech ?? ""}
onChange={(event) => set("speech", event.target.value)}
placeholder="例如:您好,在开始服务前请确认以下重要信息。"
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
<span className="mt-1.5 block text-xs leading-5 text-muted-foreground">
支持 {"{{variable}}"} 动态变量。
</span>
<span className="mt-1 block text-xs leading-5 text-muted-foreground">
使用平台内置弹窗,不需要创建或绑定 Client Tool。
</span>
</span>
<Switch
checked={showMessage}
onCheckedChange={(checked) =>
setPatch({
showMessage: checked,
...(!checked ? { requireConfirmation: false } : {}),
})
}
/>
</label>
</label>
{showMessage && (
<div className="space-y-3 rounded-xl border border-hairline bg-canvas-soft p-4">
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-foreground">
弹窗标题
</span>
<Input
value={draft.title ?? ""}
onChange={(event) => set("title", event.target.value)}
placeholder="重要提示"
className="border-hairline-strong bg-background"
/>
</label>
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-foreground">
重要信息 <span className="text-destructive">*</span>
</span>
<Textarea
rows={4}
value={draft.message ?? ""}
onChange={(event) => set("message", event.target.value)}
placeholder="请输入需要向用户展示的重要信息"
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
</label>
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-foreground">
按钮文字
</span>
<Input
value={draft.confirmLabel ?? ""}
onChange={(event) => set("confirmLabel", event.target.value)}
placeholder="确认"
className="border-hairline-strong bg-background"
/>
</label>
<label className="flex items-start justify-between gap-4 border-t border-hairline pt-3">
<span>
<span className="block text-sm font-medium text-foreground">
等待用户确认
</span>
<span className="mt-1 block text-xs leading-5 text-muted-foreground">
确认前禁止语音、文字和图片输入;确认后立即进入下一节点,不等待播报结束。
</span>
</span>
<Switch
checked={requireConfirmation}
onCheckedChange={(checked) =>
set("requireConfirmation", checked)
}
/>
</label>
{!draft.speech?.trim() && (
<p role="alert" className="text-xs leading-5 text-destructive">
请输入播报内容。
</p>
)}
</SectionCard>
<SectionCard
icon={<ArrowRight size={15} />}
title="继续方式"
description="控制 Message 节点何时进入下一节点"
>
<div>
<div className="mb-2 text-sm font-medium text-foreground">继续行为</div>
<Select
value={completionPolicy}
onValueChange={(value: MessageCompletionPolicy) =>
selectCompletionPolicy(value)
}
>
<SelectTrigger className="w-full border-hairline-strong bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
{COMPLETION_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="mt-1.5 text-xs leading-5 text-muted-foreground">
{completionDescription}
</p>
</div>
)}
{!draft.speech?.trim() && !showMessage && (
<p role="alert" className="text-xs leading-5 text-destructive">
Message 至少需要固定播报或客户端消息。
</p>
)}
</SectionCard>
{completionPolicy === "confirmation" && (
<div className="space-y-3 rounded-xl border border-hairline bg-canvas-soft p-4">
<div>
<div className="text-sm font-medium text-foreground">客户端确认消息</div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
使用平台内置弹窗,不需要创建或绑定 Client Tool。
</p>
</div>
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-foreground">
弹窗标题
</span>
<Input
value={draft.title ?? ""}
onChange={(event) => set("title", event.target.value)}
placeholder="重要提示"
className="border-hairline-strong bg-background"
/>
</label>
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-foreground">
重要信息 <span className="text-destructive">*</span>
</span>
<Textarea
rows={4}
value={draft.message ?? ""}
onChange={(event) => set("message", event.target.value)}
placeholder="请输入需要向用户展示的重要信息"
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
</label>
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-foreground">
按钮文字
</span>
<Input
value={draft.confirmLabel ?? ""}
onChange={(event) => set("confirmLabel", event.target.value)}
placeholder="确认"
className="border-hairline-strong bg-background"
/>
</label>
<p className="border-t border-hairline pt-3 text-xs leading-5 text-muted-foreground">
确认前禁止语音、文字和图片输入;确认后立即进入下一节点,不等待播报结束。
</p>
</div>
)}
</SectionCard>
</>
);
}