feat: add deterministic message interaction stages
This commit is contained in:
@@ -27,7 +27,12 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
|
||||
|
||||
const nodeData = data as WorkflowNodeData;
|
||||
const Icon = spec.icon;
|
||||
const preview = (nodeData.greeting || nodeData.prompt || nodeData.message || "")
|
||||
const preview = (
|
||||
nodeData.prompt ||
|
||||
nodeData.speech ||
|
||||
nodeData.message ||
|
||||
""
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
const entryModeLabel = {
|
||||
@@ -50,8 +55,17 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
|
||||
nodeData.visionEnabled ? "视觉理解" : null,
|
||||
].filter(Boolean)
|
||||
: type === "action" && nodeData.toolId
|
||||
? ["确定性工具"]
|
||||
: [];
|
||||
? [
|
||||
"确定性工具",
|
||||
nodeData.userInputPolicy === "block" ? "禁止输入" : null,
|
||||
].filter(Boolean)
|
||||
: type === "message"
|
||||
? [
|
||||
nodeData.speech ? "固定播报" : null,
|
||||
nodeData.showMessage ? "客户端消息" : null,
|
||||
nodeData.requireConfirmation ? "等待确认" : null,
|
||||
].filter(Boolean)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -192,6 +206,7 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
|
||||
export const nodeTypes = {
|
||||
start: GenericNode,
|
||||
agent: GenericNode,
|
||||
message: GenericNode,
|
||||
action: GenericNode,
|
||||
handoff: GenericNode,
|
||||
end: GenericNode,
|
||||
|
||||
@@ -63,21 +63,30 @@ let nodeSeq = 0;
|
||||
function defaultNodeData(spec: RuntimeNodeSpec): WorkflowNodeData {
|
||||
const data: WorkflowNodeData = {
|
||||
name: spec.displayName,
|
||||
...(spec.type === "agent"
|
||||
? {
|
||||
contextPolicy: "inherit",
|
||||
inheritGlobalConfig: true,
|
||||
entryMode: "wait_user",
|
||||
entrySpeech: "",
|
||||
}
|
||||
: spec.type === "action"
|
||||
? {
|
||||
arguments: {},
|
||||
resultAssignmentMode: "inherit",
|
||||
userInputPolicy: "queue",
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
if (spec.type === "agent") {
|
||||
Object.assign(data, {
|
||||
contextPolicy: "inherit",
|
||||
inheritGlobalConfig: true,
|
||||
entryMode: "wait_user",
|
||||
entrySpeech: "",
|
||||
});
|
||||
} else if (spec.type === "action") {
|
||||
Object.assign(data, {
|
||||
arguments: {},
|
||||
resultAssignmentMode: "inherit",
|
||||
userInputPolicy: "queue",
|
||||
});
|
||||
} else if (spec.type === "message") {
|
||||
Object.assign(data, {
|
||||
speech: "",
|
||||
showMessage: false,
|
||||
title: "重要提示",
|
||||
message: "",
|
||||
confirmLabel: "确认",
|
||||
requireConfirmation: false,
|
||||
});
|
||||
}
|
||||
for (const field of spec.fields) {
|
||||
if (field.default !== undefined) data[field.key] = field.default;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import { MessageSquareText } from "lucide-react";
|
||||
|
||||
import { SectionCard } from "@/components/editor/section-card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
import type { WorkflowNodeData } from "../specs";
|
||||
|
||||
export function MessageNodePanel({
|
||||
draft,
|
||||
set,
|
||||
setPatch,
|
||||
}: {
|
||||
draft: WorkflowNodeData;
|
||||
set: (key: string, value: unknown) => void;
|
||||
setPatch: (patch: Partial<WorkflowNodeData>) => void;
|
||||
}) {
|
||||
const showMessage = Boolean(draft.showMessage);
|
||||
const requireConfirmation = Boolean(draft.requireConfirmation);
|
||||
|
||||
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">
|
||||
显示客户端消息
|
||||
</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>
|
||||
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!draft.speech?.trim() && !showMessage && (
|
||||
<p role="alert" className="text-xs leading-5 text-destructive">
|
||||
Message 至少需要固定播报或客户端消息。
|
||||
</p>
|
||||
)}
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Flag, PhoneForwarded, Play, Tag } from "lucide-react";
|
||||
import { Flag, PhoneForwarded, Tag } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { SectionCard } from "@/components/editor/section-card";
|
||||
@@ -11,6 +11,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
import { ActionNodePanel } from "./ActionNodePanel";
|
||||
import { AgentNodePanel } from "./AgentNodePanel";
|
||||
import { MessageNodePanel } from "./MessageNodePanel";
|
||||
import { NodeSelect, ToolOptionPicker } from "./controls";
|
||||
import type { RuntimeNodeSpec, WorkflowNodeData } from "../specs";
|
||||
import type { ModelOption, WorkflowSettings } from "../types";
|
||||
@@ -82,6 +83,7 @@ export function NodeSettingsPanel({
|
||||
spec={spec}
|
||||
draft={draft}
|
||||
set={set}
|
||||
setPatch={setPatch}
|
||||
toolOptions={toolOptions}
|
||||
argumentsJson={argumentsJson}
|
||||
assignmentsJson={assignmentsJson}
|
||||
@@ -247,6 +249,10 @@ export function NodeSettingsPanel({
|
||||
/>
|
||||
)}
|
||||
|
||||
{spec.type === "message" && (
|
||||
<MessageNodePanel draft={draft} set={set} setPatch={setPatch} />
|
||||
)}
|
||||
|
||||
{spec.type === "handoff" && (
|
||||
<NodeSelect
|
||||
label="转交类型"
|
||||
@@ -283,6 +289,7 @@ function WorkflowNodePanelForm({
|
||||
spec,
|
||||
draft,
|
||||
set,
|
||||
setPatch,
|
||||
toolOptions,
|
||||
argumentsJson,
|
||||
assignmentsJson,
|
||||
@@ -294,6 +301,7 @@ function WorkflowNodePanelForm({
|
||||
spec: RuntimeNodeSpec;
|
||||
draft: WorkflowNodeData;
|
||||
set: (key: string, val: unknown) => void;
|
||||
setPatch: (patch: Partial<WorkflowNodeData>) => void;
|
||||
toolOptions: ModelOption[];
|
||||
argumentsJson: string;
|
||||
assignmentsJson: string;
|
||||
@@ -321,27 +329,6 @@ function WorkflowNodePanelForm({
|
||||
</label>
|
||||
</SectionCard>
|
||||
|
||||
{spec.type === "start" && (
|
||||
<SectionCard
|
||||
icon={<Play size={15} />}
|
||||
title="开场白"
|
||||
description="会话建立后首先向用户播放的固定内容"
|
||||
>
|
||||
<label className="block">
|
||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
||||
固定开场白
|
||||
</div>
|
||||
<Textarea
|
||||
rows={5}
|
||||
value={draft.greeting ?? ""}
|
||||
onChange={(event) => set("greeting", event.target.value)}
|
||||
placeholder="例如:你好,我是 AI 视频助手,有什么可以帮你?"
|
||||
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
</label>
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{spec.type === "action" && (
|
||||
<ActionNodePanel
|
||||
draft={draft}
|
||||
@@ -356,6 +343,10 @@ function WorkflowNodePanelForm({
|
||||
/>
|
||||
)}
|
||||
|
||||
{spec.type === "message" && (
|
||||
<MessageNodePanel draft={draft} set={set} setPatch={setPatch} />
|
||||
)}
|
||||
|
||||
{spec.type === "handoff" && (
|
||||
<SectionCard
|
||||
icon={<PhoneForwarded size={15} />}
|
||||
|
||||
@@ -6,7 +6,13 @@ import { Circle, type LucideIcon } from "lucide-react";
|
||||
import type { NodeSpecDto, TurnConfig } from "@/lib/api";
|
||||
import { defaultTurnConfig } from "@/lib/turn-config";
|
||||
|
||||
export type WorkflowNodeType = "start" | "agent" | "action" | "handoff" | "end";
|
||||
export type WorkflowNodeType =
|
||||
| "start"
|
||||
| "agent"
|
||||
| "message"
|
||||
| "action"
|
||||
| "handoff"
|
||||
| "end";
|
||||
export type ContextPolicy = "inherit" | "fresh";
|
||||
export type KnowledgeMode = "automatic" | "on_demand" | "disabled";
|
||||
export type AgentEntryMode = "wait_user" | "generate" | "fixed_speech";
|
||||
@@ -26,7 +32,6 @@ export type ExpressionOperator =
|
||||
|
||||
export type WorkflowNodeData = {
|
||||
name: string;
|
||||
greeting?: string;
|
||||
prompt?: string;
|
||||
contextPolicy?: ContextPolicy;
|
||||
inheritGlobalConfig?: boolean;
|
||||
@@ -49,6 +54,11 @@ export type WorkflowNodeData = {
|
||||
resultAssignmentMode?: ActionResultAssignmentMode;
|
||||
resultAssignments?: Record<string, string>;
|
||||
userInputPolicy?: ActionUserInputPolicy;
|
||||
speech?: string;
|
||||
showMessage?: boolean;
|
||||
title?: string;
|
||||
confirmLabel?: string;
|
||||
requireConfirmation?: boolean;
|
||||
targetType?: "ai" | "human" | "queue" | "phone";
|
||||
target?: string;
|
||||
message?: string;
|
||||
@@ -224,13 +234,26 @@ export function defaultGraph(): WorkflowGraph {
|
||||
position: { x: 360, y: 60 },
|
||||
data: {
|
||||
name: "Start",
|
||||
greeting: "你好,我是 AI 视频助手,有什么可以帮你?",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "message-1",
|
||||
type: "message",
|
||||
position: { x: 360, y: 260 },
|
||||
data: {
|
||||
name: "开场消息",
|
||||
speech: "你好,我是 AI 视频助手,有什么可以帮你?",
|
||||
showMessage: false,
|
||||
title: "重要提示",
|
||||
message: "",
|
||||
confirmLabel: "确认",
|
||||
requireConfirmation: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "agent-1",
|
||||
type: "agent",
|
||||
position: { x: 360, y: 300 },
|
||||
position: { x: 360, y: 480 },
|
||||
data: {
|
||||
name: "Agent",
|
||||
prompt: "了解用户需求并提供清晰、准确的帮助。",
|
||||
@@ -243,7 +266,7 @@ export function defaultGraph(): WorkflowGraph {
|
||||
{
|
||||
id: "end",
|
||||
type: "end",
|
||||
position: { x: 360, y: 540 },
|
||||
position: { x: 360, y: 700 },
|
||||
data: {
|
||||
name: "End",
|
||||
message: "感谢你的来电,再见。",
|
||||
@@ -253,8 +276,14 @@ export function defaultGraph(): WorkflowGraph {
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: "e-start-agent",
|
||||
id: "e-start-message",
|
||||
source: "start",
|
||||
target: "message-1",
|
||||
data: { mode: "always", priority: 0 },
|
||||
},
|
||||
{
|
||||
id: "e-message-agent",
|
||||
source: "message-1",
|
||||
target: "agent-1",
|
||||
data: { mode: "always", priority: 0 },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user