iconsulting/packages/web-client/src/features/chat/presentation/components/AssessmentResultCard.tsx

179 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-secondary-100 rounded-full overflow-hidden">
<div
className={clsx('h-full rounded-full transition-all', getColor(score))}
style={{ width: `${score}%` }}
/>
</div>
<span className="text-xs font-medium text-secondary-600 whitespace-nowrap w-16 text-right">
{label || getLabel(score)} {score}
</span>
</div>
);
}
function CategoryCard({ assessment, isRecommended }: { assessment: CategoryAssessment; isRecommended: boolean }) {
return (
<div className={clsx(
'p-3 rounded-lg border transition-all',
isRecommended ? 'border-primary-300 bg-primary-50/50' : 'border-secondary-200 bg-white',
)}>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
{isRecommended && <Award className="w-4 h-4 text-primary-500" />}
<span className="font-medium text-sm">{assessment.categoryName}</span>
<span className="text-xs text-secondary-400">{assessment.category}</span>
{assessment.subClass && (
<span className="text-xs px-1.5 py-0.5 bg-secondary-100 rounded">{assessment.subClass}</span>
)}
</div>
<div className="flex items-center gap-1">
{assessment.eligible ? (
<CheckCircle className="w-4 h-4 text-green-500" />
) : (
<XCircle className="w-4 h-4 text-red-400" />
)}
<span className={clsx('text-xs', assessment.eligible ? 'text-green-600' : 'text-red-500')}>
{assessment.eligible ? '符合条件' : '不符合'}
</span>
</div>
</div>
<ScoreBar score={assessment.score} />
{assessment.highlights.length > 0 && (
<div className="mt-2">
{assessment.highlights.map((h, i) => (
<div key={i} className="flex items-start gap-1.5 text-xs text-green-700 mt-1">
<CheckCircle className="w-3 h-3 mt-0.5 flex-shrink-0" />
<span>{h}</span>
</div>
))}
</div>
)}
{assessment.concerns.length > 0 && (
<div className="mt-1">
{assessment.concerns.map((c, i) => (
<div key={i} className="flex items-start gap-1.5 text-xs text-orange-600 mt-1">
<AlertTriangle className="w-3 h-3 mt-0.5 flex-shrink-0" />
<span>{c}</span>
</div>
))}
</div>
)}
{assessment.missingInfo && assessment.missingInfo.length > 0 && (
<div className="mt-1">
{assessment.missingInfo.map((m, i) => (
<div key={i} className="flex items-start gap-1.5 text-xs text-secondary-500 mt-1">
<Info className="w-3 h-3 mt-0.5 flex-shrink-0" />
<span>{m}</span>
</div>
))}
</div>
)}
</div>
);
}
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 (
<div className="mt-3 p-4 bg-white rounded-lg border border-secondary-200">
{/* Header with overall score */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-primary-500" />
<span className="font-semibold text-sm"></span>
</div>
<div className="flex items-center gap-1.5">
<span className="text-xs text-secondary-500"></span>
<span className={clsx(
'text-lg font-bold',
data.suitabilityScore >= 70 ? 'text-green-600' :
data.suitabilityScore >= 50 ? 'text-yellow-600' : 'text-red-500',
)}>
{data.suitabilityScore}
</span>
</div>
</div>
{/* Summary */}
{data.summary && (
<p className="text-xs text-secondary-600 mb-3 leading-relaxed">{data.summary}</p>
)}
{/* Category assessments */}
<div className="space-y-2">
{sorted.map((assessment) => (
<CategoryCard
key={assessment.category}
assessment={assessment}
isRecommended={topSet.has(assessment.category)}
/>
))}
</div>
{/* Overall recommendation */}
{data.overallRecommendation && (
<div className="mt-3 p-2 bg-primary-50 rounded-lg">
<p className="text-xs text-primary-800 leading-relaxed">
<strong></strong>{data.overallRecommendation}
</p>
</div>
)}
</div>
);
}