feat: 自适应支线插入引擎(WIP:scoring/branching 抽取、挂点重定向、10 条支线 JSON、决策落库)
This commit is contained in:
@@ -1,43 +1,18 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { EventStore, StoryEvent } from '../../engine/eventStore';
|
||||
import type { EventStore } from '../../engine/eventStore';
|
||||
import type { JevClient } from '../../services/jev';
|
||||
import { BADGE_MAP, PROFILE_DIMENSIONS } from './badges';
|
||||
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';
|
||||
|
||||
export interface DimensionScore {
|
||||
id: string;
|
||||
label: string;
|
||||
score: number;
|
||||
max: number;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
store: EventStore;
|
||||
jev: JevClient;
|
||||
onReplay: () => void;
|
||||
}
|
||||
|
||||
/** 把 append-only 事件轨迹汇总为喂给 Jev 的 state 文本(profile 身份字段是硬过滤字段,不进画像) */
|
||||
function buildState(events: StoryEvent[]): string {
|
||||
const lines = events
|
||||
.filter((e) => e.kind !== 'profile')
|
||||
.map((e) => {
|
||||
if (e.kind === 'choice') {
|
||||
return `[${e.pointId}] 选择「${e.optionLabel}」(维度: ${e.dimensions.join('/') || '字段采集'})`;
|
||||
}
|
||||
if (e.kind === 'followup') {
|
||||
return `[${e.pointId}] 追问(${e.followUpDimension})回答: ${e.transcript}`;
|
||||
}
|
||||
if (e.kind === 'followup_skipped') {
|
||||
return `[${e.pointId}] 追问(${e.followUpDimension})回答: 记不清了(跳过)`;
|
||||
}
|
||||
const t = e.transcript === null ? '(沉默)' : `(讲述: ${e.transcript})`;
|
||||
return `[${e.pointId}] 讲述${t} 情绪签名: ${e.emotionSignature ?? '未知'}`;
|
||||
});
|
||||
return `用户在互动剧情《苍梧疑冢》第一幕中的行为轨迹(按时间排序,含初刷与重玩轨迹):\n${lines.join('\n')}`;
|
||||
}
|
||||
|
||||
/** Act 1 结束后的画像报告页:Jev 一次调用并行问 7 个维度 score */
|
||||
export function ReportPage({ store, jev, onReplay }: Props) {
|
||||
const [scores, setScores] = useState<DimensionScore[] | null>(null);
|
||||
@@ -48,35 +23,14 @@ export function ReportPage({ store, jev, onReplay }: Props) {
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const events = await store.list();
|
||||
// 最近一次身份登记的称呼(旧数据缺 name 字段时回退空串 → 只显示编号)
|
||||
const name = [...events].reverse().find((e) => e.kind === 'profile')?.profile?.name ?? '';
|
||||
if (!cancelled) setPlayerName(name);
|
||||
const state = buildState(events);
|
||||
// 7 维 score 问题,criteria 为 10 级有序数组(索引 0-9 对应 1-10 级)
|
||||
const criteria = Array.from({ length: 10 }, (_, i) => `${i + 1} 级`);
|
||||
const questions = Object.fromEntries(
|
||||
PROFILE_DIMENSIONS.map((d) => [
|
||||
d.id,
|
||||
{
|
||||
type: 'score' as const,
|
||||
instructions: `根据用户行为轨迹,评估其「${d.label}」维度水平:${d.description}`,
|
||||
criteria,
|
||||
},
|
||||
]),
|
||||
);
|
||||
try {
|
||||
const answers = await jev.ask(state, questions);
|
||||
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(
|
||||
PROFILE_DIMENSIONS.map((d) => {
|
||||
const a = answers[d.id];
|
||||
// API score 是 [0, criteria.length-1] 的概率加权浮点值,+1 映射到 1-10 级展示
|
||||
return a?.type === 'score'
|
||||
? { id: d.id, label: d.label, score: Math.round((a.score + 1) * 10) / 10, max: 10, confidence: a.confidence }
|
||||
: { id: d.id, label: d.label, score: 0, max: 10, confidence: 0 };
|
||||
}),
|
||||
);
|
||||
setScores(result);
|
||||
} catch (e) {
|
||||
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
@@ -144,6 +98,7 @@ export function ReportPage({ store, jev, onReplay }: Props) {
|
||||
</div>
|
||||
<span className="dimension-value">
|
||||
{s.score.toFixed(1)}/{s.max} · 置信度 {Math.round(s.confidence * 100)}%
|
||||
{s.confidence < 0.6 && ' · 证据不足'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user