Update node specifications and enhance GenericNode component
- Change the 'addable' property of a specific node type to true, allowing for dynamic addition of nodes. - Modify the GenericNode component to include a new icon and adjust styles for better visual representation. - Update node handling logic to prevent deletion of 'startCall' nodes and improve node change handling in the workflow editor. - Refactor layout and styling in the WorkflowEditor for a more polished user interface.
This commit is contained in:
@@ -56,7 +56,7 @@ NODE_SPECS: list[dict[str, Any]] = [
|
|||||||
"description": "终止节点,礼貌结束对话。可有多个,均无出边。",
|
"description": "终止节点,礼貌结束对话。可有多个,均无出边。",
|
||||||
"icon": "Flag",
|
"icon": "Flag",
|
||||||
"accent": "rose",
|
"accent": "rose",
|
||||||
"addable": False,
|
"addable": True,
|
||||||
"constraints": {"minIncoming": 1, "minOutgoing": 0, "maxOutgoing": 0},
|
"constraints": {"minIncoming": 1, "minOutgoing": 0, "maxOutgoing": 0},
|
||||||
"fields": [
|
"fields": [
|
||||||
{"key": "name", "label": "节点名称", "type": "text"},
|
{"key": "name", "label": "节点名称", "type": "text"},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
||||||
import { Pencil, Trash2 } from "lucide-react";
|
import { MessageSquareText, Pencil, Trash2 } from "lucide-react";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -35,22 +35,30 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
|
|||||||
<div
|
<div
|
||||||
data-node-id={id}
|
data-node-id={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group relative w-[228px] rounded-2xl border bg-card p-4 shadow-sm transition-all",
|
"group relative w-[250px] rounded-2xl border bg-card p-4 text-card-foreground shadow-sm transition-[border-color,box-shadow,transform]",
|
||||||
isActive
|
isActive
|
||||||
? "border-success ring-2 ring-success/60"
|
? "border-success shadow-[0_12px_34px_color-mix(in_srgb,var(--success)_20%,transparent)] ring-2 ring-success/50"
|
||||||
: selected
|
: selected
|
||||||
? "border-primary ring-2 ring-primary/30"
|
? "border-primary shadow-[0_12px_34px_color-mix(in_srgb,var(--primary)_16%,transparent)]"
|
||||||
: "border-hairline hover:shadow-md",
|
: "border-hairline hover:border-hairline-strong hover:shadow-md",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{spec.hasTarget && (
|
{spec.hasTarget && (
|
||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-2.5 !w-2.5 !border-2 !border-card !bg-muted-soft"
|
className="!h-3 !w-3 !border-[3px] !border-card !bg-muted-soft"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="absolute left-5 right-5 top-0 h-px"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(90deg, transparent, var(${accentVar(spec.accent)}), transparent)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<div className="absolute -top-3 left-3 flex items-center gap-1.5 rounded-full bg-success px-2 py-0.5 text-[10px] font-medium text-on-primary shadow-sm">
|
<div className="absolute -top-3 left-3 flex items-center gap-1.5 rounded-full bg-success px-2 py-0.5 text-[10px] font-medium text-on-primary shadow-sm">
|
||||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-on-primary" />
|
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-on-primary" />
|
||||||
@@ -78,7 +86,7 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
|
|||||||
>
|
>
|
||||||
<Pencil size={13} />
|
<Pencil size={13} />
|
||||||
</button>
|
</button>
|
||||||
{spec.addable && (
|
{type !== "startCall" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="删除节点"
|
title="删除节点"
|
||||||
@@ -93,30 +101,31 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div
|
<div
|
||||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-ink"
|
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-foreground"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `radial-gradient(circle at 30% 30%, color-mix(in srgb, var(${accentVar(spec.accent)}) 70%, transparent), color-mix(in srgb, var(${accentVar(spec.accent)}) 35%, transparent))`,
|
background: `color-mix(in srgb, var(${accentVar(spec.accent)}) 28%, var(--surface-strong))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon size={18} />
|
<Icon size={17} />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate text-sm font-medium text-foreground">
|
<div className="caption-label text-muted-soft">{spec.displayName}</div>
|
||||||
|
<div className="mt-1 truncate text-sm font-medium text-foreground">
|
||||||
{nodeData.name || spec.displayName}
|
{nodeData.name || spec.displayName}
|
||||||
</div>
|
</div>
|
||||||
<div className="caption-label text-muted-soft">{spec.displayName}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<MessageSquareText size={15} className="mt-1 text-muted-soft" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{preview ? (
|
{preview ? (
|
||||||
<p className="mt-3 line-clamp-2 text-xs leading-5 text-muted-foreground">
|
<p className="mt-4 line-clamp-2 border-t border-hairline-soft pt-3 text-xs leading-5 text-muted-foreground">
|
||||||
{preview}
|
{preview}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-3 text-xs italic leading-5 text-muted-soft">
|
<p className="mt-4 border-t border-hairline-soft pt-3 text-xs leading-5 text-muted-foreground">
|
||||||
点击编辑节点内容…
|
{spec.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -124,7 +133,7 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
|
|||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-2.5 !w-2.5 !border-2 !border-card !bg-primary"
|
className="!h-3 !w-3 !border-[3px] !border-card !bg-primary"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
type Edge,
|
type Edge,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
type Node,
|
type Node,
|
||||||
|
type NodeChange,
|
||||||
Panel,
|
Panel,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
ReactFlowProvider,
|
ReactFlowProvider,
|
||||||
@@ -214,11 +215,26 @@ function Canvas({
|
|||||||
|
|
||||||
const deleteNode = useCallback(
|
const deleteNode = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
|
if (nodes.find((node) => node.id === id)?.type === "startCall") return;
|
||||||
setNodes((ns) => ns.filter((n) => n.id !== id));
|
setNodes((ns) => ns.filter((n) => n.id !== id));
|
||||||
setEdges((es) => es.filter((e) => e.source !== id && e.target !== id));
|
setEdges((es) => es.filter((e) => e.source !== id && e.target !== id));
|
||||||
setEditingId((cur) => (cur === id ? null : cur));
|
setEditingId((cur) => (cur === id ? null : cur));
|
||||||
},
|
},
|
||||||
[setNodes, setEdges],
|
[nodes, setNodes, setEdges],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNodesChange = useCallback(
|
||||||
|
(changes: NodeChange[]) => {
|
||||||
|
const startIds = new Set(
|
||||||
|
nodes.filter((node) => node.type === "startCall").map((node) => node.id),
|
||||||
|
);
|
||||||
|
onNodesChange(
|
||||||
|
changes.filter(
|
||||||
|
(change) => change.type !== "remove" || !startIds.has(change.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[nodes, onNodesChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateEdgeData = useCallback(
|
const updateEdgeData = useCallback(
|
||||||
@@ -259,96 +275,151 @@ function Canvas({
|
|||||||
<ActiveNodeContext.Provider value={activeNodeId ?? null}>
|
<ActiveNodeContext.Provider value={activeNodeId ?? null}>
|
||||||
<NodeActionContext.Provider value={nodeActions}>
|
<NodeActionContext.Provider value={nodeActions}>
|
||||||
<EdgeActionContext.Provider value={edgeActions}>
|
<EdgeActionContext.Provider value={edgeActions}>
|
||||||
<div className="h-full w-full overflow-hidden rounded-2xl border border-hairline bg-canvas-soft">
|
<div className="h-full w-full min-h-[560px]">
|
||||||
<ReactFlow
|
<section className="relative h-full w-full overflow-hidden rounded-2xl border border-hairline bg-canvas-soft shadow-sm">
|
||||||
nodes={nodes}
|
<div
|
||||||
edges={edges}
|
aria-hidden
|
||||||
nodeTypes={nodeTypes}
|
className="pointer-events-none absolute -right-24 -top-24 z-0 h-80 w-80 rounded-full opacity-30 blur-3xl"
|
||||||
edgeTypes={edgeTypes}
|
style={{
|
||||||
onNodesChange={onNodesChange}
|
background:
|
||||||
onEdgesChange={onEdgesChange}
|
"radial-gradient(circle, var(--gradient-sky), transparent 68%)",
|
||||||
onConnect={onConnect}
|
}}
|
||||||
isValidConnection={isValidConnection}
|
|
||||||
onPaneClick={() => {
|
|
||||||
setEditingId(null);
|
|
||||||
setEditingEdgeId(null);
|
|
||||||
}}
|
|
||||||
fitView
|
|
||||||
proOptions={{ hideAttribution: true }}
|
|
||||||
defaultEdgeOptions={{ type: "condition", animated: true }}
|
|
||||||
className="bg-canvas-soft"
|
|
||||||
>
|
|
||||||
<Background
|
|
||||||
variant={BackgroundVariant.Dots}
|
|
||||||
gap={20}
|
|
||||||
size={1}
|
|
||||||
color="var(--hairline-strong)"
|
|
||||||
/>
|
/>
|
||||||
<Controls className="!rounded-xl !border !border-hairline !bg-card !shadow-sm [&_button]:!border-hairline [&_button]:!bg-card [&_button]:!text-foreground" />
|
<div
|
||||||
<MiniMap
|
aria-hidden
|
||||||
pannable
|
className="pointer-events-none absolute -bottom-28 left-1/4 z-0 h-72 w-72 rounded-full opacity-25 blur-3xl"
|
||||||
zoomable
|
style={{
|
||||||
className="!rounded-xl !border !border-hairline !bg-card"
|
background:
|
||||||
maskColor="color-mix(in srgb, var(--canvas-soft) 70%, transparent)"
|
"radial-gradient(circle, var(--gradient-lavender), transparent 68%)",
|
||||||
nodeColor="var(--surface-strong)"
|
}}
|
||||||
/>
|
/>
|
||||||
<Panel position="top-left" className="flex gap-2">
|
|
||||||
<Button size="sm" className="gap-2" onClick={() => setAddOpen(true)}>
|
<ReactFlow
|
||||||
<Plus size={15} />
|
nodes={nodes}
|
||||||
添加节点
|
edges={edges}
|
||||||
</Button>
|
nodeTypes={nodeTypes}
|
||||||
<Button
|
edgeTypes={edgeTypes}
|
||||||
size="sm"
|
onNodesChange={handleNodesChange}
|
||||||
variant="outline"
|
onEdgesChange={onEdgesChange}
|
||||||
className="gap-2 border-hairline-strong bg-card text-foreground hover:bg-surface-strong"
|
onConnect={onConnect}
|
||||||
onClick={() => setSettingsOpen(true)}
|
isValidConnection={isValidConnection}
|
||||||
>
|
onPaneClick={() => {
|
||||||
<Settings2 size={15} />
|
setEditingId(null);
|
||||||
工作流设置
|
setEditingEdgeId(null);
|
||||||
</Button>
|
}}
|
||||||
</Panel>
|
fitView
|
||||||
</ReactFlow>
|
proOptions={{ hideAttribution: true }}
|
||||||
|
defaultEdgeOptions={{ type: "condition", animated: true }}
|
||||||
|
>
|
||||||
|
<Background
|
||||||
|
variant={BackgroundVariant.Dots}
|
||||||
|
gap={22}
|
||||||
|
size={1}
|
||||||
|
color="var(--hairline-strong)"
|
||||||
|
/>
|
||||||
|
<Controls
|
||||||
|
className="!rounded-xl !border !border-hairline !bg-card !shadow-sm [&_button]:!border-hairline [&_button]:!bg-card [&_button]:!text-foreground"
|
||||||
|
/>
|
||||||
|
<MiniMap
|
||||||
|
pannable
|
||||||
|
zoomable
|
||||||
|
className="!rounded-xl !border !border-hairline !bg-card"
|
||||||
|
maskColor="color-mix(in srgb, var(--canvas-soft) 70%, transparent)"
|
||||||
|
nodeColor="var(--surface-strong)"
|
||||||
|
/>
|
||||||
|
<Panel position="top-left" className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
onClick={() => setAddOpen(true)}
|
||||||
|
>
|
||||||
|
<Plus size={15} />
|
||||||
|
添加节点
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2 border-hairline-strong bg-card text-foreground hover:bg-surface-strong"
|
||||||
|
onClick={() => setSettingsOpen(true)}
|
||||||
|
>
|
||||||
|
<Settings2 size={15} />
|
||||||
|
工作流设置
|
||||||
|
</Button>
|
||||||
|
</Panel>
|
||||||
|
</ReactFlow>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* 添加节点弹窗 */}
|
{/* 添加节点弹窗 */}
|
||||||
<Dialog open={addOpen} onOpenChange={setAddOpen}>
|
<Dialog open={addOpen} onOpenChange={setAddOpen}>
|
||||||
<DialogContent className="sm:max-w-[460px]">
|
<DialogContent className="gap-0 overflow-hidden border border-hairline bg-card p-0 shadow-2xl sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader className="relative overflow-hidden border-b border-hairline px-6 py-6 pr-16">
|
||||||
<DialogTitle className="font-display text-ink">
|
<div
|
||||||
添加节点
|
aria-hidden
|
||||||
</DialogTitle>
|
className="pointer-events-none absolute -right-14 -top-16 h-40 w-40 rounded-full opacity-40 blur-3xl"
|
||||||
<DialogDescription>
|
style={{
|
||||||
选择要添加到画布的节点类型。
|
background:
|
||||||
</DialogDescription>
|
"radial-gradient(circle, var(--gradient-sky), transparent 68%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="relative flex items-start gap-4">
|
||||||
|
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||||
|
<Plus size={18} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="caption-label text-muted-soft">
|
||||||
|
节点目录
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="font-display display-sm mt-1 text-ink">
|
||||||
|
添加节点
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="mt-2 leading-6">
|
||||||
|
选择节点类型并添加到画布中央,随后可编辑内容并建立连线。
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-2 py-2">
|
<div className="flex max-h-[440px] flex-col gap-3 overflow-y-auto bg-canvas-soft/70 p-4">
|
||||||
{addableSpecs.length === 0 && (
|
{addableSpecs.length === 0 ? (
|
||||||
<p className="text-sm text-muted-soft">暂无可添加的节点类型。</p>
|
<p className="rounded-2xl border border-dashed border-hairline-strong bg-card px-4 py-8 text-center text-sm text-muted-soft">
|
||||||
)}
|
暂无可添加的节点类型。
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
{addableSpecs.map((spec) => {
|
{addableSpecs.map((spec) => {
|
||||||
const Icon = spec.icon;
|
const Icon = spec.icon;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={spec.type}
|
key={spec.type}
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-start gap-3 rounded-xl border border-hairline bg-card p-3 text-left transition-colors hover:border-primary hover:bg-surface-strong/40"
|
className="group relative flex items-start gap-4 overflow-hidden rounded-2xl border border-hairline bg-card p-4 text-left shadow-sm transition-[border-color,box-shadow,transform] hover:-translate-y-0.5 hover:border-hairline-strong hover:shadow-md"
|
||||||
onClick={() => addNode(spec)}
|
onClick={() => addNode(spec)}
|
||||||
>
|
>
|
||||||
<div
|
<span
|
||||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-ink"
|
aria-hidden
|
||||||
|
className="absolute left-5 right-5 top-0 h-px"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `radial-gradient(circle at 30% 30%, color-mix(in srgb, var(${accentVar(spec.accent)}) 70%, transparent), color-mix(in srgb, var(${accentVar(spec.accent)}) 35%, transparent))`,
|
background: `linear-gradient(90deg, transparent, var(${accentVar(spec.accent)}), transparent)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-foreground transition-transform group-hover:scale-105"
|
||||||
|
style={{
|
||||||
|
background: `color-mix(in srgb, var(${accentVar(spec.accent)}) 28%, var(--surface-strong))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon size={18} />
|
<Icon size={17} />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-medium text-foreground">
|
<div className="text-sm font-medium text-foreground">
|
||||||
{spec.displayName}
|
{spec.displayName}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 text-xs leading-5 text-muted-foreground">
|
<div className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||||
{spec.description}
|
{spec.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Plus
|
||||||
|
size={15}
|
||||||
|
className="mt-1 shrink-0 text-muted-soft transition-colors group-hover:text-foreground"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -358,16 +429,34 @@ function Canvas({
|
|||||||
|
|
||||||
{/* 工作流设置弹窗 */}
|
{/* 工作流设置弹窗 */}
|
||||||
<Dialog open={settingsOpen} onOpenChange={setSettingsOpen}>
|
<Dialog open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||||
<DialogContent className="sm:max-w-[460px]">
|
<DialogContent className="gap-0 overflow-hidden border border-hairline bg-card p-0 shadow-2xl sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader className="relative overflow-hidden border-b border-hairline px-6 py-6 pr-16">
|
||||||
<DialogTitle className="font-display text-ink">
|
<div
|
||||||
工作流设置
|
aria-hidden
|
||||||
</DialogTitle>
|
className="pointer-events-none absolute -right-14 -top-16 h-40 w-40 rounded-full opacity-40 blur-3xl"
|
||||||
<DialogDescription>
|
style={{
|
||||||
配置整个工作流使用的语音与大模型,以及交互策略。
|
background:
|
||||||
</DialogDescription>
|
"radial-gradient(circle, var(--gradient-lavender), transparent 68%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="relative flex items-start gap-4">
|
||||||
|
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||||
|
<Settings2 size={18} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="caption-label text-muted-soft">
|
||||||
|
全局配置
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="font-display display-sm mt-1 text-ink">
|
||||||
|
工作流设置
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="mt-2 leading-6">
|
||||||
|
配置整个工作流使用的语音、大模型与交互策略。
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-5 py-2">
|
<div className="flex max-h-[62vh] flex-col gap-3 overflow-y-auto bg-canvas-soft/70 p-4">
|
||||||
<ModelSelect
|
<ModelSelect
|
||||||
label="大模型(LLM)"
|
label="大模型(LLM)"
|
||||||
value={settings.llm}
|
value={settings.llm}
|
||||||
@@ -386,9 +475,14 @@ function Canvas({
|
|||||||
options={modelOptions.tts}
|
options={modelOptions.tts}
|
||||||
onChange={(v) => onSettingsChange({ ...settings, tts: v })}
|
onChange={(v) => onSettingsChange({ ...settings, tts: v })}
|
||||||
/>
|
/>
|
||||||
<label className="flex items-center justify-between gap-3 pt-1">
|
<label className="flex items-center justify-between gap-4 rounded-2xl border border-hairline bg-card p-4 shadow-sm">
|
||||||
<span className="text-sm font-medium text-foreground">
|
<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>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
checked={settings.allowInterrupt}
|
checked={settings.allowInterrupt}
|
||||||
@@ -398,7 +492,7 @@ function Canvas({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter className="border-t border-hairline bg-card px-5 py-4">
|
||||||
<Button onClick={() => setSettingsOpen(false)}>完成</Button>
|
<Button onClick={() => setSettingsOpen(false)}>完成</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -420,7 +514,7 @@ function Canvas({
|
|||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
}}
|
}}
|
||||||
onDelete={
|
onDelete={
|
||||||
editingSpec.addable
|
editingNode.type !== "startCall"
|
||||||
? () => deleteNode(editingNode.id)
|
? () => deleteNode(editingNode.id)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -468,13 +562,13 @@ function ModelSelect({
|
|||||||
onChange: (value: string | undefined) => void;
|
onChange: (value: string | undefined) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2 rounded-2xl border border-hairline bg-card p-4 shadow-sm">
|
||||||
<label className="text-sm font-medium text-foreground">{label}</label>
|
<label className="text-sm font-medium text-foreground">{label}</label>
|
||||||
<Select
|
<Select
|
||||||
value={value ?? NONE}
|
value={value ?? NONE}
|
||||||
onValueChange={(v) => onChange(v === NONE ? undefined : v)}
|
onValueChange={(v) => onChange(v === NONE ? undefined : v)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="border-hairline-strong bg-background text-foreground">
|
<SelectTrigger className="w-full border-hairline-strong bg-canvas-soft text-foreground">
|
||||||
<SelectValue placeholder="选择模型资源" />
|
<SelectValue placeholder="选择模型资源" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user