feat: update UI styles and add lightningcss dependency

- Introduced lightningcss as a dependency for enhanced styling capabilities.
- Updated global CSS variables to reflect a new design theme, including background, border, and text colors.
- Modified layout components to incorporate new fonts and improved spacing.
- Enhanced sidebar and dashboard layouts with subtle background textures and improved responsiveness.
- Refined button styles and added new animations for a more dynamic user experience.
- Improved the Run History Table with a new grid layout and status indicators.
This commit is contained in:
Ali AL OGAILI 2026-03-23 05:18:06 +01:00
parent ae6776afc3
commit 0690f628ab
16 changed files with 1352 additions and 763 deletions

View File

@ -5,13 +5,25 @@ import RunHistoryTable from '@/features/history/components/RunHistoryTable'
export default function HistoryPage() { export default function HistoryPage() {
const { runs, loading, error } = useRunHistory() const { runs, loading, error } = useRunHistory()
return ( return (
<div className="max-w-4xl animate-fade-up"> <div className="max-w-4xl animate-fade-up">
<div className="flex items-center justify-between mb-8"> {/* Header */}
<div className="flex items-end justify-between mb-8 gap-4">
<div> <div>
<div className="apex-label mb-3" style={{ color: 'var(--accent)', opacity: 0.7 }}>
Execution Log
</div>
<h1 <h1
className="text-2xl font-bold mb-1" style={{
style={{ color: 'var(--text-high)', fontFamily: 'var(--font-manrope)' }} fontFamily: 'var(--font-syne)',
fontSize: '32px',
fontWeight: 800,
letterSpacing: '-0.04em',
color: 'var(--text-high)',
lineHeight: 1.1,
marginBottom: '8px',
}}
> >
Run History Run History
</h1> </h1>
@ -19,21 +31,40 @@ export default function HistoryPage() {
Comprehensive log of all agent execution cycles Comprehensive log of all agent execution cycles
</p> </p>
</div> </div>
<Link href="/new-run" className="btn-primary px-5 py-2.5 text-sm">
+ New Analysis <Link href="/new-run" className="btn-primary shrink-0">
<svg width="11" height="11" viewBox="0 0 11 11" fill="none">
<polygon points="2.5,2 9,5.5 2.5,9" fill="var(--bg-base)"/>
</svg>
New Analysis
</Link> </Link>
</div> </div>
{loading && ( {loading && (
<p className="text-sm" style={{ color: 'var(--text-mid)' }}> <div className="flex items-center gap-2.5 py-8" style={{ color: 'var(--text-mid)' }}>
Loading... <div
</p> className="w-2 h-2 rounded-full"
style={{ background: 'var(--accent)', animation: 'shimmer 1s infinite' }}
/>
<span className="text-sm" style={{ fontFamily: 'var(--font-mono)', fontSize: '12px', letterSpacing: '0.04em' }}>
Loading runs
</span>
</div>
)} )}
{error && ( {error && (
<p className="text-sm" style={{ color: 'var(--error)' }}> <div
className="px-4 py-3 rounded-xl text-sm"
style={{
background: 'var(--error-bg)',
color: 'var(--error)',
border: '1px solid rgba(255,43,62,0.25)',
}}
>
{error} {error}
</p> </div>
)} )}
{!loading && <RunHistoryTable runs={runs} />} {!loading && <RunHistoryTable runs={runs} />}
</div> </div>
) )

View File

@ -4,7 +4,22 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
return ( return (
<div className="flex min-h-screen" style={{ background: 'var(--bg-base)', color: 'var(--text-high)' }}> <div className="flex min-h-screen" style={{ background: 'var(--bg-base)', color: 'var(--text-high)' }}>
<Sidebar /> <Sidebar />
<main className="flex-1 p-8 overflow-auto">{children}</main> <main className="flex-1 overflow-auto relative">
{/* Subtle mesh background */}
<div
className="fixed inset-0 pointer-events-none"
style={{
background: `
radial-gradient(ellipse 50% 40% at 70% 15%, rgba(0,196,232,0.04) 0%, transparent 60%),
radial-gradient(ellipse 40% 50% at 20% 80%, rgba(80,40,200,0.04) 0%, transparent 60%)
`,
zIndex: 0,
}}
/>
<div className="relative z-10 p-8">
{children}
</div>
</main>
</div> </div>
) )
} }

View File

@ -2,27 +2,28 @@ import RunConfigForm from '@/features/new-run/components/RunConfigForm'
export default function NewRunPage() { export default function NewRunPage() {
return ( return (
<div className="max-w-[640px] animate-fade-up"> <div className="max-w-[660px] animate-fade-up">
{/* Page header */} {/* Page header */}
<div className="mb-8"> <div className="mb-8">
<div <div className="apex-label mb-3" style={{ color: 'var(--accent)', opacity: 0.7 }}>
className="apex-label mb-3"
>
Intelligence Engine Intelligence Engine
</div> </div>
<h1 <h1
className="text-[28px] font-bold tracking-tight mb-2"
style={{ style={{
fontFamily: 'var(--font-syne)',
fontSize: '32px',
fontWeight: 800,
letterSpacing: '-0.04em',
color: 'var(--text-high)', color: 'var(--text-high)',
fontFamily: 'var(--font-manrope)', lineHeight: 1.1,
letterSpacing: '-0.03em', marginBottom: '10px',
}} }}
> >
New Analysis New Analysis
</h1> </h1>
<p <p
className="text-sm leading-relaxed" className="text-sm leading-relaxed"
style={{ color: 'var(--text-mid)' }} style={{ color: 'var(--text-mid)', maxWidth: '480px', lineHeight: 1.7 }}
> >
Configure a multi-agent analysis run. Your AI team will research market data, Configure a multi-agent analysis run. Your AI team will research market data,
debate investment thesis, and deliver a structured decision. debate investment thesis, and deliver a structured decision.

View File

@ -7,11 +7,13 @@ import PhaseTabs from '@/features/run-detail/components/PhaseTabs'
import { getRun } from '@/lib/api-client' import { getRun } from '@/lib/api-client'
import type { RunSummary } from '@/lib/types/run' import type { RunSummary } from '@/lib/types/run'
const STATUS_CONFIG: Record<string, { bg: string; color: string; dot: string; label: string }> = { const STATUS_CONFIG: Record<string, {
connecting: { bg: 'var(--bg-elevated)', color: 'var(--text-mid)', dot: 'var(--text-low)', label: 'Connecting' }, bg: string; color: string; dot: string; label: string; pulse: boolean
running: { bg: 'var(--hold-bg)', color: 'var(--hold)', dot: 'var(--hold)', label: 'Running' }, }> = {
complete: { bg: 'var(--buy-bg)', color: 'var(--buy)', dot: 'var(--buy)', label: 'Complete' }, connecting: { bg: 'var(--bg-elevated)', color: 'var(--text-mid)', dot: 'var(--text-low)', label: 'Connecting', pulse: false },
error: { bg: 'var(--error-bg)', color: 'var(--error)', dot: 'var(--error)', label: 'Error' }, running: { bg: 'var(--hold-bg)', color: 'var(--hold)', dot: 'var(--hold)', label: 'Running', pulse: true },
complete: { bg: 'var(--buy-bg)', color: 'var(--buy)', dot: 'var(--buy)', label: 'Complete', pulse: false },
error: { bg: 'var(--error-bg)', color: 'var(--error)', dot: 'var(--error)', label: 'Error', pulse: false },
} }
export default function RunDetailPage({ params }: { params: Promise<{ id: string }> }) { export default function RunDetailPage({ params }: { params: Promise<{ id: string }> }) {
@ -29,51 +31,60 @@ export default function RunDetailPage({ params }: { params: Promise<{ id: string
<div className="max-w-4xl space-y-4 animate-fade-up"> <div className="max-w-4xl space-y-4 animate-fade-up">
{/* Header */} {/* Header */}
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4 mb-2">
<div> <div>
<div <div className="apex-label mb-3" style={{ color: 'var(--accent)', opacity: 0.7 }}>
className="apex-label mb-2"
>
Analysis Run Analysis Run
</div> </div>
<h1 <h1
className="text-[26px] font-bold tracking-tight" className="flex items-baseline gap-3"
style={{ style={{
color: 'var(--text-high)', fontFamily: 'var(--font-syne)',
fontFamily: 'var(--font-manrope)', fontSize: '32px',
letterSpacing: '-0.03em', fontWeight: 800,
letterSpacing: '-0.04em',
lineHeight: 1.1,
}} }}
> >
{run ? ( {run ? (
<> <>
<span style={{ color: 'var(--accent-light)' }}>{run.ticker}</span> <span
<span style={{ color: 'var(--text-low)', fontWeight: 400, margin: '0 8px' }}>·</span> className="terminal-text"
<span>{run.date}</span> style={{ color: 'var(--accent-light)', fontFamily: 'var(--font-mono)', fontWeight: 700, letterSpacing: '0.04em' }}
>
{run.ticker}
</span>
<span style={{ color: 'var(--text-low)', fontWeight: 400, fontFamily: 'var(--font-manrope)' }}>·</span>
<span style={{ color: 'var(--text-mid)', fontSize: '22px', fontFamily: 'var(--font-mono)', fontWeight: 500 }}>
{run.date}
</span>
</> </>
) : ( ) : (
<span style={{ color: 'var(--text-mid)' }}>Loading</span> <span style={{ color: 'var(--text-mid)', fontSize: '24px' }}>Loading</span>
)} )}
</h1> </h1>
</div> </div>
{/* Status badge */} {/* Status pill */}
<div <div
className="flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium mt-1" className="flex items-center gap-2 px-3.5 py-1.5 rounded-full text-[10px] font-bold mt-1.5 shrink-0"
style={{ style={{
background: sc.bg, background: sc.bg,
color: sc.color, color: sc.color,
border: `1px solid ${sc.dot}40`, border: `1px solid ${sc.dot}40`,
fontFamily: 'var(--font-manrope)', fontFamily: 'var(--font-mono)',
letterSpacing: '0.1em',
}} }}
> >
<div <div
className="w-1.5 h-1.5 rounded-full shrink-0" className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ style={{
background: sc.dot, background: sc.dot,
animation: status === 'running' ? 'shimmer 1.2s ease-in-out infinite' : 'none', boxShadow: sc.pulse ? `0 0 6px ${sc.dot}` : 'none',
animation: sc.pulse ? 'shimmer 1s ease-in-out infinite' : 'none',
}} }}
/> />
{sc.label} {sc.label.toUpperCase()}
</div> </div>
</div> </div>
@ -83,14 +94,14 @@ export default function RunDetailPage({ params }: { params: Promise<{ id: string
{/* Error */} {/* Error */}
{error && ( {error && (
<div <div
className="px-4 py-3 rounded-lg text-sm" className="px-4 py-3 rounded-xl text-sm flex items-center gap-2"
style={{ style={{
background: 'var(--error-bg)', background: 'var(--error-bg)',
color: 'var(--error)', color: 'var(--error)',
border: '1px solid rgba(255,68,68,0.25)', border: '1px solid rgba(255,43,62,0.25)',
}} }}
> >
<span className="font-medium">Error:</span> {error} <span className="font-bold">Error:</span> {error}
</div> </div>
)} )}
@ -101,7 +112,6 @@ export default function RunDetailPage({ params }: { params: Promise<{ id: string
{/* Phase tabs + reports */} {/* Phase tabs + reports */}
<PhaseTabs steps={steps} reports={reports} /> <PhaseTabs steps={steps} reports={reports} />
</div> </div>
) )
} }

View File

@ -1,57 +1,63 @@
@import "tailwindcss"; @import "tailwindcss";
/* ─── APEX Design Tokens ────────────────────────────────────────── */ /* ─── OBSIDIAN Design Tokens ─────────────────────────────────────── */
:root { :root {
/* Backgrounds */ /* Backgrounds */
--bg-base: #040C1A; --bg-base: #050508;
--bg-surface: #070F1C; --bg-surface: #08080F;
--bg-card: #0C1628; --bg-card: #0C0C18;
--bg-elevated: #121E30; --bg-elevated: #111120;
--bg-hover: #182338; --bg-hover: #17172A;
--bg-active: #1E2E42; --bg-active: #1D1D38;
--bg-sidebar: #030810; --bg-sidebar: #030306;
/* Borders */ /* Borders */
--border: rgba(82, 122, 196, 0.10); --border: rgba(80, 80, 200, 0.07);
--border-raised: rgba(82, 122, 196, 0.18); --border-raised: rgba(80, 80, 200, 0.14);
--border-active: rgba(68, 128, 255, 0.40); --border-active: rgba(0, 200, 240, 0.28);
--border-accent: rgba(68, 128, 255, 0.60); --border-accent: rgba(0, 200, 240, 0.55);
/* Text */ /* Text */
--text-high: #E0E8FF; --text-high: #F0F2FF;
--text-mid: #7A8FAD; --text-mid: #525E80;
--text-low: #354869; --text-low: #202840;
--text-faint: #1C2A40; --text-faint: #0E1220;
/* Accent Blue */ /* Accent Cyan */
--accent: #4480FF; --accent: #00C4E8;
--accent-light: #8AAFFF; --accent-light: #65DAFF;
--accent-dim: #1A3A88; --accent-dim: #00243A;
--accent-glow: rgba(68, 128, 255, 0.14); --accent-glow: rgba(0, 196, 232, 0.14);
--accent-glow2: rgba(68, 128, 255, 0.06); --accent-glow2: rgba(0, 196, 232, 0.05);
/* Semantic */ /* Gold (premium highlight) */
--buy: #00CE68; --gold: #FFB400;
--buy-bg: rgba(0, 206, 104, 0.08); --gold-bg: rgba(255, 180, 0, 0.07);
--buy-ring: rgba(0, 206, 104, 0.25); --gold-dim: #271D00;
--sell: #FF3355; --gold-ring: rgba(255, 180, 0, 0.25);
--sell-bg: rgba(255, 51, 85, 0.08);
--sell-ring:rgba(255, 51, 85, 0.25); /* Semantic — BUY / SELL / HOLD */
--hold: #F59E0B; --buy: #00E078;
--hold-bg: rgba(245, 158, 11, 0.08); --buy-bg: rgba(0, 224, 120, 0.07);
--hold-ring:rgba(245, 158, 11, 0.25); --buy-ring: rgba(0, 224, 120, 0.22);
--error: #FF4444; --sell: #FF1F4C;
--error-bg: rgba(255, 68, 68, 0.08); --sell-bg: rgba(255, 31, 76, 0.07);
--sell-ring: rgba(255, 31, 76, 0.22);
--hold: #FFB400;
--hold-bg: rgba(255, 180, 0, 0.07);
--hold-ring: rgba(255, 180, 0, 0.22);
--error: #FF2B3E;
--error-bg: rgba(255, 43, 62, 0.07);
/* Status */ /* Status */
--status-running: #F59E0B; --status-running: #FFB400;
--status-done: #4480FF; --status-done: #00C4E8;
--status-pending: #1C2A40; --status-pending: #141828;
} }
/* ─── Animations ─────────────────────────────────────────────────── */ /* ─── Keyframes ──────────────────────────────────────────────────── */
@keyframes fadeUp { @keyframes fadeUp {
from { opacity: 0; transform: translateY(10px); } from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
@ -61,8 +67,8 @@
} }
@keyframes shimmer { @keyframes shimmer {
0%, 100% { opacity: 0.35; } 0%, 100% { opacity: 0.25; }
50% { opacity: 0.9; } 50% { opacity: 1; }
} }
@keyframes spin-slow { @keyframes spin-slow {
@ -70,8 +76,8 @@
} }
@keyframes pulse-glow { @keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 0 0 var(--accent-glow); } 0%, 100% { box-shadow: 0 0 0 0 var(--accent-glow); }
50% { box-shadow: 0 0 0 6px var(--accent-glow); } 50% { box-shadow: 0 0 0 8px var(--accent-glow); }
} }
@keyframes scan-line { @keyframes scan-line {
@ -79,6 +85,45 @@
to { transform: translateX(200%); } to { transform: translateX(200%); }
} }
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes glow-breathe {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
@keyframes data-flicker {
0%, 95%, 100% { opacity: 1; }
96% { opacity: 0.6; }
97% { opacity: 1; }
98% { opacity: 0.75; }
99% { opacity: 1; }
}
@keyframes verdict-reveal {
0% { opacity: 0; transform: scale(0.88) translateY(10px); filter: blur(4px); }
100% { opacity: 1; transform: scale(1) translateY(0); filter: blur(0); }
}
@keyframes orbit {
from { transform: rotate(0deg) translateX(24px) rotate(0deg); }
to { transform: rotate(360deg) translateX(24px) rotate(-360deg); }
}
@keyframes gradient-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
@keyframes step-complete {
0% { transform: scale(0.5); opacity: 0; }
60% { transform: scale(1.2); }
100% { transform: scale(1); opacity: 1; }
}
/* ─── Base ───────────────────────────────────────────────────────── */ /* ─── Base ───────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; } *, *::before, *::after { box-sizing: border-box; }
@ -91,34 +136,68 @@ html, body {
font-feature-settings: "cv02", "cv03", "cv04", "cv11"; font-feature-settings: "cv02", "cv03", "cv04", "cv11";
} }
/* ─── Background Textures ─────────────────────────────────────────── */
.dot-grid {
background-image: radial-gradient(circle, rgba(100, 100, 255, 0.12) 1px, transparent 1px);
background-size: 24px 24px;
}
.mesh-bg {
background:
radial-gradient(ellipse 60% 40% at 20% 20%, rgba(0, 196, 232, 0.05) 0%, transparent 60%),
radial-gradient(ellipse 50% 60% at 80% 70%, rgba(80, 40, 200, 0.06) 0%, transparent 60%),
radial-gradient(ellipse 40% 50% at 50% 50%, rgba(255, 180, 0, 0.03) 0%, transparent 70%);
}
/* ─── Typography ─────────────────────────────────────────────────── */ /* ─── Typography ─────────────────────────────────────────────────── */
.apex-display { .apex-display {
font-family: var(--font-manrope, 'Manrope'), sans-serif; font-family: var(--font-syne, 'Syne'), var(--font-manrope, 'Manrope'), sans-serif;
font-weight: 700; font-weight: 800;
letter-spacing: -0.03em; letter-spacing: -0.04em;
color: var(--text-high); color: var(--text-high);
} }
.apex-label { .apex-label {
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 700;
letter-spacing: 0.12em; letter-spacing: 0.14em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-mid); color: var(--text-mid);
font-family: var(--font-manrope, 'Manrope'), sans-serif; font-family: var(--font-manrope, 'Manrope'), sans-serif;
} }
.terminal-text {
font-family: var(--font-mono, 'JetBrains Mono'), 'Courier New', monospace;
font-feature-settings: "tnum";
font-variant-numeric: tabular-nums;
}
.gradient-text {
background: linear-gradient(135deg, var(--text-high) 0%, var(--accent-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ─── Cards ──────────────────────────────────────────────────────── */ /* ─── Cards ──────────────────────────────────────────────────────── */
.apex-card { .apex-card {
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 10px; border-radius: 12px;
} }
.apex-card-elevated { .apex-card-elevated {
background: var(--bg-elevated); background: var(--bg-elevated);
border: 1px solid var(--border-raised); border: 1px solid var(--border-raised);
border-radius: 10px; border-radius: 12px;
}
.glass-card {
background: rgba(12, 12, 28, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border-raised);
border-radius: 12px;
} }
/* ─── Buttons ────────────────────────────────────────────────────── */ /* ─── Buttons ────────────────────────────────────────────────────── */
@ -127,34 +206,36 @@ html, body {
overflow: hidden; overflow: hidden;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 7px;
background: var(--accent); background: var(--accent);
color: #fff; color: #020508;
font-weight: 600; font-weight: 700;
font-size: 13px; font-size: 13px;
letter-spacing: 0.01em; letter-spacing: 0.02em;
border-radius: 8px; border-radius: 8px;
padding: 9px 20px; padding: 10px 22px;
transition: background 0.15s, opacity 0.15s, box-shadow 0.15s; transition: background 0.15s, opacity 0.15s, box-shadow 0.2s, transform 0.1s;
font-family: var(--font-manrope, 'Manrope'), sans-serif; font-family: var(--font-manrope, 'Manrope'), sans-serif;
cursor: pointer; cursor: pointer;
} }
.btn-primary::after { .btn-primary::before {
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.12) 0%, transparent 60%); background: linear-gradient(135deg, rgba(255,255,255,0.18) 0%, transparent 55%);
pointer-events: none; pointer-events: none;
} }
.btn-primary:hover { .btn-primary:hover {
background: #5590FF; background: #28DAFF;
box-shadow: 0 4px 16px rgba(68,128,255,0.35); box-shadow: 0 0 0 4px rgba(0, 196, 232, 0.18), 0 8px 24px rgba(0, 196, 232, 0.28);
transform: translateY(-1px);
} }
.btn-primary:active { opacity: 0.85; } .btn-primary:active { opacity: 0.85; transform: translateY(0); }
.btn-primary:disabled { .btn-primary:disabled {
opacity: 0.35; opacity: 0.3;
cursor: not-allowed; cursor: not-allowed;
box-shadow: none; box-shadow: none;
transform: none;
} }
.btn-secondary { .btn-secondary {
@ -163,12 +244,12 @@ html, body {
gap: 6px; gap: 6px;
background: var(--bg-elevated); background: var(--bg-elevated);
color: var(--text-mid); color: var(--text-mid);
font-weight: 500; font-weight: 600;
font-size: 13px; font-size: 13px;
border: 1px solid var(--border-raised); border: 1px solid var(--border-raised);
border-radius: 8px; border-radius: 8px;
padding: 8px 16px; padding: 9px 18px;
transition: background 0.15s, color 0.15s, border-color 0.15s; transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
font-family: var(--font-manrope, 'Manrope'), sans-serif; font-family: var(--font-manrope, 'Manrope'), sans-serif;
cursor: pointer; cursor: pointer;
} }
@ -176,10 +257,29 @@ html, body {
background: var(--bg-hover); background: var(--bg-hover);
color: var(--text-high); color: var(--text-high);
border-color: var(--border-active); border-color: var(--border-active);
transform: translateY(-1px);
}
.btn-ghost {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--bg-elevated);
color: var(--text-mid);
font-weight: 600;
font-size: 13px;
border: 1px solid var(--border-raised);
border-radius: 8px;
padding: 9px 18px;
transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
font-family: var(--font-manrope, 'Manrope'), sans-serif;
cursor: pointer;
}
.btn-ghost:hover {
background: var(--bg-hover);
color: var(--text-high);
border-color: var(--border-active);
transform: translateY(-1px);
} }
/* backwards compat aliases */
.btn-ghost { @apply btn-secondary; }
/* ─── Inputs ─────────────────────────────────────────────────────── */ /* ─── Inputs ─────────────────────────────────────────────────────── */
.vault-input { .vault-input {
@ -188,10 +288,10 @@ html, body {
color: var(--text-high); color: var(--text-high);
font-size: 13px; font-size: 13px;
border: 1px solid var(--border-raised); border: 1px solid var(--border-raised);
border-radius: 6px; border-radius: 8px;
padding: 9px 12px; padding: 10px 14px;
outline: none; outline: none;
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s; transition: border-color 0.15s, background 0.15s, box-shadow 0.2s;
font-family: var(--font-manrope, 'Manrope'), sans-serif; font-family: var(--font-manrope, 'Manrope'), sans-serif;
} }
.vault-input::placeholder { color: var(--text-low); } .vault-input::placeholder { color: var(--text-low); }
@ -202,15 +302,23 @@ html, body {
} }
.vault-input option { background: var(--bg-elevated); } .vault-input option { background: var(--bg-elevated); }
/* date input calendar icon color */
.vault-input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(0.5) sepia(1) saturate(2) hue-rotate(180deg);
opacity: 0.5;
cursor: pointer;
}
/* ─── Badges ─────────────────────────────────────────────────────── */ /* ─── Badges ─────────────────────────────────────────────────────── */
.badge-buy { .badge-buy {
display: inline-flex; align-items: center; display: inline-flex; align-items: center;
background: var(--buy-bg); background: var(--buy-bg);
color: var(--buy); color: var(--buy);
border: 1px solid var(--buy-ring); border: 1px solid var(--buy-ring);
font-size: 11px; font-weight: 700; font-size: 10px; font-weight: 800;
padding: 2px 9px; border-radius: 99px; padding: 2px 10px; border-radius: 99px;
letter-spacing: 0.04em; letter-spacing: 0.08em;
text-transform: uppercase;
font-family: var(--font-manrope, 'Manrope'), sans-serif; font-family: var(--font-manrope, 'Manrope'), sans-serif;
} }
.badge-sell { .badge-sell {
@ -218,9 +326,10 @@ html, body {
background: var(--sell-bg); background: var(--sell-bg);
color: var(--sell); color: var(--sell);
border: 1px solid var(--sell-ring); border: 1px solid var(--sell-ring);
font-size: 11px; font-weight: 700; font-size: 10px; font-weight: 800;
padding: 2px 9px; border-radius: 99px; padding: 2px 10px; border-radius: 99px;
letter-spacing: 0.04em; letter-spacing: 0.08em;
text-transform: uppercase;
font-family: var(--font-manrope, 'Manrope'), sans-serif; font-family: var(--font-manrope, 'Manrope'), sans-serif;
} }
.badge-hold { .badge-hold {
@ -228,22 +337,40 @@ html, body {
background: var(--hold-bg); background: var(--hold-bg);
color: var(--hold); color: var(--hold);
border: 1px solid var(--hold-ring); border: 1px solid var(--hold-ring);
font-size: 11px; font-weight: 700; font-size: 10px; font-weight: 800;
padding: 2px 9px; border-radius: 99px; padding: 2px 10px; border-radius: 99px;
letter-spacing: 0.04em; letter-spacing: 0.08em;
text-transform: uppercase;
font-family: var(--font-manrope, 'Manrope'), sans-serif; font-family: var(--font-manrope, 'Manrope'), sans-serif;
} }
/* ─── Horizontal Rule ────────────────────────────────────────────── */
.apex-rule {
height: 1px;
background: linear-gradient(90deg, transparent, var(--border-raised), transparent);
border: none;
}
/* ─── Scrollbar ──────────────────────────────────────────────────── */ /* ─── Scrollbar ──────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-raised); border-radius: 99px; } ::-webkit-scrollbar-thumb { background: var(--border-raised); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-active); } ::-webkit-scrollbar-thumb:hover { background: var(--border-active); }
/* ─── Selection ──────────────────────────────────────────────────── */ /* ─── Selection ──────────────────────────────────────────────────── */
::selection { background: var(--accent-glow); color: var(--text-high); } ::selection { background: rgba(0, 196, 232, 0.20); color: var(--text-high); }
/* ─── Fade animations for components ────────────────────────────── */ /* ─── Animations ─────────────────────────────────────────────────── */
.animate-fade-up { animation: fadeUp 0.3s ease-out both; } .animate-fade-up { animation: fadeUp 0.35s cubic-bezier(0.16,1,0.3,1) both; }
.animate-fade-in { animation: fadeIn 0.25s ease-out both; } .animate-fade-in { animation: fadeIn 0.25s ease-out both; }
.animate-shimmer { animation: shimmer 1.6s ease-in-out infinite; } .animate-shimmer { animation: shimmer 1.6s ease-in-out infinite; }
.animate-float { animation: float 4s ease-in-out infinite; }
.animate-glow { animation: glow-breathe 2.5s ease-in-out infinite; }
.animate-flicker { animation: data-flicker 8s ease-in-out infinite; }
/* Staggered fade-up delays */
.delay-50 { animation-delay: 50ms; }
.delay-100 { animation-delay: 100ms; }
.delay-150 { animation-delay: 150ms; }
.delay-200 { animation-delay: 200ms; }
.delay-300 { animation-delay: 300ms; }

View File

@ -1,5 +1,5 @@
import type { Metadata } from 'next' import type { Metadata } from 'next'
import { Manrope, Inter } from 'next/font/google' import { Manrope, Syne, JetBrains_Mono } from 'next/font/google'
import './globals.css' import './globals.css'
const manrope = Manrope({ const manrope = Manrope({
@ -8,15 +8,21 @@ const manrope = Manrope({
weight: ['400', '500', '600', '700', '800'], weight: ['400', '500', '600', '700', '800'],
}) })
const inter = Inter({ const syne = Syne({
variable: '--font-inter', variable: '--font-syne',
subsets: ['latin'], subsets: ['latin'],
weight: ['400', '500', '600'], weight: ['700', '800'],
})
const jetbrainsMono = JetBrains_Mono({
variable: '--font-mono',
subsets: ['latin'],
weight: ['400', '500', '600', '700'],
}) })
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'TradingAgents', title: 'TradingAgents — Multi-Agent AI Analysis',
description: 'Multi-agent trading analysis', description: 'Institutional-grade multi-agent trading intelligence',
} }
export default function RootLayout({ export default function RootLayout({
@ -27,7 +33,7 @@ export default function RootLayout({
return ( return (
<html <html
lang="en" lang="en"
className={`${manrope.variable} ${inter.variable} h-full antialiased`} className={`${manrope.variable} ${syne.variable} ${jetbrainsMono.variable} h-full antialiased`}
> >
<body className="min-h-full flex flex-col">{children}</body> <body className="min-h-full flex flex-col">{children}</body>
</html> </html>

View File

@ -6,31 +6,33 @@ const NAV = [
{ {
href: '/new-run', href: '/new-run',
label: 'New Analysis', label: 'New Analysis',
tag: 'RUN',
icon: ( icon: (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="7" cy="7" r="6" stroke="currentColor" strokeWidth="1.2"/> <polygon points="4,3 13,8 4,13" fill="currentColor" opacity=".9"/>
<path d="M7 4v3l2 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/>
</svg> </svg>
), ),
}, },
{ {
href: '/history', href: '/history',
label: 'Run History', label: 'Run History',
tag: 'LOG',
icon: ( icon: (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="1" y="2" width="12" height="2" rx="1" fill="currentColor" opacity=".9"/> <rect x="2" y="2" width="12" height="2.5" rx="1.25" fill="currentColor" opacity=".9"/>
<rect x="1" y="6" width="8" height="2" rx="1" fill="currentColor" opacity=".6"/> <rect x="2" y="6.75" width="8" height="2.5" rx="1.25" fill="currentColor" opacity=".65"/>
<rect x="1" y="10" width="10" height="2" rx="1" fill="currentColor" opacity=".75"/> <rect x="2" y="11.5" width="10" height="2.5" rx="1.25" fill="currentColor" opacity=".8"/>
</svg> </svg>
), ),
}, },
{ {
href: '/settings', href: '/settings',
label: 'Settings', label: 'Settings',
tag: 'CFG',
icon: ( icon: (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="7" cy="7" r="2.2" stroke="currentColor" strokeWidth="1.2"/> <circle cx="8" cy="8" r="2.5" stroke="currentColor" strokeWidth="1.4"/>
<path d="M7 1v1.5M7 11.5V13M1 7h1.5M11.5 7H13M2.5 2.5l1 1M10.5 10.5l1 1M11.5 2.5l-1 1M3.5 10.5l-1 1" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/> <path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.05 3.05l1.42 1.42M11.53 11.53l1.42 1.42M12.95 3.05l-1.42 1.42M4.47 11.53l-1.42 1.42" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
</svg> </svg>
), ),
}, },
@ -41,70 +43,105 @@ export default function Sidebar() {
return ( return (
<aside <aside
className="w-[220px] min-h-screen flex flex-col shrink-0" className="w-[240px] min-h-screen flex flex-col shrink-0 relative"
style={{ style={{
background: 'var(--bg-sidebar)', background: 'var(--bg-sidebar)',
borderRight: '1px solid var(--border)', borderRight: '1px solid var(--border)',
}} }}
> >
{/* Logo */} {/* Subtle dot grid texture */}
<div <div
className="px-5 py-5" className="absolute inset-0 pointer-events-none"
style={{ borderBottom: '1px solid var(--border)' }} style={{
> backgroundImage: 'radial-gradient(circle, rgba(80,80,200,0.08) 1px, transparent 1px)',
<div className="flex items-center gap-2.5"> backgroundSize: '20px 20px',
{/* Geometric logo mark */} }}
/>
{/* Top glow */}
<div
className="absolute top-0 left-0 right-0 h-px pointer-events-none"
style={{
background: 'linear-gradient(90deg, transparent 10%, var(--accent) 50%, transparent 90%)',
opacity: 0.3,
}}
/>
{/* Logo */}
<div className="relative px-5 pt-6 pb-5" style={{ borderBottom: '1px solid var(--border)' }}>
<div className="flex items-center gap-3">
{/* Logo mark */}
<div <div
className="relative w-7 h-7 rounded-lg shrink-0 flex items-center justify-center" className="relative w-9 h-9 rounded-xl shrink-0 flex items-center justify-center overflow-hidden"
style={{ style={{
background: 'var(--accent-dim)', background: 'var(--accent-dim)',
border: '1px solid var(--accent)', border: '1px solid rgba(0,196,232,0.35)',
boxShadow: '0 0 20px rgba(0,196,232,0.15), inset 0 1px 0 rgba(255,255,255,0.06)',
}} }}
> >
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"> <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<polyline points="1,10 4,6 7,8 10,4 13,2" stroke="var(--accent-light)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none"/> <polyline
<circle cx="13" cy="2" r="1.2" fill="var(--accent-light)"/> points="1,13 5,8 9,10 13,5 17,2"
stroke="var(--accent)"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx="17" cy="2" r="1.5" fill="var(--accent-light)"/>
</svg> </svg>
{/* Inner glow */}
<div
className="absolute inset-0"
style={{
background: 'radial-gradient(ellipse at 50% 0%, rgba(0,196,232,0.15) 0%, transparent 60%)',
}}
/>
</div> </div>
<div> <div>
<div <div
className="text-sm font-bold tracking-tight leading-none" className="font-bold leading-none tracking-tight"
style={{ color: 'var(--text-high)', fontFamily: 'var(--font-manrope)' }} style={{
fontFamily: 'var(--font-syne)',
fontSize: '15px',
color: 'var(--text-high)',
letterSpacing: '-0.02em',
}}
> >
TradingAgents TradingAgents
</div> </div>
<div <div
className="text-[9px] mt-0.5 font-medium tracking-widest uppercase" className="mt-1 flex items-center gap-1.5"
style={{ color: 'var(--text-low)' }} style={{ color: 'var(--accent)', fontFamily: 'var(--font-mono)', fontSize: '9px', letterSpacing: '0.12em' }}
> >
Multi-Agent AI <span
className="w-1.5 h-1.5 rounded-full inline-block"
style={{ background: 'var(--buy)', boxShadow: '0 0 5px var(--buy)', animation: 'shimmer 2s ease-in-out infinite' }}
/>
MULTI-AGENT AI
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Nav section */} {/* Nav */}
<div className="px-2.5 pt-4 flex-1"> <div className="relative px-3 pt-5 flex-1">
<div <div className="apex-label px-2 mb-3">Workspace</div>
className="apex-label px-2.5 mb-2"
>
Navigation
</div>
<nav className="flex flex-col gap-0.5"> <nav className="flex flex-col gap-0.5">
{NAV.map(({ href, label, icon }) => { {NAV.map(({ href, label, tag, icon }) => {
const active = path === href || path.startsWith(href + '/') const active = path === href || path.startsWith(href + '/')
return ( return (
<Link <Link
key={href} key={href}
href={href} href={href}
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[13px] font-medium transition-all duration-150" className="group relative flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200"
style={ style={
active active
? { ? {
background: 'var(--accent-glow)', background: 'var(--accent-glow)',
color: 'var(--accent-light)', color: 'var(--accent-light)',
borderLeft: '2px solid var(--accent)',
paddingLeft: '9px',
} }
: { : {
color: 'var(--text-mid)', color: 'var(--text-mid)',
@ -123,30 +160,86 @@ export default function Sidebar() {
} }
}} }}
> >
<span className="shrink-0 opacity-70">{icon}</span> {/* Active indicator */}
<span style={{ fontFamily: 'var(--font-manrope)' }}>{label}</span> {active && (
<div
className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 rounded-r"
style={{
height: '60%',
background: 'var(--accent)',
boxShadow: '0 0 8px var(--accent)',
}}
/>
)}
{/* Icon */}
<span
className="shrink-0 w-4 h-4 flex items-center justify-center transition-opacity"
style={{ opacity: active ? 1 : 0.5 }}
>
{icon}
</span>
{/* Label */}
<span
className="flex-1 text-[13px] font-medium"
style={{ fontFamily: 'var(--font-manrope)' }}
>
{label}
</span>
{/* Tag */}
<span
className="shrink-0 px-1.5 py-0.5 rounded text-[9px] font-bold"
style={{
fontFamily: 'var(--font-mono)',
letterSpacing: '0.08em',
background: active ? 'rgba(0,196,232,0.15)' : 'var(--bg-elevated)',
color: active ? 'var(--accent)' : 'var(--text-low)',
border: `1px solid ${active ? 'rgba(0,196,232,0.25)' : 'var(--border)'}`,
}}
>
{tag}
</span>
</Link> </Link>
) )
})} })}
</nav> </nav>
</div> </div>
{/* Divider */}
<div className="mx-3 h-px" style={{ background: 'var(--border)' }} />
{/* Footer */} {/* Footer */}
<div <div className="relative px-5 py-4">
className="px-5 py-4" <div className="flex items-center gap-2.5">
style={{ borderTop: '1px solid var(--border)' }}
>
<div className="flex items-center gap-2">
<div <div
className="w-1.5 h-1.5 rounded-full" className="w-6 h-6 rounded-lg flex items-center justify-center shrink-0"
style={{ background: 'var(--buy)', boxShadow: '0 0 4px var(--buy)' }} style={{ background: 'var(--bg-elevated)', border: '1px solid var(--border-raised)' }}
/>
<span
className="text-[10px] font-medium"
style={{ color: 'var(--text-low)', fontFamily: 'var(--font-manrope)' }}
> >
Local · Development <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
</span> <rect x="1" y="1" width="10" height="10" rx="2" stroke="var(--text-low)" strokeWidth="1.2"/>
<circle cx="6" cy="6" r="1.5" fill="var(--text-low)"/>
</svg>
</div>
<div>
<div
className="text-[11px] font-semibold"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}
>
Local Instance
</div>
<div
className="text-[9px] flex items-center gap-1"
style={{ color: 'var(--text-low)', fontFamily: 'var(--font-mono)', letterSpacing: '0.06em' }}
>
<span
className="w-1 h-1 rounded-full inline-block"
style={{ background: 'var(--buy)', animation: 'shimmer 3s ease-in-out infinite' }}
/>
DEV · PORT 8000
</div>
</div>
</div> </div>
</div> </div>
</aside> </aside>

View File

@ -5,118 +5,203 @@ type Props = { runs: RunSummary[] }
function DecisionBadge({ decision }: { decision: string }) { function DecisionBadge({ decision }: { decision: string }) {
const lower = decision.toLowerCase() const lower = decision.toLowerCase()
if (lower === 'buy') return <span className="badge-buy">{decision}</span> if (lower === 'buy') return <span className="badge-buy">{decision}</span>
if (lower === 'sell') return <span className="badge-sell">{decision}</span> if (lower === 'sell') return <span className="badge-sell">{decision}</span>
if (lower === 'hold') return <span className="badge-hold">{decision}</span> if (lower === 'hold') return <span className="badge-hold">{decision}</span>
return ( return (
<span <span
className="px-2.5 py-1 rounded-full text-xs font-semibold" className="px-2.5 py-1 rounded-full text-[10px] font-bold"
style={{ backgroundColor: 'var(--bg-elevated)', color: 'var(--text-mid)' }} style={{
fontFamily: 'var(--font-mono)',
letterSpacing: '0.08em',
background: 'var(--bg-elevated)',
color: 'var(--text-mid)',
border: '1px solid var(--border-raised)',
}}
> >
{decision} {decision}
</span> </span>
) )
} }
function StatusDot({ decision }: { decision?: string }) {
const lower = decision?.toLowerCase()
const color = lower === 'buy' ? 'var(--buy)' : lower === 'sell' ? 'var(--sell)' : lower === 'hold' ? 'var(--hold)' : 'var(--text-low)'
return (
<div
className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ background: color, boxShadow: decision ? `0 0 5px ${color}80` : 'none' }}
/>
)
}
export default function RunHistoryTable({ runs }: Props) { export default function RunHistoryTable({ runs }: Props) {
if (runs.length === 0) { if (runs.length === 0) {
return ( return (
<div <div
className="rounded-lg px-6 py-12 text-center" className="rounded-2xl flex flex-col items-center justify-center py-20 gap-4"
style={{ backgroundColor: 'var(--bg-card)' }} style={{ background: 'var(--bg-card)', border: '1px solid var(--border)' }}
> >
<p className="text-sm" style={{ color: 'var(--text-low)' }}> {/* Empty state icon */}
No runs yet. Start a new analysis. <div
</p> className="w-14 h-14 rounded-2xl flex items-center justify-center"
style={{ background: 'var(--bg-elevated)', border: '1px solid var(--border-raised)' }}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect x="3" y="3" width="18" height="4" rx="2" fill="var(--text-low)"/>
<rect x="3" y="10" width="12" height="4" rx="2" fill="var(--text-low)" opacity=".6"/>
<rect x="3" y="17" width="15" height="4" rx="2" fill="var(--text-low)" opacity=".4"/>
</svg>
</div>
<div className="text-center">
<p
className="text-sm font-medium mb-1"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}
>
No analysis runs yet
</p>
<p className="text-xs" style={{ color: 'var(--text-low)' }}>
Start your first analysis to see results here
</p>
</div>
<Link href="/new-run" className="btn-primary mt-2">
<svg width="11" height="11" viewBox="0 0 11 11" fill="none">
<polygon points="2.5,2 9,5.5 2.5,9" fill="currentColor"/>
</svg>
New Analysis
</Link>
</div> </div>
) )
} }
return ( return (
<div <div
className="rounded-lg overflow-hidden" className="overflow-hidden"
style={{ backgroundColor: 'var(--bg-card)', border: '1px solid var(--border)' }} style={{
background: 'var(--bg-card)',
border: '1px solid var(--border)',
borderRadius: '14px',
}}
> >
<table className="w-full text-sm"> {/* Table header */}
<thead> <div
<tr style={{ backgroundColor: 'var(--bg-elevated)' }}> className="grid gap-0"
<th style={{
className="apex-label px-5 py-3 text-left" display: 'grid',
style={{ fontFamily: 'var(--font-manrope)' }} gridTemplateColumns: '1fr 1fr 100px 160px 80px',
> background: 'var(--bg-elevated)',
Ticker borderBottom: '1px solid var(--border)',
</th> padding: '0 20px',
<th }}
className="apex-label px-5 py-3 text-left" >
style={{ fontFamily: 'var(--font-manrope)' }} {['Ticker', 'Date', 'Decision', 'Created', ''].map((h) => (
> <div
Date key={h}
</th> className="apex-label py-3"
<th >
className="apex-label px-5 py-3 text-left" {h}
style={{ fontFamily: 'var(--font-manrope)' }} </div>
> ))}
Decision </div>
</th>
<th {/* Rows */}
className="apex-label px-5 py-3 text-left" <div className="divide-y" style={{ borderColor: 'var(--border)' }}>
style={{ fontFamily: 'var(--font-manrope)' }} {runs.map((run, idx) => (
> <div
Created key={run.id}
</th> className="group transition-colors duration-100 animate-fade-up"
<th className="px-5 py-3" /> style={{
</tr> display: 'grid',
</thead> gridTemplateColumns: '1fr 1fr 100px 160px 80px',
<tbody> padding: '0 20px',
{runs.map((run) => ( alignItems: 'center',
<tr animationDelay: `${idx * 30}ms`,
key={run.id} }}
className="transition-colors duration-100" onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-hover)')}
style={{ borderTop: '1px solid var(--border)' }} onMouseLeave={(e) => (e.currentTarget.style.background = '')}
onMouseEnter={(e) => >
(e.currentTarget.style.backgroundColor = 'var(--bg-hover)') {/* Ticker */}
} <div className="py-4 flex items-center gap-2.5">
onMouseLeave={(e) => <StatusDot decision={run.decision} />
(e.currentTarget.style.backgroundColor = '') <span
} className="terminal-text font-bold text-sm tracking-wider"
>
<td
className="px-5 py-4 font-mono font-semibold tracking-wider"
style={{ color: 'var(--text-high)' }} style={{ color: 'var(--text-high)' }}
> >
{run.ticker} {run.ticker}
</td> </span>
<td className="px-5 py-4" style={{ color: 'var(--text-mid)' }}> </div>
{run.date}
</td> {/* Date */}
<td className="px-5 py-4"> <div
{run.decision ? ( className="py-4 terminal-text text-sm"
<DecisionBadge decision={run.decision} /> style={{ color: 'var(--text-mid)', letterSpacing: '0.02em' }}
) : ( >
<span style={{ color: 'var(--text-low)' }}></span> {run.date}
)} </div>
</td>
<td className="px-5 py-4 text-xs" style={{ color: 'var(--text-mid)' }}> {/* Decision */}
{new Date(run.created_at).toLocaleString()} <div className="py-4">
</td> {run.decision ? (
<td className="px-5 py-4"> <DecisionBadge decision={run.decision} />
<Link ) : (
href={`/runs/${run.id}`} <span
className="text-xs font-medium transition-colors" className="text-xs font-bold"
style={{ color: 'var(--accent)' }} style={{ fontFamily: 'var(--font-mono)', color: 'var(--text-low)', letterSpacing: '0.08em' }}
onMouseEnter={(e) =>
(e.currentTarget.style.color = 'var(--accent-light)')
}
onMouseLeave={(e) =>
(e.currentTarget.style.color = 'var(--accent)')
}
> >
View Report PENDING
</Link> </span>
</td> )}
</tr> </div>
))}
</tbody> {/* Created */}
</table> <div
className="py-4 text-xs terminal-text"
style={{ color: 'var(--text-low)', letterSpacing: '0.02em' }}
>
{new Date(run.created_at).toLocaleString()}
</div>
{/* Action */}
<div className="py-4">
<Link
href={`/runs/${run.id}`}
className="inline-flex items-center gap-1.5 text-xs font-semibold transition-all duration-150 opacity-0 group-hover:opacity-100"
style={{
color: 'var(--accent)',
fontFamily: 'var(--font-manrope)',
letterSpacing: '0.02em',
}}
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.color = 'var(--accent-light)' }}
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.color = 'var(--accent)' }}
>
View
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2.5 6h7M6.5 3l3 3-3 3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</Link>
</div>
</div>
))}
</div>
{/* Footer */}
<div
className="px-5 py-3 flex items-center justify-between"
style={{ borderTop: '1px solid var(--border)', background: 'var(--bg-elevated)' }}
>
<span
className="text-[10px] font-bold"
style={{ fontFamily: 'var(--font-mono)', color: 'var(--text-low)', letterSpacing: '0.1em' }}
>
{runs.length} RUN{runs.length !== 1 ? 'S' : ''} TOTAL
</span>
<span
className="text-[10px]"
style={{ fontFamily: 'var(--font-mono)', color: 'var(--text-low)', letterSpacing: '0.06em' }}
>
TRADINGAGENTS · LOCAL
</span>
</div>
</div> </div>
) )
} }

View File

@ -6,28 +6,56 @@ const ANALYSTS = [
label: 'Market', label: 'Market',
full: 'Market Analyst', full: 'Market Analyst',
desc: 'Price action & technicals', desc: 'Price action & technicals',
dot: '#4480FF', accent: '#00C4E8',
icon: (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<polyline points="1,14 5,9 8,11 12,6 17,3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
<circle cx="17" cy="3" r="1.4" fill="currentColor"/>
</svg>
),
}, },
{ {
id: 'news', id: 'news',
label: 'News', label: 'News',
full: 'News Analyst', full: 'News Analyst',
desc: 'Sentiment & headlines', desc: 'Sentiment & headlines',
dot: '#A78BFA', accent: '#A78BFA',
icon: (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<rect x="2" y="3" width="14" height="3" rx="1.5" fill="currentColor" opacity=".9"/>
<rect x="2" y="8" width="10" height="2.5" rx="1.25" fill="currentColor" opacity=".65"/>
<rect x="2" y="12.5" width="12" height="2.5" rx="1.25" fill="currentColor" opacity=".8"/>
</svg>
),
}, },
{ {
id: 'fundamentals', id: 'fundamentals',
label: 'Fundamentals', label: 'Fundamentals',
full: 'Fundamentals Analyst', full: 'Fundamentals Analyst',
desc: 'Earnings & financials', desc: 'Earnings & financials',
dot: '#00CE68', accent: '#00E078',
icon: (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<rect x="3" y="11" width="3" height="5" rx="1" fill="currentColor" opacity=".7"/>
<rect x="7.5" y="7" width="3" height="9" rx="1" fill="currentColor" opacity=".85"/>
<rect x="12" y="3" width="3" height="13" rx="1" fill="currentColor"/>
</svg>
),
}, },
{ {
id: 'social', id: 'social',
label: 'Social', label: 'Social',
full: 'Social Analyst', full: 'Social Analyst',
desc: 'Social media signals', desc: 'Social media signals',
dot: '#F59E0B', accent: '#FFB400',
icon: (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="5" r="2.5" stroke="currentColor" strokeWidth="1.4"/>
<circle cx="3.5" cy="12" r="2" stroke="currentColor" strokeWidth="1.4" opacity=".7"/>
<circle cx="14.5" cy="12" r="2" stroke="currentColor" strokeWidth="1.4" opacity=".7"/>
<path d="M5.5 11C6.5 8.5 11.5 8.5 12.5 11" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" opacity=".5"/>
</svg>
),
}, },
] ]
@ -43,39 +71,63 @@ export default function AnalystSelector({ selected, onChange }: Props) {
return ( return (
<div className="grid grid-cols-2 gap-2.5 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-2.5 sm:grid-cols-4">
{ANALYSTS.map(({ id, label, desc, dot, full }) => { {ANALYSTS.map(({ id, label, desc, accent, icon, full }) => {
const active = selected.includes(id) const active = selected.includes(id)
return ( return (
<button <button
key={id} key={id}
type="button" type="button"
title={full} title={full}
onClick={() => toggle(id)} onClick={() => toggle(id)}
className="relative p-4 text-left transition-all duration-200" className="relative flex flex-col items-start p-4 text-left transition-all duration-200"
style={{ style={{
background: active ? 'var(--bg-active)' : 'var(--bg-elevated)', background: active
border: active ? `1px solid ${dot}40` : '1px solid var(--border)', ? `linear-gradient(145deg, ${accent}0F 0%, ${accent}06 100%)`
borderTop: active ? `2px solid ${dot}` : '1px solid var(--border)', : 'var(--bg-elevated)',
borderRadius: '10px', border: `1px solid ${active ? accent + '35' : 'var(--border-raised)'}`,
borderRadius: '12px',
transform: active ? 'none' : 'none',
boxShadow: active ? `0 0 20px ${accent}12, 0 0 0 1px ${accent}20` : 'none',
}}
onMouseEnter={(e) => {
if (!active) {
(e.currentTarget as HTMLElement).style.background = 'var(--bg-hover)'
;(e.currentTarget as HTMLElement).style.borderColor = `${accent}20`
}
}}
onMouseLeave={(e) => {
if (!active) {
(e.currentTarget as HTMLElement).style.background = 'var(--bg-elevated)'
;(e.currentTarget as HTMLElement).style.borderColor = 'var(--border-raised)'
}
}} }}
> >
{/* Color dot */} {/* Icon */}
<div <div
className="w-2 h-2 rounded-full mb-3" className="mb-3 w-9 h-9 rounded-xl flex items-center justify-center transition-all duration-200"
style={{ style={{
background: dot, background: active ? `${accent}18` : 'var(--bg-active)',
boxShadow: active ? `0 0 6px ${dot}80` : 'none', border: `1px solid ${active ? accent + '30' : 'var(--border)'}`,
color: active ? accent : 'var(--text-low)',
boxShadow: active ? `0 0 12px ${accent}20` : 'none',
}} }}
/> >
{icon}
</div>
{/* Check */} {/* Checkmark */}
{active && ( {active && (
<div <div
className="absolute top-3 right-3 w-4 h-4 rounded-full flex items-center justify-center" className="absolute top-3 right-3 w-4 h-4 rounded-full flex items-center justify-center"
style={{ background: dot, opacity: 0.9 }} style={{
background: accent,
boxShadow: `0 0 8px ${accent}60`,
animation: 'step-complete 0.35s cubic-bezier(0.16,1,0.3,1) both',
}}
> >
<svg width="8" height="8" viewBox="0 0 8 8" fill="none"> <svg width="8" height="8" viewBox="0 0 8 8" fill="none">
<polyline points="1.5,4 3,5.5 6.5,2" stroke="white" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"/> <polyline points="1.5,4 3,5.5 6.5,2" stroke="var(--bg-base)" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
</div> </div>
)} )}
@ -83,15 +135,19 @@ export default function AnalystSelector({ selected, onChange }: Props) {
<div <div
className="text-sm font-semibold mb-0.5" className="text-sm font-semibold mb-0.5"
style={{ style={{
color: active ? 'var(--text-high)' : 'var(--text-mid)', color: active ? 'var(--text-high)' : 'var(--text-mid)',
fontFamily: 'var(--font-manrope)', fontFamily: 'var(--font-manrope)',
}} }}
> >
{label} {label}
</div> </div>
<div <div
className="text-[11px] leading-snug" className="text-[10px] leading-snug"
style={{ color: 'var(--text-low)' }} style={{
color: active ? `${accent}B0` : 'var(--text-low)',
fontFamily: 'var(--font-mono)',
letterSpacing: '0.03em',
}}
> >
{desc} {desc}
</div> </div>

View File

@ -5,34 +5,75 @@ import { useRunSubmit } from '../hooks/useRunSubmit'
import { DEFAULT_FORM } from '../types' import { DEFAULT_FORM } from '../types'
import type { NewRunFormState } from '../types' import type { NewRunFormState } from '../types'
function SectionHeader({ step, title, subtitle }: { step: number; title: string; subtitle?: string }) { function SectionCard({
step,
title,
subtitle,
children,
}: {
step: number
title: string
subtitle?: string
children: React.ReactNode
}) {
return ( return (
<div className="flex items-start gap-3.5 mb-5"> <section
className="overflow-hidden"
style={{
background: 'var(--bg-card)',
border: '1px solid var(--border)',
borderRadius: '14px',
}}
>
{/* Section header */}
<div <div
className="w-6 h-6 rounded-full flex items-center justify-center text-[11px] font-bold shrink-0 mt-0.5" className="px-5 py-3.5 flex items-center gap-3"
style={{ style={{ borderBottom: '1px solid var(--border)', background: 'var(--bg-elevated)' }}
background: 'var(--accent-dim, #1A3A88)',
color: 'var(--accent-light)',
border: '1px solid var(--accent)',
fontFamily: 'var(--font-manrope)',
}}
> >
{step}
</div>
<div>
<div <div
className="text-sm font-semibold" className="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold shrink-0"
style={{ color: 'var(--text-high)', fontFamily: 'var(--font-manrope)' }} style={{
background: 'var(--accent-dim)',
color: 'var(--accent-light)',
border: '1px solid rgba(0,196,232,0.30)',
fontFamily: 'var(--font-mono)',
letterSpacing: '-0.01em',
}}
> >
{title} {String(step).padStart(2, '0')}
</div> </div>
{subtitle && ( <div className="flex-1 min-w-0">
<div className="text-[11px] mt-0.5" style={{ color: 'var(--text-low)' }}> <div
{subtitle} className="text-sm font-semibold"
style={{ color: 'var(--text-high)', fontFamily: 'var(--font-manrope)' }}
>
{title}
</div> </div>
)} {subtitle && (
<div
className="text-[10px] mt-0.5"
style={{ color: 'var(--text-low)', fontFamily: 'var(--font-mono)', letterSpacing: '0.03em' }}
>
{subtitle}
</div>
)}
</div>
</div> </div>
</div>
{/* Section body */}
<div className="p-5">{children}</div>
</section>
)
}
function FieldLabel({ children }: { children: React.ReactNode }) {
return (
<label
className="block mb-1.5 text-[10px] font-bold uppercase tracking-widest"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-mono)', letterSpacing: '0.1em' }}
>
{children}
</label>
) )
} }
@ -47,92 +88,66 @@ export default function RunConfigForm() {
onSubmit={(e) => { e.preventDefault(); submit(form) }} onSubmit={(e) => { e.preventDefault(); submit(form) }}
className="space-y-3" className="space-y-3"
> >
{/* Error banner */} {/* Error */}
{error && ( {error && (
<div <div
className="px-4 py-3 rounded-lg text-sm" className="px-4 py-3 rounded-xl text-sm flex items-center gap-2.5"
style={{ style={{
background: 'var(--error-bg)', background: 'var(--error-bg)',
color: 'var(--error)', color: 'var(--error)',
border: '1px solid rgba(255,68,68,0.25)', border: '1px solid rgba(255,43,62,0.25)',
}} }}
> >
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<circle cx="7" cy="7" r="6" stroke="currentColor" strokeWidth="1.4"/>
<path d="M7 4v4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
<circle cx="7" cy="10" r="0.8" fill="currentColor"/>
</svg>
{error} {error}
</div> </div>
)} )}
{/* ── Section 1: Target ──────────────────────────────────────── */} {/* ── Section 1: Target ─────────────────────────────────────── */}
<section <SectionCard
className="p-5" step={1}
style={{ title="Analysis Target"
background: 'var(--bg-card)', subtitle="Select the security and trade date"
border: '1px solid var(--border)',
borderRadius: '10px',
}}
> >
<SectionHeader
step={1}
title="Analysis Target"
subtitle="Choose the security and date for the analysis"
/>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label <FieldLabel>Ticker Symbol</FieldLabel>
className="block text-[11px] font-medium mb-1.5"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}
>
Ticker Symbol
</label>
<input <input
className="vault-input font-mono text-sm font-semibold tracking-wider" className="vault-input terminal-text font-bold text-sm tracking-widest"
placeholder="e.g. NVDA" placeholder="e.g. NVDA"
value={form.ticker} value={form.ticker}
onChange={(e) => set('ticker', e.target.value.toUpperCase())} onChange={(e) => set('ticker', e.target.value.toUpperCase())}
required required
style={{ letterSpacing: '0.12em' }}
/> />
</div> </div>
<div> <div>
<label <FieldLabel>Trade Date</FieldLabel>
htmlFor="trade-date"
className="block text-[11px] font-medium mb-1.5"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}
>
Trade Date
</label>
<input <input
id="trade-date" id="trade-date"
type="date" type="date"
className="vault-input" className="vault-input terminal-text text-sm"
value={form.date} value={form.date}
onChange={(e) => set('date', e.target.value)} onChange={(e) => set('date', e.target.value)}
required required
/> />
</div> </div>
</div> </div>
</section> </SectionCard>
{/* ── Section 2: Model ───────────────────────────────────────── */} {/* ── Section 2: Model ──────────────────────────────────────── */}
<section <SectionCard
className="p-5" step={2}
style={{ title="Model Configuration"
background: 'var(--bg-card)', subtitle="LLM provider and reasoning models"
border: '1px solid var(--border)',
borderRadius: '10px',
}}
> >
<SectionHeader
step={2}
title="Model Configuration"
subtitle="Select your LLM provider and reasoning models"
/>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2">
<label <FieldLabel>LLM Provider</FieldLabel>
className="block text-[11px] font-medium mb-1.5"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}
>
LLM Provider
</label>
<select <select
className="vault-input" className="vault-input"
value={form.llm_provider} value={form.llm_provider}
@ -143,101 +158,79 @@ export default function RunConfigForm() {
<option value="google">Google</option> <option value="google">Google</option>
</select> </select>
</div> </div>
<div />
<div> <div>
<label <FieldLabel>Deep Think LLM</FieldLabel>
className="block text-[11px] font-medium mb-1.5"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}
>
Deep Think LLM
</label>
<input <input
className="vault-input text-[13px]" className="vault-input terminal-text text-[12px]"
value={form.deep_think_llm} value={form.deep_think_llm}
onChange={(e) => set('deep_think_llm', e.target.value)} onChange={(e) => set('deep_think_llm', e.target.value)}
/> />
</div> </div>
<div> <div>
<label <FieldLabel>Quick Think LLM</FieldLabel>
className="block text-[11px] font-medium mb-1.5"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}
>
Quick Think LLM
</label>
<input <input
className="vault-input text-[13px]" className="vault-input terminal-text text-[12px]"
value={form.quick_think_llm} value={form.quick_think_llm}
onChange={(e) => set('quick_think_llm', e.target.value)} onChange={(e) => set('quick_think_llm', e.target.value)}
/> />
</div> </div>
<div> <div>
<label <FieldLabel>Debate Rounds</FieldLabel>
className="block text-[11px] font-medium mb-1.5"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}
>
Debate Rounds
</label>
<input <input
type="number" type="number"
min={1} min={1}
max={5} max={5}
className="vault-input" className="vault-input terminal-text"
value={form.max_debate_rounds} value={form.max_debate_rounds}
onChange={(e) => set('max_debate_rounds', Number(e.target.value))} onChange={(e) => set('max_debate_rounds', Number(e.target.value))}
/> />
</div> </div>
<div> <div>
<label <FieldLabel>Risk Discussion Rounds</FieldLabel>
className="block text-[11px] font-medium mb-1.5"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}
>
Risk Discussion Rounds
</label>
<input <input
type="number" type="number"
min={1} min={1}
max={5} max={5}
className="vault-input" className="vault-input terminal-text"
value={form.max_risk_discuss_rounds} value={form.max_risk_discuss_rounds}
onChange={(e) => set('max_risk_discuss_rounds', Number(e.target.value))} onChange={(e) => set('max_risk_discuss_rounds', Number(e.target.value))}
/> />
</div> </div>
</div> </div>
</section> </SectionCard>
{/* ── Section 3: Analysts ────────────────────────────────────── */} {/* ── Section 3: Analysts ───────────────────────────────────── */}
<section <SectionCard
className="p-5" step={3}
style={{ title="Active Analysts"
background: 'var(--bg-card)', subtitle="Select AI analysts for this run"
border: '1px solid var(--border)',
borderRadius: '10px',
}}
> >
<SectionHeader
step={3}
title="Active Analysts"
subtitle="Select which AI analysts participate in this run"
/>
<AnalystSelector <AnalystSelector
selected={form.enabled_analysts} selected={form.enabled_analysts}
onChange={(v) => set('enabled_analysts', v)} onChange={(v) => set('enabled_analysts', v)}
/> />
</section> </SectionCard>
{/* ── Submit ──────────────────────────────────────────────────── */} {/* ── Submit ────────────────────────────────────────────────── */}
<div className="flex items-center justify-between pt-1"> <div className="flex items-center justify-between pt-1 px-1">
<p <div
className="text-[11px]" className="flex items-center gap-2 text-[10px]"
style={{ color: 'var(--text-low)' }} style={{ color: 'var(--text-low)', fontFamily: 'var(--font-mono)', letterSpacing: '0.04em' }}
> >
Analysis takes 25 minutes depending on model and configuration. <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
</p> <circle cx="5" cy="5" r="4" stroke="currentColor" strokeWidth="1.2"/>
<path d="M5 3v2.5l1.5 1.5" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/>
</svg>
25 min · varies by model
</div>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="btn-primary" className="btn-primary"
style={{ minWidth: '150px', justifyContent: 'center' }} style={{ minWidth: '160px', justifyContent: 'center' }}
> >
{loading ? ( {loading ? (
<> <>
@ -246,28 +239,17 @@ export default function RunConfigForm() {
height="13" height="13"
viewBox="0 0 13 13" viewBox="0 0 13 13"
fill="none" fill="none"
style={{ animation: 'spin-slow 0.8s linear infinite' }} style={{ animation: 'spin-slow 0.7s linear infinite' }}
> >
<circle <circle cx="6.5" cy="6.5" r="5" stroke="rgba(0,0,0,0.25)" strokeWidth="1.5"/>
cx="6.5" <path d="M6.5 1.5a5 5 0 0 1 5 5" stroke="var(--bg-base)" strokeWidth="1.5" strokeLinecap="round"/>
cy="6.5"
r="5"
stroke="rgba(255,255,255,0.3)"
strokeWidth="1.5"
/>
<path
d="M6.5 1.5a5 5 0 0 1 5 5"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg> </svg>
Starting Starting
</> </>
) : ( ) : (
<> <>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"> <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<polygon points="3,2 10,6 3,10" fill="white"/> <polygon points="3,2 10,6 3,10" fill="var(--bg-base)"/>
</svg> </svg>
Run Analysis Run Analysis
</> </>

View File

@ -9,20 +9,34 @@ const MULTI_TURN_STEPS = new Set<AgentStep>([
'aggressive_analyst', 'conservative_analyst', 'neutral_analyst', 'aggressive_analyst', 'conservative_analyst', 'neutral_analyst',
]) ])
// Color accent per step for visual differentiation
const STEP_ACCENT: Record<AgentStep, string> = { const STEP_ACCENT: Record<AgentStep, string> = {
market_analyst: '#4480FF', market_analyst: '#00C4E8',
news_analyst: '#A78BFA', news_analyst: '#A78BFA',
fundamentals_analyst: '#00CE68', fundamentals_analyst: '#00E078',
social_analyst: '#F59E0B', social_analyst: '#FFB400',
bull_researcher: '#00CE68', bull_researcher: '#00E078',
bear_researcher: '#FF3355', bear_researcher: '#FF1F4C',
research_manager: '#4480FF', research_manager: '#00C4E8',
trader: '#F59E0B', trader: '#FFB400',
aggressive_analyst: '#FF3355', aggressive_analyst: '#FF1F4C',
conservative_analyst: '#4480FF', conservative_analyst: '#00C4E8',
neutral_analyst: '#A78BFA', neutral_analyst: '#A78BFA',
risk_judge: '#F59E0B', risk_judge: '#FFB400',
}
const STEP_ROLE_DESC: Partial<Record<AgentStep, string>> = {
market_analyst: 'Technical & price action',
news_analyst: 'Sentiment & headlines',
fundamentals_analyst: 'Earnings & financials',
social_analyst: 'Social signals',
bull_researcher: 'Bullish thesis',
bear_researcher: 'Bearish thesis',
research_manager: 'Research synthesis',
trader: 'Trade plan',
aggressive_analyst: 'High-risk perspective',
conservative_analyst: 'Risk-averse perspective',
neutral_analyst: 'Balanced view',
risk_judge: 'Final risk decision',
} }
type Props = { type Props = {
@ -35,35 +49,41 @@ export default function AnalystReports({ phase, steps, reports }: Props) {
const phaseSteps = AGENT_STEPS.filter((s) => STEP_PHASE[s] === phase) const phaseSteps = AGENT_STEPS.filter((s) => STEP_PHASE[s] === phase)
return ( return (
<div className="space-y-2.5"> <div className="space-y-3">
{phaseSteps.map((step) => { {phaseSteps.map((step, stepIdx) => {
const stepStatus = steps[step] ?? 'pending' const stepStatus = steps[step] ?? 'pending'
const turns = reports[step] ?? [] const turns = reports[step] ?? []
const isRunning = stepStatus === 'running' const isRunning = stepStatus === 'running'
const isDone = stepStatus === 'done' const isDone = stepStatus === 'done'
const isMulti = MULTI_TURN_STEPS.has(step) const isMulti = MULTI_TURN_STEPS.has(step)
const accent = STEP_ACCENT[step] const accent = STEP_ACCENT[step]
const roleDesc = STEP_ROLE_DESC[step]
return ( return (
<div key={step}> <div key={step} className="animate-fade-up" style={{ animationDelay: `${stepIdx * 40}ms` }}>
{/* ── Completed turns ─────────────────────────────── */} {/* Completed turns */}
{turns.map((report, i) => ( {turns.map((report, i) => (
<div <div
key={`${step}-${i}`} key={`${step}-${i}`}
className="p-5 mb-2" className="mb-2.5 overflow-hidden"
style={{ style={{
background: 'var(--bg-card)', background: 'var(--bg-card)',
border: '1px solid var(--border-raised)', border: '1px solid var(--border-raised)',
borderLeft: `3px solid ${accent}`, borderRadius: '12px',
borderRadius: '10px',
}} }}
> >
{/* Header */} {/* Colored header bar */}
<div className="flex items-center justify-between mb-3"> <div
<div className="flex items-center gap-2"> className="px-4 py-2.5 flex items-center justify-between"
style={{
background: `${accent}0D`,
borderBottom: `1px solid ${accent}20`,
}}
>
<div className="flex items-center gap-2.5">
<div <div
className="w-2 h-2 rounded-full" className="w-2 h-2 rounded-full shrink-0"
style={{ background: accent, flexShrink: 0 }} style={{ background: accent, boxShadow: `0 0 6px ${accent}80` }}
/> />
<span <span
className="text-[13px] font-semibold" className="text-[13px] font-semibold"
@ -71,63 +91,77 @@ export default function AnalystReports({ phase, steps, reports }: Props) {
> >
{AGENT_STEP_LABELS[step]} {AGENT_STEP_LABELS[step]}
</span> </span>
{roleDesc && (
<span
className="text-[10px]"
style={{ color: accent, opacity: 0.7, fontFamily: 'var(--font-mono)', letterSpacing: '0.04em' }}
>
· {roleDesc}
</span>
)}
{isMulti && ( {isMulti && (
<span <span
className="px-1.5 py-0.5 rounded text-[10px] font-medium" className="px-2 py-0.5 rounded text-[9px] font-bold"
style={{ style={{
background: `${accent}18`, background: `${accent}18`,
color: accent, color: accent,
fontFamily: 'var(--font-manrope)', fontFamily: 'var(--font-mono)',
letterSpacing: '0.06em',
}} }}
> >
Turn {i + 1} T{i + 1}
</span> </span>
)} )}
</div> </div>
<div <div
className="flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-medium" className="flex items-center gap-1.5 text-[10px] font-bold"
style={{ style={{
background: 'rgba(0,206,104,0.08)', fontFamily: 'var(--font-mono)',
color: '#00CE68', letterSpacing: '0.08em',
border: '1px solid rgba(0,206,104,0.20)', color: 'var(--buy)',
}} }}
> >
<div className="w-1.5 h-1.5 rounded-full" style={{ background: '#00CE68' }} /> <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
Done <polyline points="2,5 4,7 8,3" stroke="var(--buy)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
DONE
</div> </div>
</div> </div>
{/* Report text */} {/* Report body */}
<p <div className="px-4 py-3">
className="text-sm leading-relaxed line-clamp-5" <p
style={{ color: 'var(--text-mid)', lineHeight: '1.7' }} className="text-sm leading-relaxed line-clamp-5"
> style={{ color: 'var(--text-mid)', lineHeight: '1.75', fontFamily: 'var(--font-manrope)' }}
{report} >
</p> {report}
</p>
</div>
</div> </div>
))} ))}
{/* ── Running spinner ──────────────────────────────── */} {/* Running state */}
{isRunning && ( {isRunning && (
<div <div
className="p-5 mb-2" className="mb-2.5 overflow-hidden"
style={{ style={{
background: 'var(--bg-elevated)', background: 'var(--bg-elevated)',
border: '1px solid var(--border)', border: '1px solid rgba(255,180,0,0.20)',
borderLeft: `3px solid var(--status-running)`, borderRadius: '12px',
borderRadius: '10px',
opacity: 0.9,
}} }}
> >
<div className="flex items-center justify-between mb-3"> <div
<div className="flex items-center gap-2"> className="px-4 py-2.5 flex items-center justify-between"
style={{
background: 'rgba(255,180,0,0.06)',
borderBottom: '1px solid rgba(255,180,0,0.12)',
}}
>
<div className="flex items-center gap-2.5">
<div <div
className="w-2 h-2 rounded-full" className="w-2 h-2 rounded-full shrink-0"
style={{ style={{ background: accent, animation: 'shimmer 0.8s infinite' }}
background: accent,
animation: 'shimmer 1s ease-in-out infinite',
flexShrink: 0,
}}
/> />
<span <span
className="text-[13px] font-semibold" className="text-[13px] font-semibold"
@ -137,86 +171,75 @@ export default function AnalystReports({ phase, steps, reports }: Props) {
</span> </span>
{isMulti && turns.length > 0 && ( {isMulti && turns.length > 0 && (
<span <span
className="px-1.5 py-0.5 rounded text-[10px] font-medium" className="px-2 py-0.5 rounded text-[9px] font-bold"
style={{ style={{
background: `${accent}18`, background: `${accent}18`,
color: accent, color: accent,
fontFamily: 'var(--font-manrope)', fontFamily: 'var(--font-mono)',
letterSpacing: '0.06em',
}} }}
> >
Turn {turns.length + 1} T{turns.length + 1}
</span> </span>
)} )}
</div> </div>
<div <div
className="flex items-center gap-1.5 text-[10px] font-medium" className="flex items-center gap-1.5 text-[10px] font-bold"
style={{ color: 'var(--status-running)', fontFamily: 'var(--font-manrope)' }} style={{ fontFamily: 'var(--font-mono)', letterSpacing: '0.08em', color: 'var(--status-running)' }}
> >
<div <div
className="w-1.5 h-1.5 rounded-full" className="w-1.5 h-1.5 rounded-full"
style={{ style={{ background: 'var(--status-running)', animation: 'shimmer 0.7s infinite' }}
background: 'var(--status-running)',
animation: 'shimmer 1s ease-in-out infinite',
}}
/> />
Analyzing ANALYZING
</div> </div>
</div> </div>
{/* Shimmer lines */} <div className="px-4 py-3 space-y-2">
<div className="space-y-2"> {[85, 62, 44, 30].map((w, i) => (
<div <div
className="h-2.5 rounded animate-shimmer" key={i}
style={{ background: 'var(--border-raised)', width: '85%' }} className="h-2 rounded animate-shimmer"
/> style={{
<div background: 'var(--border-raised)',
className="h-2.5 rounded animate-shimmer" width: `${w}%`,
style={{ background: 'var(--border-raised)', width: '65%', animationDelay: '0.2s' }} animationDelay: `${i * 0.18}s`,
/> }}
<div />
className="h-2.5 rounded animate-shimmer" ))}
style={{ background: 'var(--border-raised)', width: '45%', animationDelay: '0.4s' }}
/>
</div> </div>
</div> </div>
)} )}
{/* ── Pending placeholder ──────────────────────────── */} {/* Pending state */}
{turns.length === 0 && !isRunning && ( {turns.length === 0 && !isRunning && (
<div <div
className="p-5 mb-2" className="mb-2.5 overflow-hidden"
style={{ style={{
background: 'var(--bg-elevated)', background: 'var(--bg-elevated)',
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderLeft: `3px solid var(--border)`, borderRadius: '12px',
borderRadius: '10px', opacity: 0.45,
opacity: 0.5,
}} }}
> >
<div className="flex items-center justify-between mb-3"> <div className="px-4 py-2.5 flex items-center justify-between" style={{ borderBottom: '1px solid var(--border)' }}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2.5">
<div <div className="w-2 h-2 rounded-full shrink-0" style={{ background: 'var(--text-low)' }} />
className="w-2 h-2 rounded-full" <span className="text-[13px] font-medium" style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}>
style={{ background: 'var(--text-low)', flexShrink: 0 }}
/>
<span
className="text-[13px] font-medium"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}
>
{AGENT_STEP_LABELS[step]} {AGENT_STEP_LABELS[step]}
</span> </span>
</div> </div>
<span <span
className="text-[10px]" className="text-[9px] font-bold"
style={{ color: 'var(--text-low)', fontFamily: 'var(--font-manrope)' }} style={{ fontFamily: 'var(--font-mono)', letterSpacing: '0.1em', color: 'var(--text-low)' }}
> >
Queued QUEUED
</span> </span>
</div> </div>
<div <div className="px-4 py-3">
className="h-2 rounded" <div className="h-2 rounded" style={{ background: 'var(--border)', width: '45%' }} />
style={{ background: 'var(--border)', width: '40%' }} </div>
/>
</div> </div>
)} )}
</div> </div>

View File

@ -7,11 +7,11 @@ import AnalystReports from './AnalystReports'
type Phase = 'analysts' | 'researchers' | 'trader' | 'risk' type Phase = 'analysts' | 'researchers' | 'trader' | 'risk'
const TABS: { label: string; phase: Phase; desc: string }[] = [ const TABS: { label: string; phase: Phase; count: string }[] = [
{ label: 'Analysts', phase: 'analysts', desc: '4 agents' }, { label: 'Analysts', phase: 'analysts', count: '4' },
{ label: 'Researchers', phase: 'researchers', desc: '3 agents' }, { label: 'Researchers', phase: 'researchers', count: '3' },
{ label: 'Trader', phase: 'trader', desc: '1 agent' }, { label: 'Trader', phase: 'trader', count: '1' },
{ label: 'Risk', phase: 'risk', desc: '4 agents' }, { label: 'Risk', phase: 'risk', count: '4' },
] ]
type Props = { type Props = {
@ -25,60 +25,93 @@ function getPhaseCompletion(phase: Phase, steps: Record<AgentStep, StepStatus>):
return phaseSteps.length > 0 ? Math.round((done / phaseSteps.length) * 100) : 0 return phaseSteps.length > 0 ? Math.round((done / phaseSteps.length) * 100) : 0
} }
function getPhaseStatus(phase: Phase, steps: Record<AgentStep, StepStatus>): 'done' | 'running' | 'pending' {
const phaseSteps = AGENT_STEPS.filter((s) => STEP_PHASE[s] === phase)
if (phaseSteps.every((s) => steps[s as AgentStep] === 'done')) return 'done'
if (phaseSteps.some((s) => steps[s as AgentStep] === 'running')) return 'running'
return 'pending'
}
export default function PhaseTabs({ steps, reports }: Props) { export default function PhaseTabs({ steps, reports }: Props) {
const [active, setActive] = useState<Phase>('analysts') const [active, setActive] = useState<Phase>('analysts')
return ( return (
<div> <div>
{/* Section label */} {/* Section header */}
<div <div className="flex items-center justify-between mb-4">
className="apex-label mb-3" <div className="apex-label">Agent Reports</div>
> <div
Agent Reports className="text-[10px] font-medium"
style={{ fontFamily: 'var(--font-mono)', color: 'var(--text-low)', letterSpacing: '0.06em' }}
>
{TABS.filter((t) => getPhaseCompletion(t.phase, steps) === 100).length} / {TABS.length} COMPLETE
</div>
</div> </div>
{/* Tab bar */} {/* Tab bar */}
<div <div
className="flex gap-1 p-1 mb-5" className="flex gap-1 p-1 mb-5"
style={{ style={{
background: 'var(--bg-card)', background: 'var(--bg-card)',
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderRadius: '10px', borderRadius: '12px',
width: 'fit-content', width: 'fit-content',
}} }}
> >
{TABS.map(({ label, phase }) => { {TABS.map(({ label, phase, count }) => {
const isActive = active === phase const isActive = active === phase
const completion = getPhaseCompletion(phase, steps) const status = getPhaseStatus(phase, steps)
const allDone = completion === 100 const isDone = status === 'done'
const isRunning = status === 'running'
return ( return (
<button <button
key={phase} key={phase}
onClick={() => setActive(phase)} onClick={() => setActive(phase)}
className="relative flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-[13px] font-medium transition-all duration-150" className="relative flex items-center gap-2 px-4 py-1.5 rounded-lg text-[13px] font-medium transition-all duration-150"
style={ style={
isActive isActive
? { ? {
background: 'var(--accent-glow)', background: 'var(--bg-elevated)',
color: 'var(--accent-light)', color: 'var(--text-high)',
border: '1px solid var(--border-active)', border: '1px solid var(--border-active)',
fontFamily: 'var(--font-manrope)', fontFamily: 'var(--font-manrope)',
boxShadow: '0 1px 8px rgba(0,0,0,0.3)',
} }
: { : {
color: 'var(--text-mid)', color: 'var(--text-mid)',
fontFamily: 'var(--font-manrope)', fontFamily: 'var(--font-manrope)',
} }
} }
> >
{/* Done indicator dot */} {/* Status indicator */}
{allDone && ( {isDone && (
<div <div
className="w-1.5 h-1.5 rounded-full" className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ background: 'var(--buy)', flexShrink: 0 }} style={{ background: 'var(--buy)' }}
/> />
)} )}
{isRunning && (
<div
className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ background: 'var(--status-running)', animation: 'shimmer 1s infinite' }}
/>
)}
{label} {label}
{/* Agent count */}
<span
className="px-1.5 rounded text-[9px] font-bold"
style={{
fontFamily: 'var(--font-mono)',
background: isActive ? 'var(--accent-glow)' : 'var(--bg-elevated)',
color: isActive ? 'var(--accent)' : 'var(--text-low)',
letterSpacing: '0.05em',
}}
>
{count}
</span>
</button> </button>
) )
})} })}

View File

@ -6,11 +6,12 @@ type Phase = 'analysts' | 'researchers' | 'trader' | 'risk'
type Props = { steps: Record<string, StepStatus> } type Props = { steps: Record<string, StepStatus> }
const PHASES: Phase[] = ['analysts', 'researchers', 'trader', 'risk'] const PHASES: Phase[] = ['analysts', 'researchers', 'trader', 'risk']
const PHASE_LABELS: Record<Phase, string> = {
analysts: 'Analysts', researchers: 'Researchers', trader: 'Trader', risk: 'Risk', const PHASE_META: Record<Phase, { label: string; code: string; desc: string }> = {
} analysts: { label: 'Analysis', code: 'PHASE-01', desc: 'Market data & signals' },
const PHASE_NUMS: Record<Phase, string> = { researchers: { label: 'Research', code: 'PHASE-02', desc: 'Bull/bear debate' },
analysts: '01', researchers: '02', trader: '03', risk: '04', trader: { label: 'Trade Plan', code: 'PHASE-03', desc: 'Strategy formulation' },
risk: { label: 'Risk Review', code: 'PHASE-04', desc: 'Risk-adjusted decision' },
} }
function phaseStatus(phase: Phase, steps: Record<string, StepStatus>): StepStatus { function phaseStatus(phase: Phase, steps: Record<string, StepStatus>): StepStatus {
@ -21,112 +22,190 @@ function phaseStatus(phase: Phase, steps: Record<string, StepStatus>): StepStatu
} }
export default function PipelineStepper({ steps }: Props) { export default function PipelineStepper({ steps }: Props) {
const doneCount = PHASES.filter((p) => phaseStatus(p, steps) === 'done').length
const progressPct = (doneCount / PHASES.length) * 100
return ( return (
<div <div
className="px-6 py-5" className="relative overflow-hidden"
style={{ style={{
background: 'var(--bg-card)', background: 'var(--bg-card)',
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderRadius: '10px', borderRadius: '14px',
padding: '20px 24px',
}} }}
> >
<div {/* Header row */}
className="apex-label mb-4" <div className="flex items-center justify-between mb-5">
> <div className="apex-label">Agent Pipeline</div>
Pipeline <div
className="flex items-center gap-2"
style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: 'var(--text-mid)', letterSpacing: '0.06em' }}
>
<span style={{ color: 'var(--accent)' }}>{doneCount}</span>
<span style={{ color: 'var(--text-low)' }}>/</span>
<span>{PHASES.length}</span>
<span style={{ color: 'var(--text-low)', marginLeft: 4 }}>PHASES</span>
</div>
</div> </div>
<div className="flex items-center gap-0">
{/* Progress track */}
<div
className="relative h-1 rounded-full mb-6 overflow-hidden"
style={{ background: 'var(--border-raised)' }}
>
<div
className="absolute left-0 top-0 h-full rounded-full transition-all duration-700"
style={{
width: `${progressPct}%`,
background: 'linear-gradient(90deg, var(--accent-dim), var(--accent))',
boxShadow: '0 0 8px var(--accent-glow)',
}}
>
{/* Moving glow tip */}
{progressPct > 0 && progressPct < 100 && (
<div
className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 rounded-full"
style={{
background: 'var(--accent)',
boxShadow: '0 0 8px var(--accent)',
animation: 'pulse-glow 1.5s ease-in-out infinite',
}}
/>
)}
</div>
{/* Scanline on active */}
{doneCount < PHASES.length && (
<div
className="absolute inset-y-0 w-12"
style={{
background: 'linear-gradient(90deg, transparent, rgba(0,196,232,0.4), transparent)',
animation: 'scan-line 2s ease-in-out infinite',
}}
/>
)}
</div>
{/* Phase nodes */}
<div className="grid grid-cols-4 gap-2">
{PHASES.map((phase, i) => { {PHASES.map((phase, i) => {
const status = phaseStatus(phase, steps) const status = phaseStatus(phase, steps)
const isDone = status === 'done' const isDone = status === 'done'
const isRunning = status === 'running' const isRunning = status === 'running'
const isPending = status === 'pending' const meta = PHASE_META[phase]
return ( return (
<div key={phase} className="flex items-center flex-1 last:flex-none"> <div
{/* Step node */} key={phase}
<div className="flex flex-col items-center gap-2 relative"> className="relative flex flex-col items-center gap-2.5 p-3 rounded-xl transition-all duration-300"
{/* Circle */} style={{
background: isDone
? 'rgba(0,196,232,0.06)'
: isRunning
? 'rgba(255,180,0,0.06)'
: 'var(--bg-elevated)',
border: isDone
? '1px solid rgba(0,196,232,0.20)'
: isRunning
? '1px solid rgba(255,180,0,0.25)'
: '1px solid var(--border)',
}}
>
{/* Node circle */}
<div
className="relative w-10 h-10 rounded-full flex items-center justify-center transition-all duration-500"
style={{
background: isDone
? 'var(--accent)'
: isRunning
? 'var(--bg-hover)'
: 'var(--bg-elevated)',
border: isDone
? '2px solid var(--accent)'
: isRunning
? '2px solid var(--status-running)'
: '1px solid var(--border-raised)',
boxShadow: isDone
? '0 0 20px rgba(0,196,232,0.35)'
: isRunning
? '0 0 16px rgba(255,180,0,0.25)'
: 'none',
animation: isRunning ? 'pulse-glow 2s ease-in-out infinite' : isDone ? 'step-complete 0.4s cubic-bezier(0.16,1,0.3,1) both' : 'none',
}}
>
{isDone ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<polyline
points="3.5,8.5 6.5,11.5 12.5,5"
stroke="var(--bg-base)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) : isRunning ? (
<div
className="w-2.5 h-2.5 rounded-full"
style={{
background: 'var(--status-running)',
animation: 'shimmer 0.8s ease-in-out infinite',
}}
/>
) : (
<span
style={{
fontFamily: 'var(--font-mono)',
fontSize: '11px',
fontWeight: 700,
color: 'var(--text-low)',
letterSpacing: '0.02em',
}}
>
{String(i + 1).padStart(2, '0')}
</span>
)}
</div>
{/* Labels */}
<div className="text-center">
<div <div
className="relative w-9 h-9 rounded-full flex items-center justify-center transition-all duration-500" className="text-[12px] font-semibold leading-tight mb-0.5"
style={{ style={{
background: isDone fontFamily: 'var(--font-manrope)',
? 'var(--accent)' color: isDone
? 'var(--accent-light)'
: isRunning : isRunning
? 'var(--bg-elevated)' ? 'var(--status-running)'
: 'var(--bg-elevated)', : 'var(--text-mid)',
border: isDone
? '2px solid var(--accent)'
: isRunning
? '2px solid var(--status-running)'
: '1px solid var(--status-pending)',
boxShadow: isDone
? '0 0 16px var(--accent-glow)'
: isRunning
? '0 0 12px rgba(245,158,11,0.20)'
: 'none',
animation: isRunning ? 'pulse-glow 2s ease-in-out infinite' : 'none',
}} }}
> >
{isDone ? ( {meta.label}
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<polyline
points="3,7 5.5,9.5 11,4"
stroke="white"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) : isRunning ? (
<div
className="w-2 h-2 rounded-full"
style={{
background: 'var(--status-running)',
animation: 'shimmer 1s ease-in-out infinite',
}}
/>
) : (
<span
className="text-[10px] font-bold"
style={{ color: 'var(--text-low)', fontFamily: 'var(--font-manrope)' }}
>
{PHASE_NUMS[phase]}
</span>
)}
</div> </div>
<div
{/* Label */} className="text-[9px] leading-snug hidden sm:block"
<span
className="text-[10px] font-medium text-center whitespace-nowrap transition-all duration-300"
style={{ style={{
color: isDone ? 'var(--accent-light)' : isRunning ? 'var(--status-running)' : 'var(--text-low)', fontFamily: 'var(--font-mono)',
fontFamily: 'var(--font-manrope)', color: 'var(--text-low)',
letterSpacing: '0.04em', letterSpacing: '0.04em',
}} }}
> >
{PHASE_LABELS[phase]} {meta.desc}
</span> </div>
</div> </div>
{/* Connector */} {/* Phase code */}
{i < PHASES.length - 1 && ( <div
<div className="absolute top-2 right-2 text-[8px] font-bold"
className="flex-1 h-px mx-3 transition-all duration-700 relative overflow-hidden" style={{
style={{ fontFamily: 'var(--font-mono)',
background: isDone ? 'var(--accent)' : 'var(--border)', letterSpacing: '0.05em',
boxShadow: isDone ? '0 0 6px var(--accent-glow)' : 'none', color: isDone ? 'var(--accent)' : 'var(--text-faint)',
marginBottom: '20px', opacity: 0.6,
}} }}
> >
{isRunning && ( {meta.code}
<div </div>
className="absolute inset-y-0 w-1/2 bg-gradient-to-r from-transparent via-yellow-400 to-transparent opacity-60"
style={{ animation: 'scan-line 1.5s ease-in-out infinite' }}
/>
)}
</div>
)}
</div> </div>
) )
})} })}

View File

@ -6,33 +6,41 @@ const VERDICT_CONFIG: Record<Decision, {
color: string color: string
colorBg: string colorBg: string
colorRing: string colorRing: string
colorGlow: string
label: string label: string
sublabel: string sublabel: string
arrow: string symbol: string
description: string
}> = { }> = {
BUY: { BUY: {
color: 'var(--buy)', color: 'var(--buy)',
colorBg: 'var(--buy-bg)', colorBg: 'var(--buy-bg)',
colorRing: 'var(--buy-ring)', colorRing: 'var(--buy-ring)',
label: 'BUY', colorGlow: 'rgba(0, 224, 120, 0.15)',
sublabel: 'Long position recommended', label: 'BUY',
arrow: '↑', sublabel: 'Long Position',
symbol: '↑',
description: 'AI consensus recommends entering a long position',
}, },
SELL: { SELL: {
color: 'var(--sell)', color: 'var(--sell)',
colorBg: 'var(--sell-bg)', colorBg: 'var(--sell-bg)',
colorRing: 'var(--sell-ring)', colorRing: 'var(--sell-ring)',
label: 'SELL', colorGlow: 'rgba(255, 31, 76, 0.15)',
sublabel: 'Exit position recommended', label: 'SELL',
arrow: '↓', sublabel: 'Exit Position',
symbol: '↓',
description: 'AI consensus recommends exiting the position',
}, },
HOLD: { HOLD: {
color: 'var(--hold)', color: 'var(--hold)',
colorBg: 'var(--hold-bg)', colorBg: 'var(--hold-bg)',
colorRing: 'var(--hold-ring)', colorRing: 'var(--hold-ring)',
label: 'HOLD', colorGlow: 'rgba(255, 180, 0, 0.15)',
sublabel: 'Maintain current position', label: 'HOLD',
arrow: '→', sublabel: 'Maintain Position',
symbol: '→',
description: 'AI consensus recommends maintaining current exposure',
}, },
} }
@ -42,99 +50,150 @@ export default function VerdictBanner({ verdict, ticker, date }: Props) {
return ( return (
<div <div
className="relative overflow-hidden animate-fade-up" className="relative overflow-hidden"
style={{ style={{
background: cfg.colorBg, background: cfg.colorBg,
border: `1px solid ${cfg.colorRing}`, border: `1px solid ${cfg.colorRing}`,
borderRadius: '12px', borderRadius: '16px',
padding: '24px 28px', animation: 'verdict-reveal 0.55s cubic-bezier(0.16,1,0.3,1) both',
}} }}
> >
{/* Background glow */} {/* Background glow blobs */}
<div <div
className="absolute inset-0 pointer-events-none" className="absolute inset-0 pointer-events-none"
style={{ style={{
background: `radial-gradient(ellipse at 80% 50%, ${cfg.color}08 0%, transparent 70%)`, background: `radial-gradient(ellipse 70% 80% at 100% 50%, ${cfg.colorGlow} 0%, transparent 65%)`,
}}
/>
<div
className="absolute inset-0 pointer-events-none"
style={{
background: `radial-gradient(ellipse 40% 60% at 0% 50%, ${cfg.colorBg} 0%, transparent 70%)`,
}} }}
/> />
<div className="relative flex items-center justify-between gap-4"> {/* Scanline shimmer on active color */}
{/* Left: metadata */} <div
<div> className="absolute top-0 left-0 right-0 h-px pointer-events-none"
<div style={{
className="apex-label mb-2" background: `linear-gradient(90deg, transparent 5%, ${cfg.color}60 50%, transparent 95%)`,
> }}
/>
{/* Corner decorative mark */}
<div
className="absolute top-4 right-4 opacity-10 pointer-events-none"
style={{ color: cfg.color, fontFamily: 'var(--font-syne)', fontSize: '120px', fontWeight: 800, lineHeight: 1, letterSpacing: '-0.06em' }}
>
{cfg.symbol}
</div>
{/* Main content */}
<div className="relative px-7 py-6">
{/* Top row: label + meta */}
<div className="flex items-center justify-between mb-5">
<div className="apex-label" style={{ color: cfg.color, opacity: 0.7 }}>
Analysis Complete Analysis Complete
</div> </div>
<div <div
className="text-[22px] font-bold tracking-tight mb-1" className="flex items-center gap-2 px-3 py-1 rounded-full text-[10px] font-bold"
style={{ style={{
color: 'var(--text-high)', fontFamily: 'var(--font-mono)',
fontFamily: 'var(--font-manrope)', letterSpacing: '0.1em',
letterSpacing: '-0.03em', background: `${cfg.colorRing}`,
color: cfg.color,
border: `1px solid ${cfg.colorRing}`,
}} }}
> >
{ticker} AI CONSENSUS
</div>
<div
className="text-xs font-mono"
style={{ color: 'var(--text-mid)' }}
>
{date}
</div> </div>
</div> </div>
{/* Divider */} {/* Main verdict row */}
<div <div className="flex items-end justify-between gap-6">
className="hidden sm:block w-px self-stretch" {/* Left: ticker + description */}
style={{ background: cfg.colorRing, opacity: 0.4 }} <div className="flex-1 min-w-0">
/>
{/* Right: verdict */}
<div className="flex items-center gap-5">
<div>
<div <div
className="text-xs font-medium mb-1 text-right" className="flex items-baseline gap-3 mb-2"
style={{ color: cfg.color, opacity: 0.8, fontFamily: 'var(--font-manrope)' }} >
<span
className="terminal-text font-bold"
style={{
fontSize: '42px',
lineHeight: 1,
letterSpacing: '-0.02em',
color: 'var(--text-high)',
}}
>
{ticker}
</span>
<span
className="terminal-text text-sm font-medium"
style={{ color: 'var(--text-mid)', letterSpacing: '0.02em' }}
>
{date}
</span>
</div>
<p
className="text-sm"
style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)', lineHeight: 1.5 }}
>
{cfg.description}
</p>
</div>
{/* Right: big verdict badge */}
<div className="shrink-0 flex flex-col items-center gap-2">
<div
style={{
fontSize: '11px',
fontFamily: 'var(--font-mono)',
fontWeight: 700,
letterSpacing: '0.12em',
color: cfg.color,
opacity: 0.7,
textTransform: 'uppercase',
}}
> >
{cfg.sublabel} {cfg.sublabel}
</div> </div>
<div <div
className="text-right" className="flex items-center justify-center gap-2"
style={{ style={{
color: 'var(--text-low)', padding: '14px 32px',
fontSize: '11px', borderRadius: '12px',
background: `${cfg.colorBg}`,
border: `2px solid ${cfg.color}`,
boxShadow: `0 0 32px ${cfg.colorGlow}, 0 0 64px ${cfg.colorBg}, inset 0 1px 0 rgba(255,255,255,0.06)`,
}} }}
> >
AI consensus decision <span
className="terminal-text"
style={{
fontSize: '48px',
fontWeight: 700,
lineHeight: 1,
color: cfg.color,
letterSpacing: '-0.02em',
}}
>
{cfg.symbol}
</span>
<span
style={{
fontFamily: 'var(--font-syne)',
fontSize: '48px',
fontWeight: 800,
lineHeight: 1,
color: cfg.color,
letterSpacing: '-0.02em',
textShadow: `0 0 30px ${cfg.color}80`,
}}
>
{cfg.label}
</span>
</div> </div>
</div> </div>
{/* Big verdict */}
<div
className="flex items-center gap-2 px-5 py-3 rounded-xl"
style={{
background: `${cfg.colorRing}`,
border: `1px solid ${cfg.colorRing}`,
}}
>
<span
className="text-2xl font-bold leading-none"
style={{ color: cfg.color }}
>
{cfg.arrow}
</span>
<span
className="text-2xl font-bold tracking-tight leading-none"
style={{
color: cfg.color,
fontFamily: 'var(--font-manrope)',
letterSpacing: '0.04em',
}}
>
{cfg.label}
</span>
</div>
</div> </div>
</div> </div>
</div> </div>

14
ui/package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "ui", "name": "ui",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0",
"next": "16.2.1", "next": "16.2.1",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4" "react-dom": "19.2.4"
@ -4459,7 +4460,6 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -7827,7 +7827,6 @@
"version": "1.32.0", "version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"detect-libc": "^2.0.3" "detect-libc": "^2.0.3"
@ -7860,7 +7859,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -7881,7 +7879,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -7902,7 +7899,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -7923,7 +7919,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -7944,7 +7939,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -7965,7 +7959,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -7986,7 +7979,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -8007,7 +7999,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -8028,7 +8019,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -8049,7 +8039,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -8070,7 +8059,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [

View File

@ -9,6 +9,7 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0",
"next": "16.2.1", "next": "16.2.1",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4" "react-dom": "19.2.4"