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; }