feat: enhance workflow node components with new condition handling and UI improvements
This commit is contained in:
@@ -1,22 +1,31 @@
|
||||
import { Field, type WorkflowNodeRegistry, useNodeRender } from "@flowgram.ai/free-layout-editor"
|
||||
import { PlusIcon, XIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import type { AIWorkflowNodeSpec } from "@/lib/api/admin"
|
||||
import {
|
||||
createConditionBranchID,
|
||||
normalizeNodeConfig,
|
||||
type WorkflowConditionBranch,
|
||||
} from "./workflow-utils"
|
||||
import { useWorkflowBranchSelection } from "./workflow-branch-selection"
|
||||
import { WorkflowConditionNodeContent } from "./workflow-condition-node-content"
|
||||
import { WorkflowNodeCard } from "./workflow-node-card"
|
||||
import { WorkflowSimpleNodeContent } from "./workflow-simple-node-content"
|
||||
|
||||
export function buildFlowgramNodeRegistries(nodeSpecs: AIWorkflowNodeSpec[]): WorkflowNodeRegistry[] {
|
||||
const seen = new Set<string>()
|
||||
const specs = nodeSpecs.length > 0
|
||||
? nodeSpecs
|
||||
: [
|
||||
{ type: "start", title: "开始" },
|
||||
{ type: "end", title: "结束" },
|
||||
{
|
||||
type: "start",
|
||||
title: "开始",
|
||||
description: "流程入口",
|
||||
riskLevel: "low" as const,
|
||||
interruptible: false,
|
||||
requiresConfirmationPredecessor: false,
|
||||
},
|
||||
{
|
||||
type: "end",
|
||||
title: "结束",
|
||||
description: "流程结束",
|
||||
riskLevel: "low" as const,
|
||||
interruptible: false,
|
||||
requiresConfirmationPredecessor: false,
|
||||
},
|
||||
]
|
||||
|
||||
return specs
|
||||
@@ -41,6 +50,7 @@ export function buildFlowgramNodeRegistries(nodeSpecs: AIWorkflowNodeSpec[]): Wo
|
||||
<FlowgramNodeForm
|
||||
nodeType={spec.type}
|
||||
fallbackTitle={spec.title || spec.type}
|
||||
spec={spec}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -50,33 +60,41 @@ export function buildFlowgramNodeRegistries(nodeSpecs: AIWorkflowNodeSpec[]): Wo
|
||||
function FlowgramNodeForm({
|
||||
nodeType,
|
||||
fallbackTitle,
|
||||
spec,
|
||||
}: {
|
||||
nodeType: string
|
||||
fallbackTitle: string
|
||||
spec?: AIWorkflowNodeSpec
|
||||
}) {
|
||||
const { node } = useNodeRender()
|
||||
const { node, selected } = useNodeRender()
|
||||
const nodeId = String(node.id ?? "")
|
||||
|
||||
if (nodeType === "condition") {
|
||||
return (
|
||||
<ConditionNodeForm
|
||||
fallbackTitle={fallbackTitle}
|
||||
nodeId={nodeId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
<Field<string> name="title">
|
||||
{({ field }) => (
|
||||
<div className="border-b px-3 py-2.5 text-sm font-medium leading-5">
|
||||
{field.value || fallbackTitle}
|
||||
</div>
|
||||
<WorkflowNodeCard
|
||||
nodeType={nodeType}
|
||||
title={field.value || fallbackTitle}
|
||||
spec={spec}
|
||||
selected={selected}
|
||||
width={nodeType === "condition" ? "wide" : "normal"}
|
||||
>
|
||||
{nodeType === "condition" ? (
|
||||
<Field<Record<string, unknown>> name="config">
|
||||
{({ field: configField }) => (
|
||||
<WorkflowConditionNodeContent
|
||||
configValue={configField.value}
|
||||
nodeId={nodeId}
|
||||
onChange={configField.onChange}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
) : (
|
||||
<WorkflowSimpleNodeContent spec={spec} />
|
||||
)}
|
||||
</WorkflowNodeCard>
|
||||
)}
|
||||
</Field>
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground">{nodeType}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -92,133 +110,3 @@ function defaultPortsForNodeType(type: string) {
|
||||
}
|
||||
return [{ type: "input" as const }, { type: "output" as const }]
|
||||
}
|
||||
|
||||
function ConditionNodeForm({
|
||||
fallbackTitle,
|
||||
nodeId,
|
||||
}: {
|
||||
fallbackTitle: string
|
||||
nodeId: string
|
||||
}) {
|
||||
const { selectedBranch, onSelectBranch } = useWorkflowBranchSelection()
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
<Field<string> name="title">
|
||||
{({ field }) => (
|
||||
<div className="border-b px-3 py-2.5 text-sm font-medium leading-5">
|
||||
{field.value || fallbackTitle}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Field<Record<string, unknown>> name="config">
|
||||
{({ field }) => {
|
||||
const config = normalizeNodeConfig(field.value)
|
||||
const branches = ensureConditionBranches(config.branches ?? [])
|
||||
const updateBranches = (nextBranches: WorkflowConditionBranch[]) => {
|
||||
field.onChange({
|
||||
...config,
|
||||
branches: ensureConditionBranches(nextBranches),
|
||||
})
|
||||
}
|
||||
const deleteBranch = (branchId: string) => {
|
||||
updateBranches(branches.filter((branch) => branch.id !== branchId))
|
||||
if (selectedBranch?.nodeId === nodeId && selectedBranch.branchId === branchId) {
|
||||
onSelectBranch?.(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-3 py-2.5">
|
||||
<div className="space-y-2">
|
||||
{branches.map((branch, index) => (
|
||||
<div
|
||||
key={branch.id}
|
||||
className={[
|
||||
"relative flex min-h-9 cursor-pointer items-center gap-2 rounded-sm border px-2.5 text-xs transition-colors",
|
||||
selectedBranch?.nodeId === nodeId && selectedBranch.branchId === branch.id
|
||||
? "border-[var(--g-selection-background)] bg-muted"
|
||||
: "border-transparent bg-muted/60 hover:bg-muted",
|
||||
].join(" ")}
|
||||
onPointerDownCapture={(event) => {
|
||||
event.stopPropagation()
|
||||
onSelectBranch?.({ nodeId, branchId: branch.id })
|
||||
}}
|
||||
onMouseDownCapture={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate font-medium text-foreground/90">
|
||||
{branch.name || (branch.default ? "默认分支" : branch.id)}
|
||||
</span>
|
||||
<span className="shrink-0 rounded-sm border bg-background px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
||||
{branch.default ? "else" : index === 0 ? "if" : "elseif"}
|
||||
</span>
|
||||
{branch.default ? null : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex size-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
||||
aria-label={`删除条件 ${branch.name || branch.id}`}
|
||||
onPointerDownCapture={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onMouseDownCapture={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
deleteBranch(branch.id)
|
||||
}}
|
||||
>
|
||||
<XIcon className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<span
|
||||
data-port-id={branch.id}
|
||||
data-port-type="output"
|
||||
className="absolute -right-4 top-1/2 size-0"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="mt-2 h-7 px-2 text-xs text-muted-foreground"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
updateBranches([
|
||||
...branches,
|
||||
{
|
||||
id: createConditionBranchID(branches),
|
||||
name: "新条件",
|
||||
targetNodeId: "",
|
||||
condition: { operator: "eq" },
|
||||
},
|
||||
])
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="size-3.5" />
|
||||
添加条件
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ensureConditionBranches(branches: WorkflowConditionBranch[]) {
|
||||
const normalized = branches.some((branch) => branch.default)
|
||||
? branches
|
||||
: [...branches, { id: "default", name: "默认分支", targetNodeId: "", default: true }]
|
||||
return [
|
||||
...normalized.filter((branch) => !branch.default),
|
||||
...normalized.filter((branch) => branch.default).slice(0, 1),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useLayoutEffect } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function FlowgramNodeRenderer(props: WorkflowNodeProps) {
|
||||
const { selected, node, form } = useNodeRender()
|
||||
const { node, form } = useNodeRender()
|
||||
const nodeType = String(node.flowNodeType ?? "")
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -24,10 +24,7 @@ export function FlowgramNodeRenderer(props: WorkflowNodeProps) {
|
||||
return (
|
||||
<WorkflowNodeRenderer
|
||||
node={props.node}
|
||||
className={cn(
|
||||
"w-[320px] overflow-visible rounded-md border bg-background shadow-sm transition-colors",
|
||||
selected ? "border-[var(--g-selection-background)]" : "border-border"
|
||||
)}
|
||||
className={cn("overflow-visible")}
|
||||
style={{ padding: 0 }}
|
||||
portPrimaryColor="var(--g-selection-background)"
|
||||
portSecondaryColor="#c9cdd4"
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { PlusIcon, XIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useWorkflowBranchSelection } from "./workflow-branch-selection"
|
||||
import {
|
||||
createConditionBranchID,
|
||||
normalizeNodeConfig,
|
||||
type WorkflowConditionBranch,
|
||||
} from "./workflow-utils"
|
||||
|
||||
export function WorkflowConditionNodeContent({
|
||||
configValue,
|
||||
nodeId,
|
||||
onChange,
|
||||
}: {
|
||||
configValue: Record<string, unknown> | undefined
|
||||
nodeId: string
|
||||
onChange: (value: Record<string, unknown>) => void
|
||||
}) {
|
||||
const { selectedBranch, onSelectBranch } = useWorkflowBranchSelection()
|
||||
const config = normalizeNodeConfig(configValue)
|
||||
const branches = ensureConditionBranches(config.branches ?? [])
|
||||
|
||||
const updateBranches = (nextBranches: WorkflowConditionBranch[]) => {
|
||||
onChange({
|
||||
...config,
|
||||
branches: ensureConditionBranches(nextBranches),
|
||||
})
|
||||
}
|
||||
const deleteBranch = (branchId: string) => {
|
||||
updateBranches(branches.filter((branch) => branch.id !== branchId))
|
||||
if (selectedBranch?.nodeId === nodeId && selectedBranch.branchId === branchId) {
|
||||
onSelectBranch?.(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{branches.map((branch, index) => (
|
||||
<WorkflowConditionBranchRow
|
||||
key={branch.id}
|
||||
branch={branch}
|
||||
index={index}
|
||||
selected={selectedBranch?.nodeId === nodeId && selectedBranch.branchId === branch.id}
|
||||
onSelect={() => onSelectBranch?.({ nodeId, branchId: branch.id })}
|
||||
onDelete={() => deleteBranch(branch.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs text-muted-foreground"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
updateBranches([
|
||||
...branches,
|
||||
{
|
||||
id: createConditionBranchID(branches),
|
||||
name: "新条件",
|
||||
targetNodeId: "",
|
||||
condition: { operator: "eq" },
|
||||
},
|
||||
])
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="size-3.5" />
|
||||
添加条件
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkflowConditionBranchRow({
|
||||
branch,
|
||||
index,
|
||||
selected,
|
||||
onSelect,
|
||||
onDelete,
|
||||
}: {
|
||||
branch: WorkflowConditionBranch
|
||||
index: number
|
||||
selected: boolean
|
||||
onSelect: () => void
|
||||
onDelete: () => void
|
||||
}) {
|
||||
const branchType = branch.default ? "else" : index === 0 ? "if" : "elseif"
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex min-h-10 cursor-pointer items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs transition-colors",
|
||||
selected
|
||||
? "border-[var(--g-selection-background)] bg-background shadow-sm"
|
||||
: "border-border/50 bg-background/70 hover:border-border hover:bg-background"
|
||||
)}
|
||||
onPointerDownCapture={(event) => {
|
||||
event.stopPropagation()
|
||||
onSelect()
|
||||
}}
|
||||
onMouseDownCapture={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<span className="shrink-0 rounded-md border bg-muted px-1.5 py-0.5 text-[10px] font-semibold text-muted-foreground">
|
||||
{branchType}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate font-medium text-foreground/90">
|
||||
{branch.name || (branch.default ? "默认分支" : branch.id)}
|
||||
</span>
|
||||
{branch.default ? null : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex size-5 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
||||
aria-label={`删除条件 ${branch.name || branch.id}`}
|
||||
onPointerDownCapture={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onMouseDownCapture={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onDelete()
|
||||
}}
|
||||
>
|
||||
<XIcon className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<span
|
||||
data-port-id={branch.id}
|
||||
data-port-type="output"
|
||||
className="absolute -right-4 top-1/2 size-0"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ensureConditionBranches(branches: WorkflowConditionBranch[]) {
|
||||
const normalized = branches.some((branch) => branch.default)
|
||||
? branches
|
||||
: [...branches, { id: "default", name: "默认分支", targetNodeId: "", default: true }]
|
||||
return [
|
||||
...normalized.filter((branch) => !branch.default),
|
||||
...normalized.filter((branch) => branch.default).slice(0, 1),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { AIWorkflowNodeSpec } from "@/lib/api/admin"
|
||||
import { getWorkflowNodeAccentClass, getWorkflowNodeMeta } from "./workflow-node-meta"
|
||||
import { WorkflowNodeIcon } from "./workflow-node-icon"
|
||||
|
||||
export function WorkflowNodeCard({
|
||||
nodeType,
|
||||
title,
|
||||
spec,
|
||||
selected,
|
||||
width = "normal",
|
||||
children,
|
||||
}: {
|
||||
nodeType: string
|
||||
title: string
|
||||
spec?: AIWorkflowNodeSpec
|
||||
selected: boolean
|
||||
width?: "normal" | "wide"
|
||||
children: ReactNode
|
||||
}) {
|
||||
const meta = getWorkflowNodeMeta(nodeType)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative rounded-2xl border bg-background p-0.5 transition-all",
|
||||
width === "wide" ? "w-[320px]" : "w-[280px]",
|
||||
selected
|
||||
? "border-[var(--g-selection-background)] shadow-[0_8px_24px_rgba(20,24,38,0.14)]"
|
||||
: "border-border/80 shadow-sm hover:border-border hover:shadow-md"
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden rounded-[15px] border border-transparent bg-background">
|
||||
<div className="flex items-center gap-2 px-3 pb-2 pt-3">
|
||||
<WorkflowNodeIcon type={nodeType} tone={meta.tone} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold leading-5 text-foreground">
|
||||
{title}
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex h-4 shrink-0 items-center rounded-md border px-1.5 text-[10px] font-medium leading-none",
|
||||
getWorkflowNodeAccentClass(meta.tone)
|
||||
)}
|
||||
>
|
||||
{meta.label}
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-[11px] leading-4 text-muted-foreground">
|
||||
{nodeType}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{spec?.description ? (
|
||||
<div className="mx-3 mb-2 line-clamp-2 rounded-lg bg-muted/45 px-2 py-1.5 text-xs leading-4 text-muted-foreground">
|
||||
{spec.description}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="border-t bg-muted/20 px-3 py-2.5">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
getWorkflowNodeIconClass,
|
||||
getWorkflowNodeMeta,
|
||||
type WorkflowNodeTone,
|
||||
} from "./workflow-node-meta"
|
||||
|
||||
export function WorkflowNodeIcon({
|
||||
type,
|
||||
tone,
|
||||
size = "md",
|
||||
className,
|
||||
}: {
|
||||
type: string
|
||||
tone?: WorkflowNodeTone
|
||||
size?: "sm" | "md"
|
||||
className?: string
|
||||
}) {
|
||||
const meta = getWorkflowNodeMeta(type)
|
||||
const Icon = meta.icon
|
||||
const resolvedTone = tone ?? meta.tone
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex shrink-0 items-center justify-center rounded-lg shadow-sm",
|
||||
size === "md" ? "size-7" : "size-6",
|
||||
getWorkflowNodeIconClass(resolvedTone),
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Icon className={size === "md" ? "size-4" : "size-3.5"} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
BookOpenIcon,
|
||||
BotIcon,
|
||||
ClipboardListIcon,
|
||||
FileTextIcon,
|
||||
FlagIcon,
|
||||
GitBranchIcon,
|
||||
HeadphonesIcon,
|
||||
HelpCircleIcon,
|
||||
MessageCircleIcon,
|
||||
PlayCircleIcon,
|
||||
SearchIcon,
|
||||
SendIcon,
|
||||
ShieldCheckIcon,
|
||||
TicketIcon,
|
||||
UserCheckIcon,
|
||||
type LucideIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
export type WorkflowNodeTone =
|
||||
| "blue"
|
||||
| "cyan"
|
||||
| "emerald"
|
||||
| "indigo"
|
||||
| "amber"
|
||||
| "violet"
|
||||
| "rose"
|
||||
| "slate"
|
||||
|
||||
export type WorkflowNodeMeta = {
|
||||
icon: LucideIcon
|
||||
tone: WorkflowNodeTone
|
||||
label: string
|
||||
}
|
||||
|
||||
const workflowNodeMetaByType: Record<string, WorkflowNodeMeta> = {
|
||||
start: { icon: PlayCircleIcon, tone: "blue", label: "入口" },
|
||||
conversation_understanding: { icon: MessageCircleIcon, tone: "indigo", label: "理解" },
|
||||
reply_policy: { icon: ShieldCheckIcon, tone: "violet", label: "策略" },
|
||||
knowledge_retrieve: { icon: BookOpenIcon, tone: "emerald", label: "知识" },
|
||||
answerability_gate: { icon: HelpCircleIcon, tone: "cyan", label: "判断" },
|
||||
llm_reply: { icon: BotIcon, tone: "indigo", label: "生成" },
|
||||
condition: { icon: GitBranchIcon, tone: "cyan", label: "分支" },
|
||||
analyze_conversation: { icon: SearchIcon, tone: "blue", label: "分析" },
|
||||
prepare_ticket_draft: { icon: ClipboardListIcon, tone: "amber", label: "工单" },
|
||||
human_confirm: { icon: UserCheckIcon, tone: "amber", label: "确认" },
|
||||
create_ticket: { icon: TicketIcon, tone: "rose", label: "工单" },
|
||||
handoff_to_human: { icon: HeadphonesIcon, tone: "amber", label: "人工" },
|
||||
send_reply: { icon: SendIcon, tone: "emerald", label: "发送" },
|
||||
end: { icon: FlagIcon, tone: "slate", label: "结束" },
|
||||
}
|
||||
|
||||
export function getWorkflowNodeMeta(type: string): WorkflowNodeMeta {
|
||||
return workflowNodeMetaByType[type] ?? { icon: FileTextIcon, tone: "slate", label: "节点" }
|
||||
}
|
||||
|
||||
export function getWorkflowNodeAccentClass(tone: WorkflowNodeTone) {
|
||||
const classes: Record<WorkflowNodeTone, string> = {
|
||||
blue: "border-blue-200/80 bg-blue-50 text-blue-700 dark:border-blue-900/60 dark:bg-blue-950/50 dark:text-blue-300",
|
||||
cyan: "border-cyan-200/80 bg-cyan-50 text-cyan-700 dark:border-cyan-900/60 dark:bg-cyan-950/50 dark:text-cyan-300",
|
||||
emerald: "border-emerald-200/80 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/50 dark:text-emerald-300",
|
||||
indigo: "border-indigo-200/80 bg-indigo-50 text-indigo-700 dark:border-indigo-900/60 dark:bg-indigo-950/50 dark:text-indigo-300",
|
||||
amber: "border-amber-200/80 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/50 dark:text-amber-300",
|
||||
violet: "border-violet-200/80 bg-violet-50 text-violet-700 dark:border-violet-900/60 dark:bg-violet-950/50 dark:text-violet-300",
|
||||
rose: "border-rose-200/80 bg-rose-50 text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/50 dark:text-rose-300",
|
||||
slate: "border-slate-200/80 bg-slate-50 text-slate-700 dark:border-slate-800 dark:bg-slate-900/60 dark:text-slate-300",
|
||||
}
|
||||
return classes[tone]
|
||||
}
|
||||
|
||||
export function getWorkflowNodeIconClass(tone: WorkflowNodeTone) {
|
||||
const classes: Record<WorkflowNodeTone, string> = {
|
||||
blue: "bg-blue-500 text-white shadow-blue-500/20",
|
||||
cyan: "bg-cyan-500 text-white shadow-cyan-500/20",
|
||||
emerald: "bg-emerald-500 text-white shadow-emerald-500/20",
|
||||
indigo: "bg-indigo-500 text-white shadow-indigo-500/20",
|
||||
amber: "bg-amber-500 text-white shadow-amber-500/20",
|
||||
violet: "bg-violet-500 text-white shadow-violet-500/20",
|
||||
rose: "bg-rose-500 text-white shadow-rose-500/20",
|
||||
slate: "bg-slate-600 text-white shadow-slate-600/20",
|
||||
}
|
||||
return classes[tone]
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import type { AIWorkflowNodeSpec } from "@/lib/api/admin"
|
||||
import { WorkflowNodeIcon } from "./workflow-node-icon"
|
||||
|
||||
export function WorkflowNodePalette({
|
||||
nodeSpecs,
|
||||
@@ -22,10 +21,10 @@ export function WorkflowNodePalette({
|
||||
<button
|
||||
key={spec.type}
|
||||
type="button"
|
||||
className="flex w-full items-start gap-2 rounded-sm px-2 py-2 text-left text-sm hover:bg-muted"
|
||||
className="flex w-full items-start gap-2 rounded-md px-2 py-2 text-left text-sm transition-colors hover:bg-muted"
|
||||
onClick={() => onAddNode(spec)}
|
||||
>
|
||||
<PlusIcon className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
|
||||
<WorkflowNodeIcon type={spec.type} size="sm" className="mt-0.5" />
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate font-medium">{spec.title || spec.type}</span>
|
||||
<span className="line-clamp-2 text-xs text-muted-foreground">{spec.description}</span>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { AIWorkflowNodeSpec } from "@/lib/api/admin"
|
||||
|
||||
export function WorkflowSimpleNodeContent({
|
||||
spec,
|
||||
}: {
|
||||
spec?: AIWorkflowNodeSpec
|
||||
}) {
|
||||
const inputCount = spec?.inputSchema?.length ?? 0
|
||||
const outputCount = spec?.outputSchema?.length ?? 0
|
||||
const hasSchema = inputCount > 0 || outputCount > 0
|
||||
|
||||
if (!hasSchema && !spec?.riskLevel && !spec?.interruptible) {
|
||||
return (
|
||||
<div className="text-xs leading-5 text-muted-foreground">
|
||||
通过右侧面板配置节点参数
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{inputCount > 0 ? <NodeInfoChip label={`入参 ${inputCount}`} /> : null}
|
||||
{outputCount > 0 ? <NodeInfoChip label={`出参 ${outputCount}`} /> : null}
|
||||
{spec?.riskLevel ? <NodeInfoChip label={riskLevelLabel(spec.riskLevel)} /> : null}
|
||||
{spec?.interruptible ? <NodeInfoChip label="可中断" /> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NodeInfoChip({ label }: { label: string }) {
|
||||
return (
|
||||
<span className="inline-flex h-5 items-center rounded-md border bg-background px-1.5 text-[11px] leading-none text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function riskLevelLabel(riskLevel: AIWorkflowNodeSpec["riskLevel"]) {
|
||||
if (riskLevel === "high") return "高风险"
|
||||
if (riskLevel === "medium") return "中风险"
|
||||
return "低风险"
|
||||
}
|
||||
Reference in New Issue
Block a user