Files
jpremill 1feea40684 feat(ui): redesign voice cockpit with clinical companion drawer, Svelte 5 migration, and Gemini 3.8 voice
- Re-architect VoiceSession into full-bleed studio cockpit with overlay slide-over companion drawer
- Eliminate width squishing defect on LiveInsightPanel and add native dark/light theme support
- Integrate authentic Google Gemini 3.8 Flash TTS with Fenrir persona and fallback audio pipeline
- Implement comprehensive Playwright E2E visual regression suite across Desktop, iPad, and iPhone (21 tests, 45 snapshots)
- Migrate core application to Svelte 5 runes (, , ) with reactive stores
- Maintain .NET 8 Azure Functions serverless endpoints and clean build hygiene
2026-09-29 23:35:05 -04:00

151 lines
7.0 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { AnalysisResult, Message } from '../types';
import { ClipboardCheck, X, CheckCircle2, AlertTriangle, Lightbulb, TrendingUp, Download, Copy, Check, FileText } from 'lucide-react';
import { Button } from './ui/button';
import { formatSessionAsMarkdown, downloadFile } from '../services/sessionStorage';
interface SessionReportProps {
analysis: AnalysisResult;
scenarioTitle?: string;
messages?: Message[];
onExit: () => void;
}
const SessionReport: React.FC<SessionReportProps> = ({
analysis,
scenarioTitle = "CBT Roleplay Session",
messages = [],
onExit
}) => {
const [copied, setCopied] = useState(false);
const handleCopyReframe = () => {
navigator.clipboard.writeText(analysis.suggestions.alternativeThought);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleExportMarkdown = () => {
const md = formatSessionAsMarkdown(scenarioTitle, analysis, messages);
const fileName = `cbt-report-${scenarioTitle.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}.md`;
downloadFile(md, fileName, 'text/markdown');
};
const handleExportJSON = () => {
const json = JSON.stringify({ scenarioTitle, analysis, messages, exportedAt: new Date().toISOString() }, null, 2);
const fileName = `cbt-report-${scenarioTitle.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}.json`;
downloadFile(json, fileName, 'application/json');
};
return (
<div className="flex flex-col h-full bg-[#FDFDFF] rounded-3xl border border-slate-200 overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500 w-full max-w-4xl mx-auto">
<div className="bg-teal-600 p-6 flex justify-between items-center text-white">
<div className="flex items-center gap-3">
<ClipboardCheck size={28} />
<div>
<h2 className="text-xl font-bold leading-none">Session Summary Report</h2>
<span className="text-[10px] font-bold uppercase tracking-widest text-teal-100 opacity-80">
{scenarioTitle} • Clinical Evaluation Complete
</span>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={handleExportMarkdown}
className="text-white hover:bg-white/20 rounded-xl text-xs flex items-center gap-1.5"
title="Download Markdown Report"
>
<FileText size={16} />
<span className="hidden sm:inline">Export MD</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleExportJSON}
className="text-white hover:bg-white/20 rounded-xl text-xs flex items-center gap-1.5"
title="Download JSON Report"
>
<Download size={16} />
<span className="hidden sm:inline">JSON</span>
</Button>
<button onClick={onExit} className="bg-white/10 hover:bg-white/20 p-2 rounded-xl border border-white/10 transition-colors ml-1">
<X size={20} />
</button>
</div>
</div>
<div className="flex-grow overflow-y-auto p-8 flex flex-col gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className={`p-6 rounded-2xl border-l-8 ${analysis.analysis.isAdaptive ? 'bg-teal-50/50 border-teal-500' : 'bg-rose-50/50 border-rose-500'}`}>
<div className="flex items-center gap-2 mb-4">
{analysis.analysis.isAdaptive ? <CheckCircle2 className="text-teal-600" /> : <AlertTriangle className="text-rose-600" />}
<span className="font-black text-xs uppercase tracking-widest">{analysis.analysis.isAdaptive ? 'Adaptive Growth Observed' : 'Clinical Patterns Detected'}</span>
</div>
{analysis.analysis.distortions.length > 0 && (
<div className="flex flex-wrap gap-2 mb-4">
{analysis.analysis.distortions.map(d => (
<span key={d} className="bg-white border border-slate-200 px-3 py-1 rounded-lg text-[10px] font-bold text-slate-600 shadow-sm">{d}</span>
))}
</div>
)}
<p className="text-sm leading-relaxed text-slate-700 font-medium italic">"{analysis.analysis.evidence}"</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm flex flex-col justify-center">
<div className="flex justify-between items-center mb-4">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Mastery Level</span>
<div className="flex gap-1.5">
{[1,2,3,4,5].map(s => (
<div key={s} className={`h-2 w-8 rounded-full ${s <= analysis.rating.score ? 'bg-teal-500' : 'bg-slate-100'}`} />
))}
</div>
</div>
<p className="text-sm text-slate-600 leading-relaxed font-light">{analysis.rating.reason}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-2xl border border-teal-100 shadow-sm relative group">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2 text-teal-600">
<Lightbulb size={20} />
<span className="text-[10px] font-black uppercase tracking-widest">Master Reframe</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={handleCopyReframe}
className="h-7 px-2 text-[10px] text-slate-400 hover:text-teal-700 flex items-center gap-1"
>
{copied ? <Check size={12} className="text-teal-600" /> : <Copy size={12} />}
{copied ? 'Copied' : 'Copy'}
</Button>
</div>
<p className="text-base text-slate-800 italic font-medium leading-relaxed">"{analysis.suggestions.alternativeThought}"</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-teal-100 shadow-sm">
<div className="flex items-center gap-2 text-teal-600 mb-3">
<TrendingUp size={20} />
<span className="text-[10px] font-black uppercase tracking-widest">Homework & Next Steps</span>
</div>
<p className="text-sm text-slate-700 leading-relaxed">{analysis.suggestions.behavioralStep}</p>
</div>
</div>
</div>
<div className="p-6 bg-slate-50 border-t border-slate-100 flex justify-center">
<button
onClick={onExit}
className="bg-teal-600 text-white px-12 py-3 rounded-xl font-bold hover:bg-teal-700 shadow-lg shadow-teal-100 transition-all uppercase tracking-widest text-xs"
>
Close Report & Return
</button>
</div>
</div>
);
};
export default React.memo(SessionReport);