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:
parent
ae6776afc3
commit
0690f628ab
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 2–5 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>
|
||||
2–5 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
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"next": "16.2.1",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
|
|
|
|||
Loading…
Reference in New Issue