feat: P1 풀빌드 — React 프론트 7화면 + 백엔드 상담루프·평가·음성·RAG
web (Vite+React19+TS, Cloudflare Pages 배포): - 디자인토큰(세이지틸/테라코타 SSOT), 앱셸, 공통 UI 프리미티브 - 7화면: 로그인/학습자홈/상담세션/회기리뷰/교수자/관리자/설정 - ClientAvatar: SVG 반구상 흉상 4상태 + RMS 립싱크 + 6파라미터 정서 - 회기리뷰는 외부 레퍼런스 디자인을 Vignette 토큰으로 리스킨 api (FastAPI): - 게이트웨이 /v1/generate·/v1/stream 어댑터(상주풀/EngineSession 보존) - services: 페르소나 L0~L6 빌더 / 결정론 상태머신 / 가드레일 / 턴 오케스트레이터 / 회기간 메모리 / 평가AI / 음성 / RAG - store: DB off 폴백(in-memory), sessions 실구현 검증: - web: node22 tsc+vite build 통과(node23 segfault 회피), Pages 배포 200 - api: app.main import 통과 - 핫픽스: Topbar initials undefined-safe (undefined.trim 크래시) - E2E: 서연(P1) 상담 1턴 — 좋은/나쁜 상담에 차등 반응 실증
This commit is contained in:
parent
859ab26314
commit
24b1b7a6e1
84 changed files with 19645 additions and 107 deletions
122
apps/web/src/pages/session-review/ValenceChart.tsx
Normal file
122
apps/web/src/pages/session-review/ValenceChart.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
/* =====================================================================
|
||||
ValenceChart — 감정 밸런스 타임라인 (SVG 라인 차트).
|
||||
외부 ref의 Emotional Valence Timeline 차용 → Vignette 토큰 리스킨.
|
||||
· 내담자 valence = 실선(테라코타 --clay)
|
||||
· 상담자 baseline = 점선(세이지 --accent-bright)
|
||||
범례·시간축·0선 포함. preserveAspectRatio="none" 로 컨테이너 채움.
|
||||
순흑/순백 금지 — stroke 는 토큰 currentColor 계열만.
|
||||
===================================================================== */
|
||||
|
||||
import type { ValencePoint } from "./mock";
|
||||
|
||||
export interface ValenceChartProps {
|
||||
client: ValencePoint[];
|
||||
baseline: ValencePoint[];
|
||||
/** 시간축 라벨 (좌→우) */
|
||||
xLabels: string[];
|
||||
}
|
||||
|
||||
/** valence(-1~1) → SVG y(0~100, 위가 +1). */
|
||||
function toY(v: number): number {
|
||||
const clamped = Math.max(-1, Math.min(1, v));
|
||||
return 50 - clamped * 45; // +1 → 5, 0 → 50, -1 → 95 (상하 5% 여백)
|
||||
}
|
||||
|
||||
/** 점 배열 → 부드러운 Catmull-Rom→베지어 path d. (단조 곡선, 과한 출렁임 없게) */
|
||||
function smoothPath(points: ValencePoint[]): string {
|
||||
if (points.length === 0) return "";
|
||||
const pts = points.map((p) => ({ x: p.t * 100, y: toY(p.v) }));
|
||||
if (pts.length === 1) return `M${pts[0].x},${pts[0].y}`;
|
||||
|
||||
let d = `M${pts[0].x.toFixed(2)},${pts[0].y.toFixed(2)}`;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const p0 = pts[i - 1] ?? pts[i];
|
||||
const p1 = pts[i];
|
||||
const p2 = pts[i + 1];
|
||||
const p3 = pts[i + 2] ?? p2;
|
||||
// Catmull-Rom → 베지어 (tension 1/6)
|
||||
const c1x = p1.x + (p2.x - p0.x) / 6;
|
||||
const c1y = p1.y + (p2.y - p0.y) / 6;
|
||||
const c2x = p2.x - (p3.x - p1.x) / 6;
|
||||
const c2y = p2.y - (p3.y - p1.y) / 6;
|
||||
d += ` C${c1x.toFixed(2)},${c1y.toFixed(2)} ${c2x.toFixed(2)},${c2y.toFixed(2)} ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
export function ValenceChart({ client, baseline, xLabels }: ValenceChartProps) {
|
||||
const clientPath = smoothPath(client);
|
||||
const baselinePath = smoothPath(baseline);
|
||||
|
||||
return (
|
||||
<div className="sr-chart">
|
||||
<div className="sr-chart__legend">
|
||||
<span className="sr-legend-item">
|
||||
<span className="sr-legend-line sr-legend-line--client" />
|
||||
내담자 정서가
|
||||
</span>
|
||||
<span className="sr-legend-item">
|
||||
<span className="sr-legend-line sr-legend-line--baseline" />
|
||||
상담자 기준선
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="sr-chart__plot">
|
||||
<div className="sr-chart__yaxis" aria-hidden="true">
|
||||
<span>+1.0</span>
|
||||
<span>0.0</span>
|
||||
<span>-1.0</span>
|
||||
</div>
|
||||
|
||||
<div className="sr-chart__canvas">
|
||||
<svg
|
||||
className="sr-chart__svg"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
role="img"
|
||||
aria-label="회기 동안 내담자 정서가와 상담자 기준선의 변화 추이"
|
||||
>
|
||||
{/* 0선 (중립) */}
|
||||
<line
|
||||
x1="0"
|
||||
y1="50"
|
||||
x2="100"
|
||||
y2="50"
|
||||
stroke="var(--hair)"
|
||||
strokeWidth="1"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
{/* 상담자 baseline (점선, 세이지) */}
|
||||
<path
|
||||
d={baselinePath}
|
||||
fill="none"
|
||||
stroke="var(--accent-bright)"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="4 4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
opacity="0.7"
|
||||
/>
|
||||
{/* 내담자 valence (실선, 테라코타) */}
|
||||
<path
|
||||
d={clientPath}
|
||||
fill="none"
|
||||
stroke="var(--clay)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="sr-chart__xaxis" aria-hidden="true">
|
||||
{xLabels.map((label, i) => (
|
||||
<span key={i}>{label}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue