From db7964a4610b93c2f5f3cf7cc7af4efb3c7d5241 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 7 Feb 2026 01:20:12 -0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20P1=20=E2=80=94=20=E8=AF=84?= =?UTF-8?q?=E4=BC=B0=E7=BB=93=E6=9E=9C=E5=8F=AF=E8=A7=86=E5=8C=96=E5=8D=A1?= =?UTF-8?q?=E7=89=87=EF=BC=8CAssessment=20Expert=20=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E4=B8=BA=E7=BB=93=E6=9E=84=E5=8C=96=E6=8A=A5?= =?UTF-8?q?=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 新建 AssessmentResultCard 组件 - 渲染 Assessment Expert 返回的结构化评估数据 - 综合适合度分数(顶部大字展示,颜色编码) - 6 个移民类别评估卡片(QMAS/GEP/IANG/TTPS/CIES/TECHTAS) - 分数条形图(CSS 实现,无需 chart 库) - 颜色梯度:绿色(90+) → 蓝色(70+) → 黄色(50+) → 橙色(30+) → 红色 - 推荐类别高亮(primary 边框 + Award 图标) - 优势(highlights)、风险(concerns)、缺失信息(missingInfo) 分组展示 - 子类别标签(如 A类、综合计分制) - 排序:推荐类别优先,其次按分数降序 - 底部建议区块 ## ToolCallResult 集成 - 识别 invoke_assessment_expert 工具结果 - 自动 JSON.parse(assessment expert 返回 JSON 字符串) - 存在 assessments 数组时渲染 AssessmentResultCard Co-Authored-By: Claude Opus 4.6 --- .../components/AssessmentResultCard.tsx | 178 ++++++++++++++++++ .../presentation/components/MessageBubble.tsx | 14 ++ 2 files changed, 192 insertions(+) create mode 100644 packages/web-client/src/features/chat/presentation/components/AssessmentResultCard.tsx diff --git a/packages/web-client/src/features/chat/presentation/components/AssessmentResultCard.tsx b/packages/web-client/src/features/chat/presentation/components/AssessmentResultCard.tsx new file mode 100644 index 0000000..d9f98cd --- /dev/null +++ b/packages/web-client/src/features/chat/presentation/components/AssessmentResultCard.tsx @@ -0,0 +1,178 @@ +import { clsx } from 'clsx'; +import { CheckCircle, XCircle, AlertTriangle, Info, Award, TrendingUp } from 'lucide-react'; + +interface CategoryAssessment { + category: string; + categoryName: string; + eligible: boolean; + score: number; + confidence: number; + highlights: string[]; + concerns: string[]; + missingInfo?: string[]; + subClass?: string; +} + +interface AssessmentData { + assessments: CategoryAssessment[]; + overallRecommendation: string; + topRecommended: string[]; + suitabilityScore: number; + summary: string; +} + +function ScoreBar({ score, label }: { score: number; label?: string }) { + const getColor = (s: number) => { + if (s >= 90) return 'bg-green-500'; + if (s >= 70) return 'bg-blue-500'; + if (s >= 50) return 'bg-yellow-500'; + if (s >= 30) return 'bg-orange-500'; + return 'bg-red-500'; + }; + + const getLabel = (s: number) => { + if (s >= 90) return '高度适合'; + if (s >= 70) return '比较适合'; + if (s >= 50) return '有条件适合'; + if (s >= 30) return '适合度低'; + return '不适合'; + }; + + return ( +
+
+
+
+ + {label || getLabel(score)} {score} + +
+ ); +} + +function CategoryCard({ assessment, isRecommended }: { assessment: CategoryAssessment; isRecommended: boolean }) { + return ( +
+
+
+ {isRecommended && } + {assessment.categoryName} + {assessment.category} + {assessment.subClass && ( + {assessment.subClass} + )} +
+
+ {assessment.eligible ? ( + + ) : ( + + )} + + {assessment.eligible ? '符合条件' : '不符合'} + +
+
+ + + + {assessment.highlights.length > 0 && ( +
+ {assessment.highlights.map((h, i) => ( +
+ + {h} +
+ ))} +
+ )} + + {assessment.concerns.length > 0 && ( +
+ {assessment.concerns.map((c, i) => ( +
+ + {c} +
+ ))} +
+ )} + + {assessment.missingInfo && assessment.missingInfo.length > 0 && ( +
+ {assessment.missingInfo.map((m, i) => ( +
+ + {m} +
+ ))} +
+ )} +
+ ); +} + +export function AssessmentResultCard({ data }: { data: AssessmentData }) { + const topSet = new Set(data.topRecommended); + + // Sort: recommended first, then by score + const sorted = [...data.assessments].sort((a, b) => { + const aRec = topSet.has(a.category) ? 1 : 0; + const bRec = topSet.has(b.category) ? 1 : 0; + if (aRec !== bRec) return bRec - aRec; + return b.score - a.score; + }); + + return ( +
+ {/* Header with overall score */} +
+
+ + 移民资格评估报告 +
+
+ 综合适合度 + = 70 ? 'text-green-600' : + data.suitabilityScore >= 50 ? 'text-yellow-600' : 'text-red-500', + )}> + {data.suitabilityScore} + +
+
+ + {/* Summary */} + {data.summary && ( +

{data.summary}

+ )} + + {/* Category assessments */} +
+ {sorted.map((assessment) => ( + + ))} +
+ + {/* Overall recommendation */} + {data.overallRecommendation && ( +
+

+ 建议:{data.overallRecommendation} +

+
+ )} +
+ ); +} diff --git a/packages/web-client/src/features/chat/presentation/components/MessageBubble.tsx b/packages/web-client/src/features/chat/presentation/components/MessageBubble.tsx index 2e9f24c..458685e 100644 --- a/packages/web-client/src/features/chat/presentation/components/MessageBubble.tsx +++ b/packages/web-client/src/features/chat/presentation/components/MessageBubble.tsx @@ -3,6 +3,7 @@ import { User, Bot, Image, FileText, Download, ExternalLink, CheckCircle, Clock, import ReactMarkdown from 'react-markdown'; import { QRCodeSVG } from 'qrcode.react'; import { FileAttachment } from '../stores/chatStore'; +import { AssessmentResultCard } from './AssessmentResultCard'; interface Message { id: string; @@ -383,5 +384,18 @@ function ToolCallResult({ ); } + if (toolCall.name === 'invoke_assessment_expert') { + // Assessment expert returns a JSON string; parse it + try { + const raw = toolCall.result; + const data = typeof raw === 'string' ? JSON.parse(raw) : raw; + if (data?.assessments && Array.isArray(data.assessments)) { + return ; + } + } catch { + // Not parseable — fall through to null + } + } + return null; }