diff --git a/dashboard/src/pages/meetKevin/VideoDetail.tsx b/dashboard/src/pages/meetKevin/VideoDetail.tsx new file mode 100644 index 0000000..53b79bb --- /dev/null +++ b/dashboard/src/pages/meetKevin/VideoDetail.tsx @@ -0,0 +1,304 @@ +import { useRef, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import meetKevinApi from '../../api/meetKevin'; +import { + ActionChip, + ConvictionBar, + YouTubeEmbed, + type YouTubeEmbedHandle, +} from '../../components/meetKevin'; +import type { MarketOutlook } from '../../types/meetKevin'; + +function formatTime(seconds: number): string { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + const mm = String(m).padStart(2, '0'); + const ss = String(s).padStart(2, '0'); + return h > 0 ? `${h}:${mm}:${ss}` : `${m}:${ss}`; +} + +const OUTLOOK_BANNER: Record = { + bullish: 'bg-green-600/20 border-green-500/40 text-green-300', + bearish: 'bg-red-600/20 border-red-500/40 text-red-300', + neutral: 'bg-slate-600/20 border-slate-500/40 text-slate-300', + mixed: 'bg-purple-600/20 border-purple-500/40 text-purple-300', +}; + +type Tab = 'analysis' | 'transcript' | 'raw'; + +export default function VideoDetail() { + const { id } = useParams<{ id: string }>(); + const videoId = Number(id); + + const playerRef = useRef(null); + const [tab, setTab] = useState('analysis'); + const queryClient = useQueryClient(); + + const { data: detail, isLoading } = useQuery({ + queryKey: ['meet-kevin', 'video', videoId], + queryFn: () => meetKevinApi.getVideo(videoId), + enabled: !isNaN(videoId), + }); + + const { data: transcript } = useQuery({ + queryKey: ['meet-kevin', 'transcript', videoId], + queryFn: () => meetKevinApi.getTranscript(videoId), + enabled: Boolean(detail?.transcript_available), + }); + + const reprocessMutation = useMutation({ + mutationFn: (stage: 'captions' | 'analysis' | 'auto') => + meetKevinApi.reprocess(videoId, stage), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['meet-kevin', 'video', videoId], + }); + }, + }); + + if (isLoading || !detail) { + return ( +
+ +
+ ); + } + + const v = detail.video; + const analysis = detail.analysis; + const showReprocess = + v.status === 'failed' || v.status === 'discovered'; + + const tabs: { id: Tab; label: string; disabled?: boolean }[] = [ + { id: 'analysis', label: 'Analysis' }, + { id: 'transcript', label: 'Transcript', disabled: !detail.transcript_available }, + { id: 'raw', label: 'Raw JSON' }, + ]; + + return ( +
+ {/* Header */} +
+

{v.title}

+

+ {new Date(v.published_at).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + · + {v.status} +

+
+ + {/* Reprocess button */} + {showReprocess && ( +
+ {(['captions', 'analysis', 'auto'] as const).map((stage) => ( + + ))} +
+ )} + + {/* YouTube embed */} + + + {/* Tab strip */} +
+ {tabs.map((t) => ( + + ))} +
+ + {/* Analysis tab */} + {tab === 'analysis' && ( +
+ {!analysis ? ( +
+ Analysis not available — video has not been analyzed yet. +
+ ) : ( + <> + {/* Outlook card */} +
+

+ Market Outlook +

+

+ {analysis.market_outlook_direction} +

+

+ {analysis.market_outlook_reasoning} +

+
+ + {/* Macro themes */} + {analysis.macro_themes.length > 0 && ( +
+

+ Macro Themes +

+
+ {analysis.macro_themes.map((theme) => ( + + {theme} + + ))} +
+
+ )} + + {/* Key risks */} + {analysis.key_risks.length > 0 && ( +
+

+ Key Risks +

+
    + {analysis.key_risks.map((risk) => ( +
  • + {risk} +
  • + ))} +
+
+ )} + + {/* Summary */} + {analysis.summary && ( +
+

+ Summary +

+

+ {analysis.summary} +

+
+ )} + + {/* Per-ticker grid */} + {analysis.tickers.length > 0 && ( +
+
+

+ Tickers +

+
+
+ {analysis.tickers.map((m, idx) => ( +
+
+ + ${m.symbol} + + + + {m.time_horizon.replace('_', ' ')} + + {m.video_timestamp_seconds !== null && ( + + )} +
+
+
+ +
+ + {(m.conviction * 100).toFixed(0)}% + +
+ {m.rationale_quote && ( +

+ "{m.rationale_quote}" +

+ )} +
+ ))} +
+
+ )} + + )} +
+ )} + + {/* Transcript tab */} + {tab === 'transcript' && ( +
+ {!transcript ? ( +
+ Loading transcript… +
+ ) : ( +
+ {transcript.segments.map((s, idx) => ( + + ))} +
+ )} +
+ )} + + {/* Raw JSON tab */} + {tab === 'raw' && ( +
+
+            {JSON.stringify(detail.analysis, null, 2)}
+          
+
+ )} +
+ ); +}