diff --git a/internal/ai/workflow/registry/registry.go b/internal/ai/workflow/registry/registry.go index 1195a77..b3f1bab 100644 --- a/internal/ai/workflow/registry/registry.go +++ b/internal/ai/workflow/registry/registry.go @@ -28,11 +28,11 @@ func DefaultRegistry() *Registry { Icon: "PlayCircleIcon", RiskLevel: NodeRiskLevelLow, OutputSchema: []VariableSpec{ - output("conversationId", VariableTypeInteger, "Conversation ID."), - output("messageId", VariableTypeInteger, "Current user message ID."), - output("aiAgentId", VariableTypeInteger, "AI Agent ID."), - output("userMessage", VariableTypeString, "Current user message content."), - output("knowledgeBaseIds", VariableTypeIntegerArray, "Knowledge bases bound to the AI Agent."), + output("conversationId", "会话 ID", VariableTypeInteger, "当前客户会话的内部编号。"), + output("messageId", "消息 ID", VariableTypeInteger, "客户本轮消息的内部编号。"), + output("aiAgentId", "AI Agent ID", VariableTypeInteger, "当前处理会话的 AI Agent 编号。"), + output("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"), + output("knowledgeBaseIds", "知识库 ID 列表", VariableTypeIntegerArray, "当前 AI Agent 已绑定的知识库编号列表。"), }, }, NodeSpec{ @@ -42,11 +42,11 @@ func DefaultRegistry() *Registry { Icon: "MessageCircleIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ - requiredInput("userMessage", VariableTypeString, "Current user message content."), + requiredInput("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"), }, OutputSchema: []VariableSpec{ - output("normalizedMessage", VariableTypeString, "Normalized customer message."), - enumOutput("messageIntent", "消息意图", "Detected customer message intent.", []VariableValueOption{ + output("normalizedMessage", "规范化消息", VariableTypeString, "经过清洗和规范化后的客户消息。"), + enumOutput("messageIntent", "消息意图", "客户消息的意图分类。", []VariableValueOption{ valueOption("unknown", "未知意图", "系统暂时无法判断客户意图。"), valueOption("greeting", "打招呼", "客户在问候或开始对话。"), valueOption("thanks", "表达感谢", "客户在表示感谢。"), @@ -58,16 +58,16 @@ func DefaultRegistry() *Registry { valueOption("ambiguous_question", "问题不明确", "客户问题缺少必要上下文,需要追问。"), valueOption("business_question", "业务问题", "客户问题适合进入知识库检索。"), }), - enumOutput("answerScope", "回复策略", "Recommended answer scope.", []VariableValueOption{ + enumOutput("answerScope", "回复策略", "系统建议采用的回复处理范围。", []VariableValueOption{ valueOption("direct_reply", "直接回复客户", "无需检索知识库或转人工,可以直接生成回复。"), valueOption("needs_clarification", "追问补充信息", "当前信息不足,需要客户补充。"), valueOption("needs_handoff", "转人工处理", "需要人工客服介入。"), valueOption("needs_ticket", "创建工单", "需要进入工单处理流程。"), valueOption("needs_knowledge", "检索知识库", "需要先检索知识库再回答。"), }), - output("confidence", VariableTypeNumber, "Classifier confidence."), - output("riskSignals", VariableTypeStringArray, "Detected risk signals."), - output("reason", VariableTypeString, "Decision reason."), + output("confidence", "置信度", VariableTypeNumber, "意图和回复策略判断的置信度。"), + output("riskSignals", "风险信号", VariableTypeStringArray, "识别到的投诉、升级、人工介入等风险线索。"), + output("reason", "判断原因", VariableTypeString, "本次意图和回复策略判断的原因说明。"), }, DefaultInputs: map[string]dsl.Value{ "userMessage": dsl.RefValue("start_1", "userMessage"), @@ -80,14 +80,14 @@ func DefaultRegistry() *Registry { Icon: "ShieldCheckIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ - requiredInput("messageIntent", VariableTypeString, "Detected customer message intent."), - requiredInput("answerScope", VariableTypeString, "Recommended answer scope."), - optionalInput("userMessage", VariableTypeString, "Current user message content."), - optionalInput("riskSignals", VariableTypeStringArray, "Detected risk signals."), - optionalInput("answerability", VariableTypeString, "Knowledge answerability decision."), + requiredInput("messageIntent", "消息意图", VariableTypeString, "上游理解节点识别出的客户消息意图。"), + requiredInput("answerScope", "回复策略", VariableTypeString, "上游理解节点建议采用的回复处理范围。"), + optionalInput("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"), + optionalInput("riskSignals", "风险信号", VariableTypeStringArray, "上游识别到的风险线索列表。"), + optionalInput("answerability", "可回答性", VariableTypeString, "知识库结果是否足够支撑回答的判断。"), }, OutputSchema: []VariableSpec{ - enumOutput("action", "处理策略", "Selected policy action.", []VariableValueOption{ + enumOutput("action", "处理策略", "回复策略节点选择的下一步处理动作。", []VariableValueOption{ valueOption("direct_reply", "直接回复客户", "直接发送策略节点生成的回复。"), valueOption("clarify", "追问补充信息", "先让客户补充必要信息。"), valueOption("end_conversation", "结束会话", "发送结束语并结束本轮处理。"), @@ -96,15 +96,15 @@ func DefaultRegistry() *Registry { valueOption("retrieve_knowledge", "检索知识库", "进入知识检索和 AI 回复流程。"), valueOption("knowledge_fallback", "知识库兜底", "知识库结果不足,发送兜底回复。"), }), - output("replyText", VariableTypeString, "Customer-visible reply text when the policy can answer directly."), - output("reason", VariableTypeString, "Policy decision reason."), - output("requiresFlow", VariableTypeBoolean, "Whether the decision should continue into workflow actions."), - enumOutput("targetFlow", "目标流程", "Suggested target flow.", []VariableValueOption{ + output("replyText", "回复内容", VariableTypeString, "可直接发送给客户的回复文本。"), + output("reason", "策略原因", VariableTypeString, "选择当前处理策略的原因说明。"), + output("requiresFlow", "需要继续流程", VariableTypeBoolean, "是否需要继续执行后续工作流节点。"), + enumOutput("targetFlow", "目标流程", "建议继续执行的业务流程。", []VariableValueOption{ valueOption("handoff_to_human", "转人工流程", "继续执行转人工节点。"), valueOption("prepare_ticket", "工单流程", "继续执行工单草稿和确认节点。"), valueOption("knowledge", "知识库流程", "继续执行知识检索节点。"), }), - enumOutput("finalReplySource", "回复来源", "Source category for the final reply.", []VariableValueOption{ + enumOutput("finalReplySource", "回复来源", "最终回复内容的来源类别。", []VariableValueOption{ valueOption("direct_reply", "策略直接回复", "由回复策略节点直接生成回复。"), valueOption("clarification", "追问回复", "用于追问客户补充信息。"), valueOption("handoff_notice", "转人工提示", "用于提示客户已进入人工处理。"), @@ -121,11 +121,11 @@ func DefaultRegistry() *Registry { Icon: "BookOpenIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ - requiredInput("query", VariableTypeString, "Search query."), + requiredInput("query", "检索问题", VariableTypeString, "用于检索知识库的客户问题或查询文本。"), }, OutputSchema: []VariableSpec{ - output("items", VariableTypeObjectArray, "Retrieved knowledge items."), - output("summary", VariableTypeString, "Short retrieval summary."), + output("items", "知识条目", VariableTypeObjectArray, "从知识库命中的原始知识条目列表。"), + output("summary", "检索摘要", VariableTypeString, "对本次知识检索结果的简短摘要。"), }, DefaultInputs: map[string]dsl.Value{ "query": dsl.RefValue("start_1", "userMessage"), @@ -138,15 +138,15 @@ func DefaultRegistry() *Registry { Icon: "HelpCircleIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ - requiredInput("userMessage", VariableTypeString, "Current user message content."), - requiredInput("knowledgeItems", VariableTypeObjectArray, "Retrieved knowledge items."), + requiredInput("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"), + requiredInput("knowledgeItems", "知识条目", VariableTypeObjectArray, "上游知识检索节点命中的知识条目列表。"), }, OutputSchema: []VariableSpec{ - enumOutput("answerability", "可回答性", "Answerability decision.", []VariableValueOption{ + enumOutput("answerability", "可回答性", "知识库结果是否足够支撑回答的判断。", []VariableValueOption{ valueOption("answerable", "可以回答", "检索结果足够支撑回答。"), valueOption("unanswerable", "无法回答", "检索结果不足,应该走兜底或追问。"), }), - output("reason", VariableTypeString, "Decision reason."), + output("reason", "判断原因", VariableTypeString, "可回答性判断的原因说明。"), }, }, NodeSpec{ @@ -156,11 +156,11 @@ func DefaultRegistry() *Registry { Icon: "BotIcon", RiskLevel: NodeRiskLevelMedium, InputSchema: []VariableSpec{ - requiredInput("userMessage", VariableTypeString, "Current user message content."), - optionalInput("knowledgeItems", VariableTypeObjectArray, "Retrieved knowledge items."), + requiredInput("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"), + optionalInput("knowledgeItems", "知识条目", VariableTypeObjectArray, "可用于生成回复的知识库检索结果。"), }, OutputSchema: []VariableSpec{ - output("replyText", VariableTypeString, "Generated reply text."), + output("replyText", "回复内容", VariableTypeString, "大模型生成的客户可见回复文本。"), }, }, NodeSpec{ @@ -170,7 +170,7 @@ func DefaultRegistry() *Registry { Icon: "GitBranchIcon", RiskLevel: NodeRiskLevelLow, OutputSchema: []VariableSpec{ - output("matched", VariableTypeBoolean, "Whether the condition matched."), + output("matched", "是否命中", VariableTypeBoolean, "条件节点是否命中了某个条件分支。"), }, }, NodeSpec{ @@ -180,13 +180,13 @@ func DefaultRegistry() *Registry { Icon: "SearchIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ - requiredInput("userMessage", VariableTypeString, "Current user message content."), + requiredInput("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"), }, OutputSchema: []VariableSpec{ - output("intent", VariableTypeString, "Detected user intent."), - output("riskLevel", VariableTypeString, "Detected risk level."), - output("needTicket", VariableTypeBoolean, "Whether a ticket is recommended."), - output("needHumanHandoff", VariableTypeBoolean, "Whether human handoff is recommended."), + output("intent", "用户意图", VariableTypeString, "从会话中识别出的客户意图。"), + output("riskLevel", "风险等级", VariableTypeString, "本轮会话的风险等级判断。"), + output("needTicket", "需要工单", VariableTypeBoolean, "是否建议进入工单处理流程。"), + output("needHumanHandoff", "需要转人工", VariableTypeBoolean, "是否建议转人工客服处理。"), }, }, NodeSpec{ @@ -196,10 +196,10 @@ func DefaultRegistry() *Registry { Icon: "ClipboardListIcon", RiskLevel: NodeRiskLevelMedium, InputSchema: []VariableSpec{ - requiredInput("issue", VariableTypeString, "Issue summary."), + requiredInput("issue", "问题摘要", VariableTypeString, "需要整理进工单的客户问题摘要。"), }, OutputSchema: []VariableSpec{ - output("ticketDraft", VariableTypeObject, "Draft ticket payload."), + output("ticketDraft", "工单草稿", VariableTypeObject, "根据会话内容整理出的待确认工单草稿。"), }, }, NodeSpec{ @@ -210,11 +210,11 @@ func DefaultRegistry() *Registry { RiskLevel: NodeRiskLevelMedium, Interruptible: true, InputSchema: []VariableSpec{ - requiredInput("prompt", VariableTypeString, "Confirmation prompt."), + requiredInput("prompt", "确认提示", VariableTypeString, "发送给客户用于确认操作的提示文本。"), }, OutputSchema: []VariableSpec{ - output("confirmed", VariableTypeBoolean, "Whether the user confirmed."), - output("responseText", VariableTypeString, "Confirmation response text."), + output("confirmed", "已确认", VariableTypeBoolean, "客户是否明确确认继续执行。"), + output("responseText", "确认回复", VariableTypeString, "客户针对确认提示给出的回复文本。"), }, }, NodeSpec{ @@ -225,14 +225,14 @@ func DefaultRegistry() *Registry { RiskLevel: NodeRiskLevelHigh, RequiresConfirmationPredecessor: true, InputSchema: []VariableSpec{ - requiredInput("ticketDraft", VariableTypeObject, "Confirmed draft ticket payload."), - requiredInput("confirmed", VariableTypeBoolean, "Confirmation result."), + requiredInput("ticketDraft", "工单草稿", VariableTypeObject, "已经由客户确认的工单草稿内容。"), + requiredInput("confirmed", "已确认", VariableTypeBoolean, "客户是否已确认创建工单。"), }, OutputSchema: []VariableSpec{ - output("ticketId", VariableTypeInteger, "Created ticket ID."), - output("ticketNo", VariableTypeString, "Created ticket number."), - output("created", VariableTypeBoolean, "Whether the ticket was created."), - output("message", VariableTypeString, "Customer-visible ticket creation result."), + output("ticketId", "工单 ID", VariableTypeInteger, "创建成功后的工单内部编号。"), + output("ticketNo", "工单编号", VariableTypeString, "创建成功后的客户可见工单编号。"), + output("created", "已创建", VariableTypeBoolean, "工单是否已经成功创建。"), + output("message", "结果消息", VariableTypeString, "发送给客户的工单创建结果说明。"), }, }, NodeSpec{ @@ -242,22 +242,22 @@ func DefaultRegistry() *Registry { Icon: "HeadphonesIcon", RiskLevel: NodeRiskLevelHigh, InputSchema: []VariableSpec{ - requiredInput("reason", VariableTypeString, "Handoff reason."), - optionalInput("confirmed", VariableTypeBoolean, "Confirmation result."), + requiredInput("reason", "转人工原因", VariableTypeString, "触发转人工处理的业务原因。"), + optionalInput("confirmed", "已确认", VariableTypeBoolean, "客户是否已确认转人工。"), }, OutputSchema: []VariableSpec{ - output("handoffId", VariableTypeInteger, "Handoff operation ID."), - output("reason", VariableTypeString, "Handoff reason."), - enumOutput("decision", "转人工结果", "Handoff dispatch decision.", []VariableValueOption{ + output("handoffId", "转人工记录 ID", VariableTypeInteger, "本次转人工操作的内部记录编号。"), + output("reason", "转人工原因", VariableTypeString, "本次转人工处理的原因说明。"), + enumOutput("decision", "转人工结果", "转人工分配或排队结果。", []VariableValueOption{ valueOption("assigned", "已分配客服", "已成功分配给人工客服。"), valueOption("team_pool", "团队队列等待", "暂未分配到客服,进入团队等待队列。"), valueOption("global_pool", "全局队列等待", "非服务时间或无可用团队,进入全局等待队列。"), valueOption("off_hours", "非服务时间", "当前不在人工客服服务时间内。"), valueOption("cancelled", "已取消转人工", "由于未确认或条件不满足,未执行转人工。"), }), - output("teamId", VariableTypeInteger, "Assigned or pending team ID."), - output("assigneeId", VariableTypeInteger, "Assigned agent user ID."), - output("message", VariableTypeString, "Customer-visible handoff notice."), + output("teamId", "客服组 ID", VariableTypeInteger, "已分配或等待中的客服组编号。"), + output("assigneeId", "客服 ID", VariableTypeInteger, "已分配的人工客服用户编号。"), + output("message", "转人工提示", VariableTypeString, "发送给客户的转人工结果提示。"), }, }, NodeSpec{ @@ -267,11 +267,11 @@ func DefaultRegistry() *Registry { Icon: "SendIcon", RiskLevel: NodeRiskLevelLow, InputSchema: []VariableSpec{ - requiredInput("replyText", VariableTypeString, "Customer-visible reply text."), + requiredInput("replyText", "回复内容", VariableTypeString, "将发送或返回给客户的最终回复文本。"), }, OutputSchema: []VariableSpec{ - output("sent", VariableTypeBoolean, "Whether the reply was sent."), - output("replyMessageId", VariableTypeInteger, "Reply message ID."), + output("sent", "已发送", VariableTypeBoolean, "回复是否已经成功发送或返回。"), + output("replyMessageId", "回复消息 ID", VariableTypeInteger, "发送成功后的回复消息编号。"), }, }, NodeSpec{ @@ -281,22 +281,22 @@ func DefaultRegistry() *Registry { Icon: "FlagIcon", RiskLevel: NodeRiskLevelLow, OutputSchema: []VariableSpec{ - output("status", VariableTypeString, "Workflow terminal status."), + output("status", "结束状态", VariableTypeString, "工作流执行结束时的状态。"), }, }, ) } -func requiredInput(name string, variableType VariableType, description string) VariableSpec { - return VariableSpec{Name: name, Type: variableType, Required: true, Description: description} +func requiredInput(name string, label string, variableType VariableType, description string) VariableSpec { + return VariableSpec{Name: name, Label: label, Type: variableType, Required: true, Description: description} } -func optionalInput(name string, variableType VariableType, description string) VariableSpec { - return VariableSpec{Name: name, Type: variableType, Description: description} +func optionalInput(name string, label string, variableType VariableType, description string) VariableSpec { + return VariableSpec{Name: name, Label: label, Type: variableType, Description: description} } -func output(name string, variableType VariableType, description string) VariableSpec { - return VariableSpec{Name: name, Type: variableType, Description: description} +func output(name string, label string, variableType VariableType, description string) VariableSpec { + return VariableSpec{Name: name, Label: label, Type: variableType, Description: description} } func enumOutput(name string, label string, description string, options []VariableValueOption) VariableSpec { diff --git a/internal/ai/workflow/registry/registry_test.go b/internal/ai/workflow/registry/registry_test.go index 980ba1a..3ef41ac 100644 --- a/internal/ai/workflow/registry/registry_test.go +++ b/internal/ai/workflow/registry/registry_test.go @@ -2,6 +2,19 @@ package registry import "testing" +func TestDefaultRegistryVariablesHaveBusinessLabels(t *testing.T) { + for _, spec := range DefaultRegistry().List() { + for _, variable := range append(spec.InputSchema, spec.OutputSchema...) { + if variable.Label == "" { + t.Fatalf("%s.%s is missing business label", spec.Type, variable.Name) + } + if variable.Description == "" || variable.Description == variable.Name { + t.Fatalf("%s.%s is missing readable description", spec.Type, variable.Name) + } + } + } +} + func TestDefaultRegistryExposesStartOutputs(t *testing.T) { spec, ok := DefaultRegistry().Get(NodeTypeStart) if !ok { diff --git a/web/app/dashboard/ai-workflows/_components/node-config-panel.tsx b/web/app/dashboard/ai-workflows/_components/node-config-panel.tsx index ed932ed..ff488a0 100644 --- a/web/app/dashboard/ai-workflows/_components/node-config-panel.tsx +++ b/web/app/dashboard/ai-workflows/_components/node-config-panel.tsx @@ -15,6 +15,8 @@ import { createConditionBranchID, isRefValue, normalizeNodeConfig, + refField, + refNodeId, type WorkflowConditionBranch, type WorkflowVariableRef, } from "./workflow-utils" @@ -383,6 +385,11 @@ function ConditionFields({ compact?: boolean }) { const condition = branch.condition ?? {} + const selectedVariable = isRefValue(condition.left) + ? variables.find((item) => item.nodeId === refNodeId(condition.left) && item.field === refField(condition.left)) + : undefined + const valueOptions = selectedVariable?.valueOptions ?? [] + const rightDisabled = ["exists", "empty"].includes(condition.operator ?? "") return (
@@ -414,17 +421,37 @@ function ConditionFields({ })} />
-
+
{compact ? null : } - onChange({ - ...branch, - condition: { ...condition, right: event.target.value }, - })} - /> + {valueOptions.length > 0 && !rightDisabled ? ( + ({ + value: stringifyConditionRight(option.value), + label: option.label || stringifyConditionRight(option.value), + description: option.description, + }))} + placeholder="选择取值" + triggerClassName={cardComboboxClassName} + onChange={(nextValue) => { + const selectedOption = valueOptions.find((option) => stringifyConditionRight(option.value) === nextValue) + onChange({ + ...branch, + condition: { ...condition, right: selectedOption?.value ?? nextValue }, + }) + }} + /> + ) : ( + onChange({ + ...branch, + condition: { ...condition, right: event.target.value }, + })} + /> + )}
diff --git a/web/app/dashboard/ai-workflows/_components/variable-selector.tsx b/web/app/dashboard/ai-workflows/_components/variable-selector.tsx index 02acc35..018f6e2 100644 --- a/web/app/dashboard/ai-workflows/_components/variable-selector.tsx +++ b/web/app/dashboard/ai-workflows/_components/variable-selector.tsx @@ -3,6 +3,7 @@ import { OptionCombobox } from "@/components/option-combobox" import { + buildVariableOption, createRefValue, refField, refNodeId, @@ -24,11 +25,7 @@ export function VariableSelector({ triggerClassName?: string }) { const selected = value ? `${refNodeId(value)}.${refField(value)}` : "" - const options = variables.map((variable) => ({ - value: `${variable.nodeId}.${variable.field}`, - label: `${variable.nodeName}.${variable.label || variable.field}`, - description: variable.description, - })) + const options = variables.map(buildVariableOption) return ( {panelDescription ? ( -
+
{panelDescription}
) : null} diff --git a/web/app/dashboard/ai-workflows/_components/workflow-utils.test.mjs b/web/app/dashboard/ai-workflows/_components/workflow-utils.test.mjs index c46b4f4..68067ef 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-utils.test.mjs +++ b/web/app/dashboard/ai-workflows/_components/workflow-utils.test.mjs @@ -226,6 +226,26 @@ describe("getAvailableVariables", () => { }) }) +describe("workflow variable display helpers", () => { + it("builds business-first variable options with technical details", async () => { + const { buildVariableOption } = await loadModule() + + assert.deepEqual(plain(buildVariableOption({ + nodeId: "start_1", + nodeName: "开始", + field: "userMessage", + label: "用户消息", + type: "string", + description: "客户本轮发送的消息内容", + })), { + value: "start_1.userMessage", + label: "开始 / 用户消息", + subtitle: "start_1.userMessage · string", + description: "客户本轮发送的消息内容", + }) + }) +}) + describe("workflow definition mutations", () => { it("updates node data without changing unrelated nodes", async () => { const { updateWorkflowNodeData } = await loadModule() diff --git a/web/app/dashboard/ai-workflows/_components/workflow-utils.ts b/web/app/dashboard/ai-workflows/_components/workflow-utils.ts index 7d0ffb3..d61547a 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-utils.ts +++ b/web/app/dashboard/ai-workflows/_components/workflow-utils.ts @@ -65,6 +65,13 @@ export type WorkflowVariableRef = { valueOptions?: WorkflowVariableValueOption[] } +export type WorkflowVariableOption = { + value: string + label: string + subtitle: string + description: string +} + export type WorkflowNodeSpec = AIWorkflowNodeSpec export type WorkflowDraftValidation = { @@ -92,6 +99,17 @@ export function refField(value: WorkflowValue | undefined): string { return isRefValue(value) ? value.content[1] : "" } +export function buildVariableOption(variable: WorkflowVariableRef): WorkflowVariableOption { + const ref = `${variable.nodeId}.${variable.field}` + const label = `${variable.nodeName} / ${variable.label || variable.field}` + return { + value: ref, + label, + subtitle: [ref, variable.type].filter(Boolean).join(" · "), + description: variable.description, + } +} + export function getNodeTitle( node: AIWorkflowDefinition["nodes"][number] | undefined, specs: AIWorkflowNodeSpec[] = [] diff --git a/web/components/option-combobox.tsx b/web/components/option-combobox.tsx index d5a019b..87c9030 100644 --- a/web/components/option-combobox.tsx +++ b/web/components/option-combobox.tsx @@ -23,6 +23,7 @@ import { useI18n } from "@/i18n/provider" export type ComboboxOption = { value: string label: string + subtitle?: string description?: string } @@ -78,7 +79,7 @@ export function OptionCombobox({ {options.map((option) => ( { onChange(option.value) setOpen(false) @@ -94,6 +95,11 @@ export function OptionCombobox({ /> {option.label} + {option.subtitle ? ( + + {option.subtitle} + + ) : null} {option.description ? ( {option.description}