feat(dashboard): show actions + convictions + outlook on Videos cards
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
parent
b7a613ba17
commit
d5359691b1
3 changed files with 53 additions and 11 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue