feat: add icon property to workflow node specifications and update related components

This commit is contained in:
mlogclub
2026-06-28 15:24:00 +08:00
parent 389291143a
commit 530cf145b2
12 changed files with 42 additions and 57 deletions
+1 -1
Submodule docs updated: 97dc51ae52...fe29cfb3cc
+14
View File
@@ -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."),
+1
View File
@@ -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"`
+1
View File
@@ -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,
@@ -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)
}
@@ -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"`
@@ -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: () => (
<FlowgramNodeForm
nodeType={spec.type}
fallbackTitle={spec.title || spec.type}
/>
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({
<Field<string> name="title">
{({ field }) => (
<WorkflowNodeCard
nodeType={nodeType}
title={field.value || fallbackTitle}
icon={icon}
selected={selected}
width={nodeType === "condition" ? "wide" : "normal"}
>
@@ -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({
>
<div className="overflow-hidden rounded-lg border border-transparent bg-background">
<div className="flex items-center gap-2 px-3 pb-2 pt-3">
<WorkflowNodeIcon type={nodeType} />
<WorkflowNodeIcon icon={icon} />
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold leading-5 text-foreground">
{title}
@@ -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<string, LucideIcon>
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 (
<span
@@ -1,43 +0,0 @@
import {
BookOpenIcon,
BotIcon,
ClipboardListIcon,
FileTextIcon,
FlagIcon,
GitBranchIcon,
HeadphonesIcon,
HelpCircleIcon,
MessageCircleIcon,
PlayCircleIcon,
SearchIcon,
SendIcon,
ShieldCheckIcon,
TicketIcon,
UserCheckIcon,
type LucideIcon,
} from "lucide-react"
export type WorkflowNodeMeta = {
icon: LucideIcon
}
const workflowNodeMetaByType: Record<string, WorkflowNodeMeta> = {
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 }
}
@@ -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)}
>
<WorkflowNodeIcon type={spec.type} size="sm" className="mt-0.5" />
<WorkflowNodeIcon icon={spec.icon} 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>
+1
View File
@@ -380,6 +380,7 @@ export type AIWorkflowNodeSpec = {
type: string
title: string
description: string
icon: string
riskLevel: "low" | "medium" | "high"
interruptible: boolean
requiresConfirmationPredecessor: boolean