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() {
const { runs, loading, error } = useRunHistory()
return (
<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 className="apex-label mb-3" style={{ color: 'var(--accent)', opacity: 0.7 }}>
Execution Log
</div>
<h1
className="text-2xl font-bold mb-1"
style={{ color: 'var(--text-high)', fontFamily: 'var(--font-manrope)' }}
style={{
fontFamily: 'var(--font-syne)',
fontSize: '32px',
fontWeight: 800,
letterSpacing: '-0.04em',
color: 'var(--text-high)',
lineHeight: 1.1,
marginBottom: '8px',
}}
>
Run History
</h1>
@ -19,21 +31,40 @@ export default function HistoryPage() {
Comprehensive log of all agent execution cycles
</p>
</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>
</div>
{loading && (
<p className="text-sm" style={{ color: 'var(--text-mid)' }}>
Loading...
</p>
<div className="flex items-center gap-2.5 py-8" style={{ color: 'var(--text-mid)' }}>
<div
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 && (
<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}
</p>
</div>
)}
{!loading && <RunHistoryTable runs={runs} />}
</div>
)

View File

@ -4,7 +4,22 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
return (
<div className="flex min-h-screen" style={{ background: 'var(--bg-base)', color: 'var(--text-high)' }}>
<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>
)
}

View File

@ -2,27 +2,28 @@ import RunConfigForm from '@/features/new-run/components/RunConfigForm'
export default function NewRunPage() {
return (
<div className="max-w-[640px] animate-fade-up">
<div className="max-w-[660px] animate-fade-up">
{/* Page header */}
<div className="mb-8">
<div
className="apex-label mb-3"
>
<div className="apex-label mb-3" style={{ color: 'var(--accent)', opacity: 0.7 }}>
Intelligence Engine
</div>
<h1
className="text-[28px] font-bold tracking-tight mb-2"
style={{
fontFamily: 'var(--font-syne)',
fontSize: '32px',
fontWeight: 800,
letterSpacing: '-0.04em',
color: 'var(--text-high)',
fontFamily: 'var(--font-manrope)',
letterSpacing: '-0.03em',
lineHeight: 1.1,
marginBottom: '10px',
}}
>
New Analysis
</h1>
<p
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,
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 type { RunSummary } from '@/lib/types/run'
const STATUS_CONFIG: Record<string, { bg: string; color: string; dot: string; label: string }> = {
connecting: { bg: 'var(--bg-elevated)', color: 'var(--text-mid)', dot: 'var(--text-low)', label: 'Connecting' },
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' },
error: { bg: 'var(--error-bg)', color: 'var(--error)', dot: 'var(--error)', label: 'Error' },
const STATUS_CONFIG: Record<string, {
bg: string; color: string; dot: string; label: string; pulse: boolean
}> = {
connecting: { bg: 'var(--bg-elevated)', color: 'var(--text-mid)', dot: 'var(--text-low)', label: 'Connecting', pulse: false },
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 }> }) {
@ -29,51 +31,60 @@ export default function RunDetailPage({ params }: { params: Promise<{ id: string
<div className="max-w-4xl space-y-4 animate-fade-up">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-start justify-between gap-4 mb-2">
<div>
<div
className="apex-label mb-2"
>
<div className="apex-label mb-3" style={{ color: 'var(--accent)', opacity: 0.7 }}>
Analysis Run
</div>
<h1
className="text-[26px] font-bold tracking-tight"
className="flex items-baseline gap-3"
style={{
color: 'var(--text-high)',
fontFamily: 'var(--font-manrope)',
letterSpacing: '-0.03em',
fontFamily: 'var(--font-syne)',
fontSize: '32px',
fontWeight: 800,
letterSpacing: '-0.04em',
lineHeight: 1.1,
}}
>
{run ? (
<>
<span style={{ color: 'var(--accent-light)' }}>{run.ticker}</span>
<span style={{ color: 'var(--text-low)', fontWeight: 400, margin: '0 8px' }}>·</span>
<span>{run.date}</span>
<span
className="terminal-text"
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>
</div>
{/* Status badge */}
{/* Status pill */}
<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={{
background: sc.bg,
color: sc.color,
border: `1px solid ${sc.dot}40`,
fontFamily: 'var(--font-manrope)',
background: sc.bg,
color: sc.color,
border: `1px solid ${sc.dot}40`,
fontFamily: 'var(--font-mono)',
letterSpacing: '0.1em',
}}
>
<div
className="w-1.5 h-1.5 rounded-full shrink-0"
style={{
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>
@ -83,14 +94,14 @@ export default function RunDetailPage({ params }: { params: Promise<{ id: string
{/* Error */}
{error && (
<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={{
background: 'var(--error-bg)',
color: 'var(--error)',
border: '1px solid rgba(255,68,68,0.25)',
color: 'var(--error)',
border: '1px solid rgba(255,43,62,0.25)',
}}
>
<span className="font-medium">Error:</span> {error}
<span className="font-bold">Error:</span> {error}
</div>
)}
@ -101,7 +112,6 @@ export default function RunDetailPage({ params }: { params: Promise<{ id: string
{/* Phase tabs + reports */}
<PhaseTabs steps={steps} reports={reports} />
</div>
)
}

View File

@ -1,57 +1,63 @@
@import "tailwindcss";
/* ─── APEX Design Tokens ────────────────────────────────────────── */
/* ─── OBSIDIAN Design Tokens ─────────────────────────────────────── */
:root {
/* Backgrounds */
--bg-base: #040C1A;
--bg-surface: #070F1C;
--bg-card: #0C1628;
--bg-elevated: #121E30;
--bg-hover: #182338;
--bg-active: #1E2E42;
--bg-sidebar: #030810;
--bg-base: #050508;
--bg-surface: #08080F;
--bg-card: #0C0C18;
--bg-elevated: #111120;
--bg-hover: #17172A;
--bg-active: #1D1D38;
--bg-sidebar: #030306;
/* Borders */
--border: rgba(82, 122, 196, 0.10);
--border-raised: rgba(82, 122, 196, 0.18);
--border-active: rgba(68, 128, 255, 0.40);
--border-accent: rgba(68, 128, 255, 0.60);
--border: rgba(80, 80, 200, 0.07);
--border-raised: rgba(80, 80, 200, 0.14);
--border-active: rgba(0, 200, 240, 0.28);
--border-accent: rgba(0, 200, 240, 0.55);
/* Text */
--text-high: #E0E8FF;
--text-mid: #7A8FAD;
--text-low: #354869;
--text-faint: #1C2A40;
--text-high: #F0F2FF;
--text-mid: #525E80;
--text-low: #202840;
--text-faint: #0E1220;
/* Accent Blue */
--accent: #4480FF;
--accent-light: #8AAFFF;
--accent-dim: #1A3A88;
--accent-glow: rgba(68, 128, 255, 0.14);
--accent-glow2: rgba(68, 128, 255, 0.06);
/* Accent Cyan */
--accent: #00C4E8;
--accent-light: #65DAFF;
--accent-dim: #00243A;
--accent-glow: rgba(0, 196, 232, 0.14);
--accent-glow2: rgba(0, 196, 232, 0.05);
/* Semantic */
--buy: #00CE68;
--buy-bg: rgba(0, 206, 104, 0.08);
--buy-ring: rgba(0, 206, 104, 0.25);
--sell: #FF3355;
--sell-bg: rgba(255, 51, 85, 0.08);
--sell-ring:rgba(255, 51, 85, 0.25);
--hold: #F59E0B;
--hold-bg: rgba(245, 158, 11, 0.08);
--hold-ring:rgba(245, 158, 11, 0.25);
--error: #FF4444;
--error-bg: rgba(255, 68, 68, 0.08);
/* Gold (premium highlight) */
--gold: #FFB400;
--gold-bg: rgba(255, 180, 0, 0.07);
--gold-dim: #271D00;
--gold-ring: rgba(255, 180, 0, 0.25);
/* Semantic — BUY / SELL / HOLD */
--buy: #00E078;
--buy-bg: rgba(0, 224, 120, 0.07);
--buy-ring: rgba(0, 224, 120, 0.22);
--sell: #FF1F4C;
--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-running: #F59E0B;
--status-done: #4480FF;
--status-pending: #1C2A40;
--status-running: #FFB400;
--status-done: #00C4E8;
--status-pending: #141828;
}
/* ─── Animations ─────────────────────────────────────────────────── */
/* ─── Keyframes ──────────────────────────────────────────────────── */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(10px); }
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@ -61,8 +67,8 @@
}
@keyframes shimmer {
0%, 100% { opacity: 0.35; }
50% { opacity: 0.9; }
0%, 100% { opacity: 0.25; }
50% { opacity: 1; }
}
@keyframes spin-slow {
@ -70,8 +76,8 @@
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 0 0 var(--accent-glow); }
50% { box-shadow: 0 0 0 6px var(--accent-glow); }
0%, 100% { box-shadow: 0 0 0 0 var(--accent-glow); }
50% { box-shadow: 0 0 0 8px var(--accent-glow); }
}
@keyframes scan-line {
@ -79,6 +85,45 @@
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 ───────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
@ -91,34 +136,68 @@ html, body {
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 ─────────────────────────────────────────────────── */
.apex-display {
font-family: var(--font-manrope, 'Manrope'), sans-serif;
font-weight: 700;
letter-spacing: -0.03em;
font-family: var(--font-syne, 'Syne'), var(--font-manrope, 'Manrope'), sans-serif;
font-weight: 800;
letter-spacing: -0.04em;
color: var(--text-high);
}
.apex-label {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.12em;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-mid);
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 ──────────────────────────────────────────────────────── */
.apex-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
border-radius: 12px;
}
.apex-card-elevated {
background: var(--bg-elevated);
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 ────────────────────────────────────────────────────── */
@ -127,34 +206,36 @@ html, body {
overflow: hidden;
display: inline-flex;
align-items: center;
gap: 6px;
gap: 7px;
background: var(--accent);
color: #fff;
font-weight: 600;
color: #020508;
font-weight: 700;
font-size: 13px;
letter-spacing: 0.01em;
letter-spacing: 0.02em;
border-radius: 8px;
padding: 9px 20px;
transition: background 0.15s, opacity 0.15s, box-shadow 0.15s;
padding: 10px 22px;
transition: background 0.15s, opacity 0.15s, box-shadow 0.2s, transform 0.1s;
font-family: var(--font-manrope, 'Manrope'), sans-serif;
cursor: pointer;
}
.btn-primary::after {
.btn-primary::before {
content: '';
position: absolute;
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;
}
.btn-primary:hover {
background: #5590FF;
box-shadow: 0 4px 16px rgba(68,128,255,0.35);
background: #28DAFF;
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 {
opacity: 0.35;
opacity: 0.3;
cursor: not-allowed;
box-shadow: none;
transform: none;
}
.btn-secondary {
@ -163,12 +244,12 @@ html, body {
gap: 6px;
background: var(--bg-elevated);
color: var(--text-mid);
font-weight: 500;
font-weight: 600;
font-size: 13px;
border: 1px solid var(--border-raised);
border-radius: 8px;
padding: 8px 16px;
transition: background 0.15s, color 0.15s, border-color 0.15s;
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;
}
@ -176,10 +257,29 @@ html, body {
background: var(--bg-hover);
color: var(--text-high);
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 ─────────────────────────────────────────────────────── */
.vault-input {
@ -188,10 +288,10 @@ html, body {
color: var(--text-high);
font-size: 13px;
border: 1px solid var(--border-raised);
border-radius: 6px;
padding: 9px 12px;
border-radius: 8px;
padding: 10px 14px;
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;
}
.vault-input::placeholder { color: var(--text-low); }
@ -202,15 +302,23 @@ html, body {
}
.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 ─────────────────────────────────────────────────────── */
.badge-buy {
display: inline-flex; align-items: center;
background: var(--buy-bg);
color: var(--buy);
border: 1px solid var(--buy-ring);
font-size: 11px; font-weight: 700;
padding: 2px 9px; border-radius: 99px;
letter-spacing: 0.04em;
font-size: 10px; font-weight: 800;
padding: 2px 10px; border-radius: 99px;
letter-spacing: 0.08em;
text-transform: uppercase;
font-family: var(--font-manrope, 'Manrope'), sans-serif;
}
.badge-sell {
@ -218,9 +326,10 @@ html, body {
background: var(--sell-bg);
color: var(--sell);
border: 1px solid var(--sell-ring);
font-size: 11px; font-weight: 700;
padding: 2px 9px; border-radius: 99px;
letter-spacing: 0.04em;
font-size: 10px; font-weight: 800;
padding: 2px 10px; border-radius: 99px;
letter-spacing: 0.08em;
text-transform: uppercase;
font-family: var(--font-manrope, 'Manrope'), sans-serif;
}
.badge-hold {
@ -228,22 +337,40 @@ html, body {
background: var(--hold-bg);
color: var(--hold);
border: 1px solid var(--hold-ring);
font-size: 11px; font-weight: 700;
padding: 2px 9px; border-radius: 99px;
letter-spacing: 0.04em;
font-size: 10px; font-weight: 800;
padding: 2px 10px; border-radius: 99px;
letter-spacing: 0.08em;
text-transform: uppercase;
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 ──────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-raised); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-active); }
/* ─── 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 ────────────────────────────── */
.animate-fade-up { animation: fadeUp 0.3s ease-out both; }
.animate-fade-in { animation: fadeIn 0.25s ease-out both; }
.animate-shimmer { animation: shimmer 1.6s ease-in-out infinite; }
/* ─── Animations ─────────────────────────────────────────────────── */
.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-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 { Manrope, Inter } from 'next/font/google'
import { Manrope, Syne, JetBrains_Mono } from 'next/font/google'
import './globals.css'
const manrope = Manrope({
@ -8,15 +8,21 @@ const manrope = Manrope({
weight: ['400', '500', '600', '700', '800'],
})
const inter = Inter({
variable: '--font-inter',
const syne = Syne({
variable: '--font-syne',
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 = {
title: 'TradingAgents',
description: 'Multi-agent trading analysis',
title: 'TradingAgents — Multi-Agent AI Analysis',
description: 'Institutional-grade multi-agent trading intelligence',
}
export default function RootLayout({
@ -27,7 +33,7 @@ export default function RootLayout({
return (
<html
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>
</html>

View File

@ -6,31 +6,33 @@ const NAV = [
{
href: '/new-run',
label: 'New Analysis',
tag: 'RUN',
icon: (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<circle cx="7" cy="7" r="6" stroke="currentColor" strokeWidth="1.2"/>
<path d="M7 4v3l2 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<polygon points="4,3 13,8 4,13" fill="currentColor" opacity=".9"/>
</svg>
),
},
{
href: '/history',
label: 'Run History',
tag: 'LOG',
icon: (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="1" y="2" width="12" height="2" rx="1" fill="currentColor" opacity=".9"/>
<rect x="1" y="6" width="8" height="2" rx="1" fill="currentColor" opacity=".6"/>
<rect x="1" y="10" width="10" height="2" rx="1" fill="currentColor" opacity=".75"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="2" y="2" width="12" height="2.5" rx="1.25" fill="currentColor" opacity=".9"/>
<rect x="2" y="6.75" width="8" height="2.5" rx="1.25" fill="currentColor" opacity=".65"/>
<rect x="2" y="11.5" width="10" height="2.5" rx="1.25" fill="currentColor" opacity=".8"/>
</svg>
),
},
{
href: '/settings',
label: 'Settings',
tag: 'CFG',
icon: (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<circle cx="7" cy="7" r="2.2" stroke="currentColor" strokeWidth="1.2"/>
<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"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="2.5" stroke="currentColor" strokeWidth="1.4"/>
<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>
),
},
@ -41,70 +43,105 @@ export default function Sidebar() {
return (
<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={{
background: 'var(--bg-sidebar)',
borderRight: '1px solid var(--border)',
}}
>
{/* Logo */}
{/* Subtle dot grid texture */}
<div
className="px-5 py-5"
style={{ borderBottom: '1px solid var(--border)' }}
>
<div className="flex items-center gap-2.5">
{/* Geometric logo mark */}
className="absolute inset-0 pointer-events-none"
style={{
backgroundImage: 'radial-gradient(circle, rgba(80,80,200,0.08) 1px, transparent 1px)',
backgroundSize: '20px 20px',
}}
/>
{/* 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
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={{
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">
<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"/>
<circle cx="13" cy="2" r="1.2" fill="var(--accent-light)"/>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<polyline
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>
{/* 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
className="text-sm font-bold tracking-tight leading-none"
style={{ color: 'var(--text-high)', fontFamily: 'var(--font-manrope)' }}
className="font-bold leading-none tracking-tight"
style={{
fontFamily: 'var(--font-syne)',
fontSize: '15px',
color: 'var(--text-high)',
letterSpacing: '-0.02em',
}}
>
TradingAgents
</div>
<div
className="text-[9px] mt-0.5 font-medium tracking-widest uppercase"
style={{ color: 'var(--text-low)' }}
className="mt-1 flex items-center gap-1.5"
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>
{/* Nav section */}
<div className="px-2.5 pt-4 flex-1">
<div
className="apex-label px-2.5 mb-2"
>
Navigation
</div>
{/* Nav */}
<div className="relative px-3 pt-5 flex-1">
<div className="apex-label px-2 mb-3">Workspace</div>
<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 + '/')
return (
<Link
key={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={
active
? {
background: 'var(--accent-glow)',
color: 'var(--accent-light)',
borderLeft: '2px solid var(--accent)',
paddingLeft: '9px',
}
: {
color: 'var(--text-mid)',
@ -123,30 +160,86 @@ export default function Sidebar() {
}
}}
>
<span className="shrink-0 opacity-70">{icon}</span>
<span style={{ fontFamily: 'var(--font-manrope)' }}>{label}</span>
{/* Active indicator */}
{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>
)
})}
</nav>
</div>
{/* Divider */}
<div className="mx-3 h-px" style={{ background: 'var(--border)' }} />
{/* Footer */}
<div
className="px-5 py-4"
style={{ borderTop: '1px solid var(--border)' }}
>
<div className="flex items-center gap-2">
<div className="relative px-5 py-4">
<div className="flex items-center gap-2.5">
<div
className="w-1.5 h-1.5 rounded-full"
style={{ background: 'var(--buy)', boxShadow: '0 0 4px var(--buy)' }}
/>
<span
className="text-[10px] font-medium"
style={{ color: 'var(--text-low)', fontFamily: 'var(--font-manrope)' }}
className="w-6 h-6 rounded-lg flex items-center justify-center shrink-0"
style={{ background: 'var(--bg-elevated)', border: '1px solid var(--border-raised)' }}
>
Local · Development
</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<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>
</aside>

View File

@ -5,118 +5,203 @@ type Props = { runs: RunSummary[] }
function DecisionBadge({ decision }: { decision: string }) {
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 === 'hold') return <span className="badge-hold">{decision}</span>
return (
<span
className="px-2.5 py-1 rounded-full text-xs font-semibold"
style={{ backgroundColor: 'var(--bg-elevated)', color: 'var(--text-mid)' }}
className="px-2.5 py-1 rounded-full text-[10px] font-bold"
style={{
fontFamily: 'var(--font-mono)',
letterSpacing: '0.08em',
background: 'var(--bg-elevated)',
color: 'var(--text-mid)',
border: '1px solid var(--border-raised)',
}}
>
{decision}
</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) {
if (runs.length === 0) {
return (
<div
className="rounded-lg px-6 py-12 text-center"
style={{ backgroundColor: 'var(--bg-card)' }}
className="rounded-2xl flex flex-col items-center justify-center py-20 gap-4"
style={{ background: 'var(--bg-card)', border: '1px solid var(--border)' }}
>
<p className="text-sm" style={{ color: 'var(--text-low)' }}>
No runs yet. Start a new analysis.
</p>
{/* Empty state icon */}
<div
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>
)
}
return (
<div
className="rounded-lg overflow-hidden"
style={{ backgroundColor: 'var(--bg-card)', border: '1px solid var(--border)' }}
className="overflow-hidden"
style={{
background: 'var(--bg-card)',
border: '1px solid var(--border)',
borderRadius: '14px',
}}
>
<table className="w-full text-sm">
<thead>
<tr style={{ backgroundColor: 'var(--bg-elevated)' }}>
<th
className="apex-label px-5 py-3 text-left"
style={{ fontFamily: 'var(--font-manrope)' }}
>
Ticker
</th>
<th
className="apex-label px-5 py-3 text-left"
style={{ fontFamily: 'var(--font-manrope)' }}
>
Date
</th>
<th
className="apex-label px-5 py-3 text-left"
style={{ fontFamily: 'var(--font-manrope)' }}
>
Decision
</th>
<th
className="apex-label px-5 py-3 text-left"
style={{ fontFamily: 'var(--font-manrope)' }}
>
Created
</th>
<th className="px-5 py-3" />
</tr>
</thead>
<tbody>
{runs.map((run) => (
<tr
key={run.id}
className="transition-colors duration-100"
style={{ borderTop: '1px solid var(--border)' }}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = 'var(--bg-hover)')
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = '')
}
>
<td
className="px-5 py-4 font-mono font-semibold tracking-wider"
{/* Table header */}
<div
className="grid gap-0"
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr 100px 160px 80px',
background: 'var(--bg-elevated)',
borderBottom: '1px solid var(--border)',
padding: '0 20px',
}}
>
{['Ticker', 'Date', 'Decision', 'Created', ''].map((h) => (
<div
key={h}
className="apex-label py-3"
>
{h}
</div>
))}
</div>
{/* Rows */}
<div className="divide-y" style={{ borderColor: 'var(--border)' }}>
{runs.map((run, idx) => (
<div
key={run.id}
className="group transition-colors duration-100 animate-fade-up"
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr 100px 160px 80px',
padding: '0 20px',
alignItems: 'center',
animationDelay: `${idx * 30}ms`,
}}
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-hover)')}
onMouseLeave={(e) => (e.currentTarget.style.background = '')}
>
{/* Ticker */}
<div className="py-4 flex items-center gap-2.5">
<StatusDot decision={run.decision} />
<span
className="terminal-text font-bold text-sm tracking-wider"
style={{ color: 'var(--text-high)' }}
>
{run.ticker}
</td>
<td className="px-5 py-4" style={{ color: 'var(--text-mid)' }}>
{run.date}
</td>
<td className="px-5 py-4">
{run.decision ? (
<DecisionBadge decision={run.decision} />
) : (
<span style={{ color: 'var(--text-low)' }}></span>
)}
</td>
<td className="px-5 py-4 text-xs" style={{ color: 'var(--text-mid)' }}>
{new Date(run.created_at).toLocaleString()}
</td>
<td className="px-5 py-4">
<Link
href={`/runs/${run.id}`}
className="text-xs font-medium transition-colors"
style={{ color: 'var(--accent)' }}
onMouseEnter={(e) =>
(e.currentTarget.style.color = 'var(--accent-light)')
}
onMouseLeave={(e) =>
(e.currentTarget.style.color = 'var(--accent)')
}
</span>
</div>
{/* Date */}
<div
className="py-4 terminal-text text-sm"
style={{ color: 'var(--text-mid)', letterSpacing: '0.02em' }}
>
{run.date}
</div>
{/* Decision */}
<div className="py-4">
{run.decision ? (
<DecisionBadge decision={run.decision} />
) : (
<span
className="text-xs font-bold"
style={{ fontFamily: 'var(--font-mono)', color: 'var(--text-low)', letterSpacing: '0.08em' }}
>
View Report
</Link>
</td>
</tr>
))}
</tbody>
</table>
PENDING
</span>
)}
</div>
{/* Created */}
<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>
)
}

View File

@ -6,28 +6,56 @@ const ANALYSTS = [
label: 'Market',
full: 'Market Analyst',
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',
label: 'News',
full: 'News Analyst',
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',
label: 'Fundamentals',
full: 'Fundamentals Analyst',
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',
label: 'Social',
full: 'Social Analyst',
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 (
<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)
return (
<button
key={id}
type="button"
title={full}
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={{
background: active ? 'var(--bg-active)' : 'var(--bg-elevated)',
border: active ? `1px solid ${dot}40` : '1px solid var(--border)',
borderTop: active ? `2px solid ${dot}` : '1px solid var(--border)',
borderRadius: '10px',
background: active
? `linear-gradient(145deg, ${accent}0F 0%, ${accent}06 100%)`
: 'var(--bg-elevated)',
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
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={{
background: dot,
boxShadow: active ? `0 0 6px ${dot}80` : 'none',
background: active ? `${accent}18` : 'var(--bg-active)',
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 && (
<div
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">
<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>
</div>
)}
@ -83,15 +135,19 @@ export default function AnalystSelector({ selected, onChange }: Props) {
<div
className="text-sm font-semibold mb-0.5"
style={{
color: active ? 'var(--text-high)' : 'var(--text-mid)',
color: active ? 'var(--text-high)' : 'var(--text-mid)',
fontFamily: 'var(--font-manrope)',
}}
>
{label}
</div>
<div
className="text-[11px] leading-snug"
style={{ color: 'var(--text-low)' }}
className="text-[10px] leading-snug"
style={{
color: active ? `${accent}B0` : 'var(--text-low)',
fontFamily: 'var(--font-mono)',
letterSpacing: '0.03em',
}}
>
{desc}
</div>

View File

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

View File

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

View File

@ -7,11 +7,11 @@ import AnalystReports from './AnalystReports'
type Phase = 'analysts' | 'researchers' | 'trader' | 'risk'
const TABS: { label: string; phase: Phase; desc: string }[] = [
{ label: 'Analysts', phase: 'analysts', desc: '4 agents' },
{ label: 'Researchers', phase: 'researchers', desc: '3 agents' },
{ label: 'Trader', phase: 'trader', desc: '1 agent' },
{ label: 'Risk', phase: 'risk', desc: '4 agents' },
const TABS: { label: string; phase: Phase; count: string }[] = [
{ label: 'Analysts', phase: 'analysts', count: '4' },
{ label: 'Researchers', phase: 'researchers', count: '3' },
{ label: 'Trader', phase: 'trader', count: '1' },
{ label: 'Risk', phase: 'risk', count: '4' },
]
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
}
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) {
const [active, setActive] = useState<Phase>('analysts')
return (
<div>
{/* Section label */}
<div
className="apex-label mb-3"
>
Agent Reports
{/* Section header */}
<div className="flex items-center justify-between mb-4">
<div className="apex-label">Agent Reports</div>
<div
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>
{/* Tab bar */}
<div
className="flex gap-1 p-1 mb-5"
style={{
background: 'var(--bg-card)',
border: '1px solid var(--border)',
borderRadius: '10px',
width: 'fit-content',
background: 'var(--bg-card)',
border: '1px solid var(--border)',
borderRadius: '12px',
width: 'fit-content',
}}
>
{TABS.map(({ label, phase }) => {
const isActive = active === phase
const completion = getPhaseCompletion(phase, steps)
const allDone = completion === 100
{TABS.map(({ label, phase, count }) => {
const isActive = active === phase
const status = getPhaseStatus(phase, steps)
const isDone = status === 'done'
const isRunning = status === 'running'
return (
<button
key={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={
isActive
? {
background: 'var(--accent-glow)',
color: 'var(--accent-light)',
border: '1px solid var(--border-active)',
fontFamily: 'var(--font-manrope)',
background: 'var(--bg-elevated)',
color: 'var(--text-high)',
border: '1px solid var(--border-active)',
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)',
}
}
>
{/* Done indicator dot */}
{allDone && (
{/* Status indicator */}
{isDone && (
<div
className="w-1.5 h-1.5 rounded-full"
style={{ background: 'var(--buy)', flexShrink: 0 }}
className="w-1.5 h-1.5 rounded-full shrink-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}
{/* 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>
)
})}

View File

@ -6,11 +6,12 @@ type Phase = 'analysts' | 'researchers' | 'trader' | 'risk'
type Props = { steps: Record<string, StepStatus> }
const PHASES: Phase[] = ['analysts', 'researchers', 'trader', 'risk']
const PHASE_LABELS: Record<Phase, string> = {
analysts: 'Analysts', researchers: 'Researchers', trader: 'Trader', risk: 'Risk',
}
const PHASE_NUMS: Record<Phase, string> = {
analysts: '01', researchers: '02', trader: '03', risk: '04',
const PHASE_META: Record<Phase, { label: string; code: string; desc: string }> = {
analysts: { label: 'Analysis', code: 'PHASE-01', desc: 'Market data & signals' },
researchers: { label: 'Research', code: 'PHASE-02', desc: 'Bull/bear debate' },
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 {
@ -21,112 +22,190 @@ function phaseStatus(phase: Phase, steps: Record<string, StepStatus>): StepStatu
}
export default function PipelineStepper({ steps }: Props) {
const doneCount = PHASES.filter((p) => phaseStatus(p, steps) === 'done').length
const progressPct = (doneCount / PHASES.length) * 100
return (
<div
className="px-6 py-5"
className="relative overflow-hidden"
style={{
background: 'var(--bg-card)',
border: '1px solid var(--border)',
borderRadius: '10px',
background: 'var(--bg-card)',
border: '1px solid var(--border)',
borderRadius: '14px',
padding: '20px 24px',
}}
>
<div
className="apex-label mb-4"
>
Pipeline
{/* Header row */}
<div className="flex items-center justify-between mb-5">
<div className="apex-label">Agent Pipeline</div>
<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 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) => {
const status = phaseStatus(phase, steps)
const isDone = status === 'done'
const isRunning = status === 'running'
const isPending = status === 'pending'
const meta = PHASE_META[phase]
return (
<div key={phase} className="flex items-center flex-1 last:flex-none">
{/* Step node */}
<div className="flex flex-col items-center gap-2 relative">
{/* Circle */}
<div
key={phase}
className="relative flex flex-col items-center gap-2.5 p-3 rounded-xl transition-all duration-300"
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
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={{
background: isDone
? 'var(--accent)'
fontFamily: 'var(--font-manrope)',
color: isDone
? 'var(--accent-light)'
: isRunning
? 'var(--bg-elevated)'
: 'var(--bg-elevated)',
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',
? 'var(--status-running)'
: 'var(--text-mid)',
}}
>
{isDone ? (
<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>
)}
{meta.label}
</div>
{/* Label */}
<span
className="text-[10px] font-medium text-center whitespace-nowrap transition-all duration-300"
<div
className="text-[9px] leading-snug hidden sm:block"
style={{
color: isDone ? 'var(--accent-light)' : isRunning ? 'var(--status-running)' : 'var(--text-low)',
fontFamily: 'var(--font-manrope)',
fontFamily: 'var(--font-mono)',
color: 'var(--text-low)',
letterSpacing: '0.04em',
}}
>
{PHASE_LABELS[phase]}
</span>
{meta.desc}
</div>
</div>
{/* Connector */}
{i < PHASES.length - 1 && (
<div
className="flex-1 h-px mx-3 transition-all duration-700 relative overflow-hidden"
style={{
background: isDone ? 'var(--accent)' : 'var(--border)',
boxShadow: isDone ? '0 0 6px var(--accent-glow)' : 'none',
marginBottom: '20px',
}}
>
{isRunning && (
<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>
)}
{/* Phase code */}
<div
className="absolute top-2 right-2 text-[8px] font-bold"
style={{
fontFamily: 'var(--font-mono)',
letterSpacing: '0.05em',
color: isDone ? 'var(--accent)' : 'var(--text-faint)',
opacity: 0.6,
}}
>
{meta.code}
</div>
</div>
)
})}

View File

@ -6,33 +6,41 @@ const VERDICT_CONFIG: Record<Decision, {
color: string
colorBg: string
colorRing: string
colorGlow: string
label: string
sublabel: string
arrow: string
symbol: string
description: string
}> = {
BUY: {
color: 'var(--buy)',
colorBg: 'var(--buy-bg)',
colorRing: 'var(--buy-ring)',
label: 'BUY',
sublabel: 'Long position recommended',
arrow: '↑',
color: 'var(--buy)',
colorBg: 'var(--buy-bg)',
colorRing: 'var(--buy-ring)',
colorGlow: 'rgba(0, 224, 120, 0.15)',
label: 'BUY',
sublabel: 'Long Position',
symbol: '↑',
description: 'AI consensus recommends entering a long position',
},
SELL: {
color: 'var(--sell)',
colorBg: 'var(--sell-bg)',
colorRing: 'var(--sell-ring)',
label: 'SELL',
sublabel: 'Exit position recommended',
arrow: '↓',
color: 'var(--sell)',
colorBg: 'var(--sell-bg)',
colorRing: 'var(--sell-ring)',
colorGlow: 'rgba(255, 31, 76, 0.15)',
label: 'SELL',
sublabel: 'Exit Position',
symbol: '↓',
description: 'AI consensus recommends exiting the position',
},
HOLD: {
color: 'var(--hold)',
colorBg: 'var(--hold-bg)',
colorRing: 'var(--hold-ring)',
label: 'HOLD',
sublabel: 'Maintain current position',
arrow: '→',
color: 'var(--hold)',
colorBg: 'var(--hold-bg)',
colorRing: 'var(--hold-ring)',
colorGlow: 'rgba(255, 180, 0, 0.15)',
label: 'HOLD',
sublabel: 'Maintain Position',
symbol: '→',
description: 'AI consensus recommends maintaining current exposure',
},
}
@ -42,99 +50,150 @@ export default function VerdictBanner({ verdict, ticker, date }: Props) {
return (
<div
className="relative overflow-hidden animate-fade-up"
className="relative overflow-hidden"
style={{
background: cfg.colorBg,
border: `1px solid ${cfg.colorRing}`,
borderRadius: '12px',
padding: '24px 28px',
background: cfg.colorBg,
border: `1px solid ${cfg.colorRing}`,
borderRadius: '16px',
animation: 'verdict-reveal 0.55s cubic-bezier(0.16,1,0.3,1) both',
}}
>
{/* Background glow */}
{/* Background glow blobs */}
<div
className="absolute inset-0 pointer-events-none"
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">
{/* Left: metadata */}
<div>
<div
className="apex-label mb-2"
>
{/* Scanline shimmer on active color */}
<div
className="absolute top-0 left-0 right-0 h-px pointer-events-none"
style={{
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
</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={{
color: 'var(--text-high)',
fontFamily: 'var(--font-manrope)',
letterSpacing: '-0.03em',
fontFamily: 'var(--font-mono)',
letterSpacing: '0.1em',
background: `${cfg.colorRing}`,
color: cfg.color,
border: `1px solid ${cfg.colorRing}`,
}}
>
{ticker}
</div>
<div
className="text-xs font-mono"
style={{ color: 'var(--text-mid)' }}
>
{date}
AI CONSENSUS
</div>
</div>
{/* Divider */}
<div
className="hidden sm:block w-px self-stretch"
style={{ background: cfg.colorRing, opacity: 0.4 }}
/>
{/* Right: verdict */}
<div className="flex items-center gap-5">
<div>
{/* Main verdict row */}
<div className="flex items-end justify-between gap-6">
{/* Left: ticker + description */}
<div className="flex-1 min-w-0">
<div
className="text-xs font-medium mb-1 text-right"
style={{ color: cfg.color, opacity: 0.8, fontFamily: 'var(--font-manrope)' }}
className="flex items-baseline gap-3 mb-2"
>
<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}
</div>
<div
className="text-right"
className="flex items-center justify-center gap-2"
style={{
color: 'var(--text-low)',
fontSize: '11px',
padding: '14px 32px',
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>
{/* 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>

14
ui/package-lock.json generated
View File

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

View File

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