From 530cf145b2f2f534f1ed56b4c8277fcd03773784 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Sun, 28 Jun 2026 15:24:00 +0800 Subject: [PATCH] feat: add icon property to workflow node specifications and update related components --- docs | 2 +- internal/ai/workflow/registry/registry.go | 14 ++++++ internal/ai/workflow/registry/spec.go | 1 + internal/builders/ai_workflow_builder.go | 1 + internal/builders/ai_workflow_builder_test.go | 3 ++ .../pkg/dto/response/ai_workflow_response.go | 1 + .../_components/flowgram-node-registries.tsx | 13 ++++-- .../_components/workflow-node-card.tsx | 6 +-- .../_components/workflow-node-icon.tsx | 12 +++--- .../_components/workflow-node-meta.ts | 43 ------------------- .../_components/workflow-node-palette.tsx | 2 +- web/lib/api/admin.ts | 1 + 12 files changed, 42 insertions(+), 57 deletions(-) delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-node-meta.ts diff --git a/docs b/docs index 97dc51a..fe29cfb 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 97dc51ae521218f348fd797ff645db7c716c3d8f +Subproject commit fe29cfb3cc46382c6a2803f3cc9d7c3051ab5368 diff --git a/internal/ai/workflow/registry/registry.go b/internal/ai/workflow/registry/registry.go index f4ae791..1195a77 100644 --- a/internal/ai/workflow/registry/registry.go +++ b/internal/ai/workflow/registry/registry.go @@ -25,6 +25,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeStart, Title: "Start", Description: "Conversation workflow entry.", + Icon: "PlayCircleIcon", RiskLevel: NodeRiskLevelLow, OutputSchema: []VariableSpec{ output("conversationId", VariableTypeInteger, "Conversation ID."), @@ -38,6 +39,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeConversationUnderstanding, Title: "Conversation Understanding", Description: "Classify customer message intent and answer scope before retrieval.", + Icon: "MessageCircleIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ requiredInput("userMessage", VariableTypeString, "Current user message content."), @@ -75,6 +77,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeReplyPolicy, Title: "Reply Policy", Description: "Decide the next customer-service action from understanding output and agent policy.", + Icon: "ShieldCheckIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ requiredInput("messageIntent", VariableTypeString, "Detected customer message intent."), @@ -115,6 +118,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeKnowledgeRetrieve, Title: "Knowledge Retrieve", Description: "Retrieve knowledge for the current user message.", + Icon: "BookOpenIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ requiredInput("query", VariableTypeString, "Search query."), @@ -131,6 +135,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeAnswerabilityGate, Title: "Answerability Gate", Description: "Decide whether retrieved knowledge is enough to answer.", + Icon: "HelpCircleIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ requiredInput("userMessage", VariableTypeString, "Current user message content."), @@ -148,6 +153,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeLLMReply, Title: "LLM Reply", Description: "Generate a reply or structured analysis with the configured model.", + Icon: "BotIcon", RiskLevel: NodeRiskLevelMedium, InputSchema: []VariableSpec{ requiredInput("userMessage", VariableTypeString, "Current user message content."), @@ -161,6 +167,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeCondition, Title: "Condition", Description: "Route by controlled workflow variables.", + Icon: "GitBranchIcon", RiskLevel: NodeRiskLevelLow, OutputSchema: []VariableSpec{ output("matched", VariableTypeBoolean, "Whether the condition matched."), @@ -170,6 +177,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeAnalyzeConversation, Title: "Analyze Conversation", Description: "Analyze intent, risk, and recommended next action.", + Icon: "SearchIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ requiredInput("userMessage", VariableTypeString, "Current user message content."), @@ -185,6 +193,7 @@ func DefaultRegistry() *Registry { Type: NodeTypePrepareTicketDraft, Title: "Prepare Ticket Draft", Description: "Build a ticket draft from conversation context.", + Icon: "ClipboardListIcon", RiskLevel: NodeRiskLevelMedium, InputSchema: []VariableSpec{ requiredInput("issue", VariableTypeString, "Issue summary."), @@ -197,6 +206,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeHumanConfirm, Title: "Human Confirm", Description: "Interrupt and wait for explicit user confirmation.", + Icon: "UserCheckIcon", RiskLevel: NodeRiskLevelMedium, Interruptible: true, InputSchema: []VariableSpec{ @@ -211,6 +221,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeCreateTicket, Title: "Create Ticket", Description: "Create a ticket from a confirmed draft.", + Icon: "TicketIcon", RiskLevel: NodeRiskLevelHigh, RequiresConfirmationPredecessor: true, InputSchema: []VariableSpec{ @@ -228,6 +239,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeHandoffToHuman, Title: "Handoff To Human", Description: "Transfer the conversation to human support.", + Icon: "HeadphonesIcon", RiskLevel: NodeRiskLevelHigh, InputSchema: []VariableSpec{ requiredInput("reason", VariableTypeString, "Handoff reason."), @@ -252,6 +264,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeSendReply, Title: "Send Reply", Description: "Return or commit customer-visible reply text.", + Icon: "SendIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ requiredInput("replyText", VariableTypeString, "Customer-visible reply text."), @@ -265,6 +278,7 @@ func DefaultRegistry() *Registry { Type: NodeTypeEnd, Title: "End", Description: "End workflow execution.", + Icon: "FlagIcon", RiskLevel: NodeRiskLevelLow, OutputSchema: []VariableSpec{ output("status", VariableTypeString, "Workflow terminal status."), diff --git a/internal/ai/workflow/registry/spec.go b/internal/ai/workflow/registry/spec.go index c279704..0337696 100644 --- a/internal/ai/workflow/registry/spec.go +++ b/internal/ai/workflow/registry/spec.go @@ -44,6 +44,7 @@ type NodeSpec struct { Type string `json:"type"` Title string `json:"title"` Description string `json:"description"` + Icon string `json:"icon"` RiskLevel NodeRiskLevel `json:"riskLevel"` Interruptible bool `json:"interruptible"` RequiresConfirmationPredecessor bool `json:"requiresConfirmationPredecessor"` diff --git a/internal/builders/ai_workflow_builder.go b/internal/builders/ai_workflow_builder.go index 5488626..514ed22 100644 --- a/internal/builders/ai_workflow_builder.go +++ b/internal/builders/ai_workflow_builder.go @@ -76,6 +76,7 @@ func BuildAIWorkflowNodeSpecs(list []workflowregistry.NodeSpec) []response.AIWor Type: item.Type, Title: item.Title, Description: item.Description, + Icon: item.Icon, RiskLevel: item.RiskLevel, Interruptible: item.Interruptible, RequiresConfirmationPredecessor: item.RequiresConfirmationPredecessor, diff --git a/internal/builders/ai_workflow_builder_test.go b/internal/builders/ai_workflow_builder_test.go index 144a710..93389da 100644 --- a/internal/builders/ai_workflow_builder_test.go +++ b/internal/builders/ai_workflow_builder_test.go @@ -19,6 +19,9 @@ func TestBuildAIWorkflowNodeSpecsIncludesVariableContracts(t *testing.T) { switch spec.Type { case workflowregistry.NodeTypeStart: startFound = true + if spec.Icon != "PlayCircleIcon" { + t.Fatalf("expected start icon PlayCircleIcon, got %q", spec.Icon) + } if !hasResponseVariable(spec.OutputSchema, "userMessage") { t.Fatalf("expected start output userMessage, got %#v", spec.OutputSchema) } diff --git a/internal/pkg/dto/response/ai_workflow_response.go b/internal/pkg/dto/response/ai_workflow_response.go index d40b66a..bb19cae 100644 --- a/internal/pkg/dto/response/ai_workflow_response.go +++ b/internal/pkg/dto/response/ai_workflow_response.go @@ -45,6 +45,7 @@ type AIWorkflowNodeSpecResponse struct { Type string `json:"type"` Title string `json:"title"` Description string `json:"description"` + Icon string `json:"icon"` RiskLevel workflowregistry.NodeRiskLevel `json:"riskLevel"` Interruptible bool `json:"interruptible"` RequiresConfirmationPredecessor bool `json:"requiresConfirmationPredecessor"` diff --git a/web/app/dashboard/ai-workflows/_components/flowgram-node-registries.tsx b/web/app/dashboard/ai-workflows/_components/flowgram-node-registries.tsx index d214197..13c12fe 100644 --- a/web/app/dashboard/ai-workflows/_components/flowgram-node-registries.tsx +++ b/web/app/dashboard/ai-workflows/_components/flowgram-node-registries.tsx @@ -13,6 +13,7 @@ export function buildFlowgramNodeRegistries(nodeSpecs: AIWorkflowNodeSpec[]): Wo type: "start", title: "开始", description: "流程入口", + icon: "PlayCircleIcon", riskLevel: "low" as const, interruptible: false, requiresConfirmationPredecessor: false, @@ -21,6 +22,7 @@ export function buildFlowgramNodeRegistries(nodeSpecs: AIWorkflowNodeSpec[]): Wo type: "end", title: "结束", description: "流程结束", + icon: "FlagIcon", riskLevel: "low" as const, interruptible: false, requiresConfirmationPredecessor: false, @@ -47,9 +49,10 @@ export function buildFlowgramNodeRegistries(nodeSpecs: AIWorkflowNodeSpec[]): Wo formMeta: { render: () => ( + nodeType={spec.type} + fallbackTitle={spec.title || spec.type} + icon={spec.icon} + /> ), }, })) @@ -58,9 +61,11 @@ export function buildFlowgramNodeRegistries(nodeSpecs: AIWorkflowNodeSpec[]): Wo function FlowgramNodeForm({ nodeType, fallbackTitle, + icon, }: { nodeType: string fallbackTitle: string + icon: string }) { const { node, selected } = useNodeRender() const nodeId = String(node.id ?? "") @@ -69,8 +74,8 @@ function FlowgramNodeForm({ name="title"> {({ field }) => ( diff --git a/web/app/dashboard/ai-workflows/_components/workflow-node-card.tsx b/web/app/dashboard/ai-workflows/_components/workflow-node-card.tsx index ed78b54..d26b372 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-node-card.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-node-card.tsx @@ -4,14 +4,14 @@ import { cn } from "@/lib/utils" import { WorkflowNodeIcon } from "./workflow-node-icon" export function WorkflowNodeCard({ - nodeType, title, + icon, selected, width = "normal", children, }: { - nodeType: string title: string + icon: string selected: boolean width?: "normal" | "wide" children?: ReactNode @@ -28,7 +28,7 @@ export function WorkflowNodeCard({ >
- +
{title} diff --git a/web/app/dashboard/ai-workflows/_components/workflow-node-icon.tsx b/web/app/dashboard/ai-workflows/_components/workflow-node-icon.tsx index 2960962..0513dcc 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-node-icon.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-node-icon.tsx @@ -1,17 +1,19 @@ import { cn } from "@/lib/utils" -import { getWorkflowNodeMeta } from "./workflow-node-meta" +import { FileTextIcon, type LucideIcon } from "lucide-react" +import * as LucideIcons from "lucide-react" + +const lucideIconComponents = LucideIcons as unknown as Record export function WorkflowNodeIcon({ - type, + icon, size = "md", className, }: { - type: string + icon?: string size?: "sm" | "md" className?: string }) { - const meta = getWorkflowNodeMeta(type) - const Icon = meta.icon + const Icon = icon ? lucideIconComponents[icon] ?? FileTextIcon : FileTextIcon return ( = { - start: { icon: PlayCircleIcon }, - conversation_understanding: { icon: MessageCircleIcon }, - reply_policy: { icon: ShieldCheckIcon }, - knowledge_retrieve: { icon: BookOpenIcon }, - answerability_gate: { icon: HelpCircleIcon }, - llm_reply: { icon: BotIcon }, - condition: { icon: GitBranchIcon }, - analyze_conversation: { icon: SearchIcon }, - prepare_ticket_draft: { icon: ClipboardListIcon }, - human_confirm: { icon: UserCheckIcon }, - create_ticket: { icon: TicketIcon }, - handoff_to_human: { icon: HeadphonesIcon }, - send_reply: { icon: SendIcon }, - end: { icon: FlagIcon }, -} - -export function getWorkflowNodeMeta(type: string): WorkflowNodeMeta { - return workflowNodeMetaByType[type] ?? { icon: FileTextIcon } -} diff --git a/web/app/dashboard/ai-workflows/_components/workflow-node-palette.tsx b/web/app/dashboard/ai-workflows/_components/workflow-node-palette.tsx index 575d0e9..2053bb3 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-node-palette.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-node-palette.tsx @@ -24,7 +24,7 @@ export function WorkflowNodePalette({ 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)} > - + {spec.title || spec.type} {spec.description} diff --git a/web/lib/api/admin.ts b/web/lib/api/admin.ts index 0a70589..658c774 100644 --- a/web/lib/api/admin.ts +++ b/web/lib/api/admin.ts @@ -380,6 +380,7 @@ export type AIWorkflowNodeSpec = { type: string title: string description: string + icon: string riskLevel: "low" | "medium" | "high" interruptible: boolean requiresConfirmationPredecessor: boolean