From d4a1ca870e59f4a5e23155a16e964430a1a934b6 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 21 May 2026 20:01:53 +0000 Subject: [PATCH] feat(dashboard): Meet Kevin home page --- dashboard/src/pages/meetKevin/Home.tsx | 247 +++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 dashboard/src/pages/meetKevin/Home.tsx diff --git a/dashboard/src/pages/meetKevin/Home.tsx b/dashboard/src/pages/meetKevin/Home.tsx new file mode 100644 index 0000000..be1daa9 --- /dev/null +++ b/dashboard/src/pages/meetKevin/Home.tsx @@ -0,0 +1,247 @@ +import { useQuery } from '@tanstack/react-query'; +import { Link } from 'react-router-dom'; +import meetKevinApi from '../../api/meetKevin'; +import { ActionChip, ConvictionBar } from '../../components/meetKevin'; +import { + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, +} from 'recharts'; +import type { + DashboardData, + MarketOutlook, + PipelineHealth, +} from '../../types/meetKevin'; + +const OUTLOOK_TO_NUMERIC: Record = { + bullish: 1, + neutral: 0, + mixed: 0, + bearish: -1, +}; + +const OUTLOOK_BADGE: Record = { + bullish: 'bg-green-600/30 text-green-300 border-green-500/40', + bearish: 'bg-red-600/30 text-red-300 border-red-500/40', + neutral: 'bg-slate-600/30 text-slate-300 border-slate-500/40', + mixed: 'bg-purple-600/30 text-purple-300 border-purple-500/40', +}; + +export default function MeetKevinHome() { + const { data: dashboard, isLoading } = useQuery({ + queryKey: ['meet-kevin', 'dashboard'], + queryFn: meetKevinApi.dashboard, + refetchInterval: 60_000, + }); + const { data: health } = useQuery({ + queryKey: ['meet-kevin', 'health'], + queryFn: meetKevinApi.health, + refetchInterval: 60_000, + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + const analyzedCount = health + ? Object.values(health.counts_by_status).reduce( + (sum: number, v: number) => sum + v, + 0 + ) + : 0; + const dailyCost = health?.daily_cost_usd ?? 0; + const costCap = health?.daily_cost_cap_usd ?? 0; + + const trendData = (dashboard?.outlook_trend_14d ?? []).map( + (row: { day: string; direction: MarketOutlook; count: number }) => ({ + day: row.day.slice(5), // "MM-DD" + value: OUTLOOK_TO_NUMERIC[row.direction] ?? 0, + }) + ); + + const video = dashboard?.latest_video ?? null; + const analysis = dashboard?.latest_analysis ?? null; + + return ( +
+ {/* Header */} +
+

Meet Kevin

+
+
{analyzedCount} videos analyzed
+ {costCap > 0 && ( +
+ ${dailyCost.toFixed(2)} / ${costCap.toFixed(2)} today +
+ )} +
+
+ + {/* Empty state */} + {!video ? ( +
+

Waiting for first poll

+ {health?.last_poll_at && ( +

+ Last poll: {new Date(health.last_poll_at).toLocaleString()} +

+ )} +
+ ) : ( + <> + {/* Latest video hero */} + +
+ {video.thumbnail_url && ( + {video.title} + )} +
+
+ {video.outlook && ( + + {video.outlook} + + )} + {video.top_tickers.slice(0, 5).map((ticker: string) => ( + + ${ticker} + + ))} +
+

+ {video.title} +

+ {video.one_line_summary && ( +

+ {video.one_line_summary} +

+ )} +
+
+ + + {/* Top conviction this week */} + {dashboard && dashboard.top_conviction_week.length > 0 && ( +
+

+ Top Conviction — Last 7 Days +

+
+ {dashboard.top_conviction_week.map( + (row: { + symbol: string; + max_conviction: number; + mention_count: number; + }) => { + const tickerMention = analysis?.tickers?.find( + (t) => t.symbol === row.symbol + ); + return ( + + + ${row.symbol} + + {tickerMention && ( + + )} +
+ +
+ + {(row.max_conviction * 100).toFixed(0)}% + + + {row.mention_count}x + + + ); + } + )} +
+
+ )} + + {/* Outlook trendline */} + {trendData.length > 0 && ( +
+

+ Outlook — 14 Days +

+ + + + + v === 1 ? 'bull' : v === -1 ? 'bear' : 'neu' + } + tick={{ fill: '#94a3b8', fontSize: 11 }} + axisLine={false} + tickLine={false} + /> + { + const n = Number(v ?? 0); + return n === 1 + ? 'bullish' + : n === -1 + ? 'bearish' + : 'neutral/mixed'; + }} + /> + + + +
+ )} + + )} +
+ ); +}