Files
jev-match-game/src/ui/report/ReportPage.tsx
T

113 lines
3.7 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 { useEffect, useState } from 'react';
import type { EventStore } from '../../engine/eventStore';
import type { JevClient } from '../../services/jev';
import { BADGE_MAP } from './badges';
import { scoreProfile } from '../../engine/scoring';
import type { DimensionScore } from '../../engine/scoring';
export type { DimensionScore } from '../../engine/scoring';
import { ShareCard } from './ShareCard';
interface Props {
store: EventStore;
jev: JevClient;
onReplay: () => void;
}
/** Act 1 结束后的画像报告页:Jev 一次调用并行问 7 个维度 score */
export function ReportPage({ store, jev, onReplay }: Props) {
const [scores, setScores] = useState<DimensionScore[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [retry, setRetry] = useState(0);
const [playerName, setPlayerName] = useState('');
useEffect(() => {
let cancelled = false;
(async () => {
try {
const events = await store.list();
// 最近一次身份登记的称呼(旧数据缺 name 字段时回退空串 → 只显示编号)
const name = [...events].reverse().find((e) => e.kind === 'profile')?.profile?.name ?? '';
if (!cancelled) setPlayerName(name);
const result = await scoreProfile(events, jev);
if (cancelled) return;
setScores(result);
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
}
})();
return () => {
cancelled = true;
};
}, [store, jev, retry]);
if (error) {
return (
<div className="report">
<h1>画像报告生成失败</h1>
<p>{error}</p>
<button
className="primary"
onClick={() => {
setError(null);
setScores(null);
setRetry((r) => r + 1);
}}
>
画像生成失败,点此重试
</button>
<button className="secondary" onClick={onReplay}>
回到剧情
</button>
</div>
);
}
if (!scores) {
return (
<div className="report">
<h1>档案馆正在整理你的记忆……</h1>
<p>Jev 正在评估 7 个维度。</p>
</div>
);
}
// 勋章 = 置信度最高的维度
const top = [...scores].sort((a, b) => b.confidence - a.confidence)[0];
const badge = BADGE_MAP[top.id];
return (
<div className="report">
<h1>人格画像报告 · Act 1</h1>
<p className="hint">C-2049{playerName ? `「${playerName}」` : ''}的档案</p>
<section className="badge-banner">
<div className="badge-medal">勋</div>
<div>
<h2>{badge.title}</h2>
<p>{badge.flavor}</p>
<p className="badge-note">
(取自实测置信度最高的维度:{top.label},置信度 {Math.round(top.confidence * 100)}%)
</p>
</div>
</section>
<section className="dimension-list">
{scores.map((s) => (
<div key={s.id} className="dimension-row">
<span className="dimension-label">{s.label}</span>
<div className="dimension-bar">
<div className="dimension-fill" style={{ width: `${(s.score / s.max) * 100}%` }} />
</div>
<span className="dimension-value">
{s.score.toFixed(1)}/{s.max} · 置信度 {Math.round(s.confidence * 100)}%
{s.confidence < 0.6 && ' · 证据不足'}
</span>
</div>
))}
</section>
<ShareCard badgeDimension={top.id} scores={scores} playerName={playerName} />
<button className="secondary" onClick={onReplay}>
重玩 Act 1(新轨迹,历史保留)
</button>
</div>
);
}