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) => (
+
+ ))}
+
+ )}
+
+ {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;
}