176 lines
6.1 KiB
TypeScript
176 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import { ArrowRight, MessageSquareText } from "lucide-react";
|
|
|
|
import { SectionCard } from "@/components/editor/section-card";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
|
|
import {
|
|
type MessageCompletionPolicy,
|
|
type WorkflowNodeData,
|
|
} from "../specs";
|
|
import { PanelAnchor } from "./PanelAnchorNavigation";
|
|
|
|
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,
|
|
set,
|
|
setPatch,
|
|
}: {
|
|
draft: WorkflowNodeData;
|
|
set: (key: string, value: unknown) => void;
|
|
setPatch: (patch: Partial<WorkflowNodeData>) => void;
|
|
}) {
|
|
const completionPolicy = draft.completionPolicy ?? "playback";
|
|
const completionDescription = COMPLETION_OPTIONS.find(
|
|
(option) => option.value === completionPolicy,
|
|
)?.description;
|
|
|
|
const selectCompletionPolicy = (value: MessageCompletionPolicy) => {
|
|
setPatch({ completionPolicy: value });
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<PanelAnchor id="speech">
|
|
<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>
|
|
</label>
|
|
|
|
{!draft.speech?.trim() && (
|
|
<p role="alert" className="text-xs leading-5 text-destructive">
|
|
请输入播报内容。
|
|
</p>
|
|
)}
|
|
</SectionCard>
|
|
</PanelAnchor>
|
|
|
|
<PanelAnchor id="completion">
|
|
<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>
|
|
|
|
{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>
|
|
</PanelAnchor>
|
|
</>
|
|
);
|
|
}
|