feat(dashboard): show actions + convictions + outlook on Videos cards
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

API already returns top_tickers as [{symbol, action, conviction}] but
the dashboard's VideoSummary type was string[] — so the rich data
was dropped at the type boundary and the cards only showed bare
ticker symbols.

Changes:
- VideoSummary.top_tickers: string[] → VideoTopTicker[]
- Videos.tsx card now renders:
  * outlook pill (bullish/bearish/neutral/mixed, color-coded)
  * action-colored chip per ticker (buy=green, sell=red, watch=yellow,
    avoid=rose, hold=slate) with conviction% inline
  * one-line summary excerpt
- Home.tsx (only other top_tickers consumer) updated to use .symbol
  instead of treating each entry as a string

Effect: skim /meet-kevin/videos to see at a glance "this video says
buy NVDA 85% + watch SPCX 65%, outlook bullish" without clicking in.
This commit is contained in:
Viktor Barzin 2026-05-27 17:22:53 +00:00
parent b7a613ba17
commit d5359691b1
3 changed files with 53 additions and 11 deletions

View file

@ -118,12 +118,13 @@ export default function MeetKevinHome() {
{video.outlook}
</span>
)}
{video.top_tickers.slice(0, 5).map((ticker: string) => (
{video.top_tickers.slice(0, 5).map((t) => (
<span
key={ticker}
className="px-1.5 py-0.5 bg-slate-700 rounded text-slate-300 text-xs font-medium"
key={t.symbol}
title={`${t.action} · conviction ${Math.round(t.conviction * 100)}%`}
className="px-1.5 py-0.5 bg-slate-700 rounded text-slate-300 text-xs font-medium font-mono"
>
${ticker}
{t.symbol}
</span>
))}
</div>

View file

@ -2,7 +2,7 @@ import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import meetKevinApi from '../../api/meetKevin';
import type { VideoStatus } from '../../types/meetKevin';
import type { MarketOutlook, TickerAction, VideoStatus } from '../../types/meetKevin';
const STATUS_OPTIONS: { value: VideoStatus | ''; label: string }[] = [
{ value: '', label: 'All' },
@ -19,6 +19,21 @@ function statusColor(status: VideoStatus): string {
return 'text-yellow-300';
}
const ACTION_CHIP: Record<TickerAction, string> = {
buy: 'bg-green-600/30 text-green-300 border-green-500/40',
sell: 'bg-red-600/30 text-red-300 border-red-500/40',
hold: 'bg-slate-600/30 text-slate-300 border-slate-500/40',
watch: 'bg-yellow-600/30 text-yellow-200 border-yellow-500/40',
avoid: 'bg-rose-600/40 text-rose-200 border-rose-500/40',
};
const OUTLOOK_PILL: Record<MarketOutlook, string> = {
bullish: 'bg-green-600/20 text-green-300 border-green-500/40',
bearish: 'bg-red-600/20 text-red-300 border-red-500/40',
neutral: 'bg-slate-600/20 text-slate-300 border-slate-500/40',
mixed: 'bg-purple-600/20 text-purple-300 border-purple-500/40',
};
export default function MeetKevinVideos() {
const [status, setStatus] = useState<VideoStatus | ''>('');
const [q, setQ] = useState('');
@ -146,18 +161,38 @@ export default function MeetKevinVideos() {
</p>
)}
{video.outlook && (
<div>
<span
className={`inline-block px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide rounded border ${OUTLOOK_PILL[video.outlook]}`}
>
{video.outlook}
</span>
</div>
)}
{video.top_tickers.length > 0 && (
<div className="flex flex-wrap gap-1">
{video.top_tickers.map((ticker) => (
<div className="flex flex-wrap gap-1.5">
{video.top_tickers.map((t) => (
<span
key={ticker}
className="px-1.5 py-0.5 bg-slate-700 rounded text-xs text-slate-300 font-mono"
key={t.symbol}
title={`${t.action} · conviction ${Math.round(t.conviction * 100)}%`}
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded border text-xs ${ACTION_CHIP[t.action]}`}
>
{ticker}
<span className="font-mono font-semibold">{t.symbol}</span>
<span className="opacity-70">
{Math.round(t.conviction * 100)}
</span>
</span>
))}
</div>
)}
{video.one_line_summary && (
<p className="text-xs text-slate-500 line-clamp-2 pt-0.5">
{video.one_line_summary}
</p>
)}
</div>
</Link>
))}

View file

@ -14,6 +14,12 @@ export type VideoStatus =
| 'failed'
| 'skipped';
export interface VideoTopTicker {
symbol: string;
action: TickerAction;
conviction: number;
}
export interface VideoSummary {
id: number;
youtube_video_id: string;
@ -23,7 +29,7 @@ export interface VideoSummary {
thumbnail_url: string | null;
status: VideoStatus;
failure_reason: string | null;
top_tickers: string[];
top_tickers: VideoTopTicker[];
outlook: MarketOutlook | null;
one_line_summary: string | null;
}