Add FE dashboard

This commit is contained in:
Lux 2026-03-16 15:27:03 +07:00
parent 22616e8bdb
commit 6b833e6942
26 changed files with 8345 additions and 0 deletions

41
ui/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
ui/README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

BIN
ui/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

140
ui/app/globals.css Normal file
View File

@ -0,0 +1,140 @@
@import "tailwindcss";
/*
THEME TOKENS
*/
:root {
--bg-void: #08090c;
--bg-surface: #0e1015;
--bg-elevated: #14161d;
--bg-panel: #1a1d27;
--bg-panel-hover: #1f2230;
--border-subtle: rgba(255, 255, 255, 0.04);
--border-medium: rgba(255, 255, 255, 0.08);
--border-accent: rgba(212, 175, 55, 0.2);
--text-primary: #e8e6e1;
--text-secondary: #8a8780;
--text-tertiary: #5a5853;
--amber: #d4af37;
--amber-dim: rgba(212, 175, 55, 0.15);
--amber-glow: rgba(212, 175, 55, 0.06);
--cyan: #4ecdc4;
--cyan-dim: rgba(78, 205, 196, 0.12);
--red: #e74c3c;
--red-dim: rgba(231, 76, 60, 0.12);
--green: #2ecc71;
--green-dim: rgba(46, 204, 113, 0.12);
--blue: #5b9bd5;
--blue-dim: rgba(91, 155, 213, 0.12);
--purple: #9b59b6;
--purple-dim: rgba(155, 89, 182, 0.12);
}
@theme inline {
--color-bg-void: var(--bg-void);
--color-bg-surface: var(--bg-surface);
--color-bg-elevated: var(--bg-elevated);
--color-bg-panel: var(--bg-panel);
--color-border-subtle: var(--border-subtle);
--color-border-medium: var(--border-medium);
--color-border-accent: var(--border-accent);
--color-text-primary: var(--text-primary);
--color-text-secondary: var(--text-secondary);
--color-text-tertiary: var(--text-tertiary);
--color-amber: var(--amber);
--color-amber-dim: var(--amber-dim);
--color-amber-glow: var(--amber-glow);
--color-cyan: var(--cyan);
--color-cyan-dim: var(--cyan-dim);
--color-red: var(--red);
--color-red-dim: var(--red-dim);
--color-green: var(--green);
--color-green-dim: var(--green-dim);
--color-blue: var(--blue);
--color-blue-dim: var(--blue-dim);
--color-purple: var(--purple);
--color-purple-dim: var(--purple-dim);
--font-display: var(--font-syne);
--font-serif: var(--font-instrument-serif);
--font-mono: var(--font-dm-mono);
}
/*
BASE STYLES
*/
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background: var(--bg-void);
color: var(--text-primary);
font-family: var(--font-mono);
min-height: 100vh;
overflow-x: hidden;
}
/* Atmosphere */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 80% 50% at 20% 10%, rgba(212, 175, 55, 0.03), transparent),
radial-gradient(ellipse 60% 40% at 80% 80%, rgba(78, 205, 196, 0.02), transparent);
pointer-events: none;
z-index: 0;
}
/* Scanlines */
body::after {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.02) 2px,
rgba(0, 0, 0, 0.02) 4px
);
pointer-events: none;
z-index: 9999;
}
/* Scrollbar */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-medium); border-radius: 2px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
/*
ANIMATIONS
*/
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@keyframes node-glow {
0%, 100% { box-shadow: 0 0 15px var(--amber-dim); }
50% { box-shadow: 0 0 30px rgba(212, 175, 55, 0.2); }
}
@keyframes data-pulse {
0%, 100% { opacity: 1; transform: translateY(-50%) scale(1); }
50% { opacity: 0.3; transform: translateY(-50%) scale(1.5); }
}
@keyframes ticker-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}

44
ui/app/layout.tsx Normal file
View File

@ -0,0 +1,44 @@
import type { Metadata } from "next";
import { Syne, Instrument_Serif, DM_Mono } from "next/font/google";
import "./globals.css";
const syne = Syne({
variable: "--font-syne",
subsets: ["latin"],
weight: ["400", "500", "600", "700", "800"],
});
const instrumentSerif = Instrument_Serif({
variable: "--font-instrument-serif",
subsets: ["latin"],
weight: "400",
style: ["normal", "italic"],
});
const dmMono = DM_Mono({
variable: "--font-dm-mono",
subsets: ["latin"],
weight: ["300", "400", "500"],
});
export const metadata: Metadata = {
title: "TradingAgents — Multi-Agent Trading Intelligence",
description:
"Multi-agent LLM framework that mirrors real-world trading firm dynamics for AI-powered market analysis and trading decisions.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${syne.variable} ${instrumentSerif.variable} ${dmMono.variable}`}
>
{children}
</body>
</html>
);
}

31
ui/app/page.tsx Normal file
View File

@ -0,0 +1,31 @@
import TickerBar from "@/components/TickerBar";
import TopBar from "@/components/TopBar";
import AgentPipeline from "@/components/AgentPipeline";
import MarketOverview from "@/components/MarketOverview";
import AgentStatus from "@/components/AgentStatus";
import AnalystGrid from "@/components/AnalystGrid";
import DebatePanel from "@/components/DebatePanel";
import DecisionPanel from "@/components/DecisionPanel";
export default function Home() {
return (
<div className="relative z-1 min-h-screen px-5 pb-5 max-w-[1600px] mx-auto">
<TickerBar />
<TopBar />
<AgentPipeline />
<main className="grid grid-cols-[1fr_1fr_340px] gap-3">
{/* Row 1: Market Overview + Agent Status */}
<MarketOverview />
<AgentStatus />
{/* Row 2: Analyst Grid + Debate Panel (spans 2 rows) */}
<AnalystGrid />
<DebatePanel />
{/* Row 3: Decision Panel */}
<DecisionPanel />
</main>
</div>
);
}

View File

@ -0,0 +1,150 @@
"use client";
import { motion } from "framer-motion";
type NodeState = "complete" | "active" | "pending";
interface PipelineNode {
icon: string;
label: string;
state: NodeState;
}
interface PipelineStage {
group: string;
nodes: PipelineNode[];
}
const stages: PipelineStage[] = [
{
group: "Analysts",
nodes: [
{ icon: "\u{1F4CA}", label: "Market", state: "complete" },
{ icon: "\u{1F4CB}", label: "Fundamentals", state: "complete" },
{ icon: "\u{1F4F0}", label: "News", state: "complete" },
{ icon: "\u{1F4AC}", label: "Social", state: "complete" },
],
},
{
group: "Debate",
nodes: [
{ icon: "\u{1F402}", label: "Bull", state: "active" },
{ icon: "\u{1F43B}", label: "Bear", state: "active" },
{ icon: "\u2696\uFE0F", label: "Judge", state: "pending" },
],
},
{
group: "Execution",
nodes: [{ icon: "\u{1F4B9}", label: "Trader", state: "pending" }],
},
{
group: "Risk",
nodes: [
{ icon: "\u{1F525}", label: "Aggressive", state: "pending" },
{ icon: "\u{1F6E1}\uFE0F", label: "Conservative", state: "pending" },
{ icon: "\u2696\uFE0F", label: "Neutral", state: "pending" },
{ icon: "\u{1F3DB}\uFE0F", label: "Risk Judge", state: "pending" },
],
},
];
function NodeIcon({ node }: { node: PipelineNode }) {
const stateClasses: Record<NodeState, string> = {
complete: "bg-green-dim border-green",
active: "bg-amber-dim border-amber",
pending: "bg-bg-elevated border-border-medium",
};
return (
<div className="flex flex-col items-center gap-1.5 px-2 py-1 group cursor-default">
<div
className={`w-10 h-10 rounded-[10px] grid place-items-center text-base
border transition-all duration-400 ${stateClasses[node.state]}
group-hover:-translate-y-0.5 group-hover:border-amber group-hover:shadow-[0_4px_20px_var(--amber-dim)]`}
style={
node.state === "active"
? { animation: "node-glow 2s ease-in-out infinite" }
: undefined
}
>
{node.icon}
</div>
<span
className={`text-[9px] uppercase tracking-[1.5px] whitespace-nowrap
${node.state === "active" ? "text-amber" : node.state === "complete" ? "text-green" : "text-text-tertiary"}`}
>
{node.label}
</span>
</div>
);
}
function Connector({ active }: { active: boolean }) {
return (
<div className="relative">
<div
className={`h-px transition-all duration-500 ${
active
? "bg-gradient-to-r from-amber to-amber-dim h-0.5 shadow-[0_0_8px_var(--amber-dim)]"
: "bg-border-medium"
}`}
style={{ width: 20 }}
/>
{active && (
<div
className="absolute right-[-2px] top-1/2 w-1 h-1 bg-amber rounded-full"
style={{
transform: "translateY(-50%)",
animation: "data-pulse 1.5s ease-in-out infinite",
}}
/>
)}
</div>
);
}
export default function AgentPipeline() {
return (
<motion.section
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: [0.22, 1, 0.36, 1] }}
className="flex items-center justify-center gap-0 py-7 overflow-x-auto"
>
{stages.map((stage, si) => (
<div key={stage.group} className="flex items-center">
{si > 0 && (
<div className="mx-1">
<div
className={`h-px ${
stages[si - 1].nodes.every((n) => n.state === "complete")
? "bg-gradient-to-r from-amber to-amber-dim h-0.5 shadow-[0_0_8px_var(--amber-dim)]"
: "bg-border-medium"
}`}
style={{ width: 40 }}
/>
</div>
)}
<div className="relative flex items-center px-2.5 py-1.5 border border-border-subtle rounded-xl bg-bg-surface gap-1">
<span className="absolute -top-2 left-3 text-[8px] text-text-tertiary uppercase tracking-[2px] bg-bg-surface px-1.5">
{stage.group}
</span>
{stage.nodes.map((node, ni) => (
<div key={node.label} className="flex items-center">
{ni > 0 && (
<Connector
active={
stage.nodes[ni - 1].state === "complete" ||
stage.nodes[ni - 1].state === "active"
}
/>
)}
<NodeIcon node={node} />
</div>
))}
</div>
</div>
))}
</motion.section>
);
}

View File

@ -0,0 +1,78 @@
"use client";
import Panel from "./ui/Panel";
const agents = [
{ icon: "\u{1F4CA}", label: "Market Analyst", detail: "RSI, MACD, Bollinger analysis complete", status: "Done", color: "cyan" as const },
{ icon: "\u{1F4CB}", label: "Fundamentals Analyst", detail: "Balance sheet & cash flow reviewed", status: "Done", color: "amber" as const },
{ icon: "\u{1F4F0}", label: "News Analyst", detail: "Global news & macro scan complete", status: "Done", color: "cyan" as const },
{ icon: "\u{1F4AC}", label: "Social Media Analyst", detail: "Sentiment scoring finalized", status: "Done", color: "purple" as const },
{ icon: "\u{1F402}", label: "Bull Researcher", detail: "Argument round 1 submitted", status: "Active", color: "green" as const },
{ icon: "\u{1F43B}", label: "Bear Researcher", detail: "Counter-argument pending", status: "Waiting", color: "amber" as const },
{ icon: "\u2696\uFE0F", label: "Trader", detail: "Awaiting debate conclusion", status: "Idle", color: "amber" as const },
{ icon: "\u{1F6E1}\uFE0F", label: "Risk Manager", detail: "Awaiting trade proposal", status: "Idle", color: "amber" as const },
];
const logs = [
{ time: "14:32:08", agent: "MKT", agentType: "analyst" as const, msg: "Technical indicators computed \u2014 MACD bullish cross detected" },
{ time: "14:32:15", agent: "FND", agentType: "analyst" as const, msg: "Balance sheet analysis complete \u2014 strong cash position" },
{ time: "14:32:22", agent: "NWS", agentType: "analyst" as const, msg: "Processed 47 news articles \u2014 net positive sentiment" },
{ time: "14:32:28", agent: "SOC", agentType: "analyst" as const, msg: "Social sentiment score: 0.62 \u2014 mixed retail signals" },
{ time: "14:32:35", agent: "BULL", agentType: "researcher" as const, msg: "Opening argument submitted \u2014 AI infrastructure thesis" },
{ time: "14:32:42", agent: "BEAR", agentType: "researcher" as const, msg: "Counter-argument: valuation stretched at 65x forward" },
];
const iconBgMap = {
cyan: "bg-cyan-dim",
amber: "bg-amber-dim",
green: "bg-green-dim",
purple: "bg-purple-dim",
};
const statusColorMap: Record<string, string> = {
Done: "text-green",
Active: "text-amber",
Waiting: "text-text-tertiary",
Idle: "text-text-tertiary",
};
const agentTypeColorMap = {
analyst: "text-cyan",
researcher: "text-amber",
trader: "text-green",
risk: "text-red",
};
export default function AgentStatus() {
return (
<Panel title="Agent Status" badge="8 Agents" badgeVariant="amber" delay={0.2}>
<div className="flex flex-col gap-2">
{agents.map((a) => (
<div key={a.label} className="flex items-center gap-2.5 p-2 rounded bg-bg-elevated">
<div className={`w-7 h-7 rounded-md grid place-items-center text-xs shrink-0 ${iconBgMap[a.color]}`}>
{a.icon}
</div>
<div className="flex-1 min-w-0">
<div className="text-[11px] text-text-primary font-medium">{a.label}</div>
<div className="text-[10px] text-text-tertiary mt-px">{a.detail}</div>
</div>
<div className={`text-[10px] font-medium ${statusColorMap[a.status]}`}>{a.status}</div>
</div>
))}
<div className="mt-2 max-h-[150px] overflow-y-auto flex flex-col">
{logs.map((l, i) => (
<div
key={i}
className="flex gap-2 py-1.5 border-b border-border-subtle text-[10px]"
>
<span className="text-text-tertiary shrink-0 tabular-nums">{l.time}</span>
<span className={`shrink-0 font-medium ${agentTypeColorMap[l.agentType]}`}>{l.agent}</span>
<span className="text-text-secondary overflow-hidden text-ellipsis whitespace-nowrap">{l.msg}</span>
</div>
))}
</div>
</div>
</Panel>
);
}

View File

@ -0,0 +1,158 @@
"use client";
import { motion } from "framer-motion";
import Panel from "./ui/Panel";
interface AnalystCard {
name: string;
role: string;
avatarType: string;
icon: string;
signal: "bullish" | "bearish" | "neutral";
signalText: string;
metrics: { label: string; value: string }[];
excerpt: string;
}
const analysts: AnalystCard[] = [
{
name: "Market Analyst",
role: "Technical Analysis",
avatarType: "market",
icon: "\u{1F4CA}",
signal: "bullish",
signalText: "Bullish \u2014 Uptrend Confirmed",
metrics: [
{ label: "Trend", value: "Strong Uptrend" },
{ label: "Momentum", value: "Accelerating" },
{ label: "Support", value: "$842.10" },
],
excerpt:
"MACD crossed above signal line with increasing histogram bars. RSI at 67.4 shows room before overbought. Price above all major moving averages with expanding volume.",
},
{
name: "Fundamentals",
role: "Financial Analysis",
avatarType: "fundamentals",
icon: "\u{1F4CB}",
signal: "bullish",
signalText: "Bullish \u2014 Strong Financials",
metrics: [
{ label: "Revenue Growth", value: "+122% YoY" },
{ label: "Gross Margin", value: "74.8%" },
{ label: "Free Cash Flow", value: "$27.1B" },
],
excerpt:
"Data center revenue surged 409% YoY driven by AI infrastructure demand. Operating margins expanding to 61.6%. Balance sheet shows $26B cash with manageable debt.",
},
{
name: "News Analyst",
role: "Macro & News",
avatarType: "news",
icon: "\u{1F4F0}",
signal: "bullish",
signalText: "Bullish \u2014 Favorable Headlines",
metrics: [
{ label: "Sentiment", value: "Positive (82%)" },
{ label: "Key Events", value: "3 Catalysts" },
{ label: "Risk Flags", value: "1 Moderate" },
],
excerpt:
"New Blackwell GPU architecture receiving strong OEM adoption. Sovereign AI investments from multiple nations. Minor concern: potential China export restrictions.",
},
{
name: "Social Media",
role: "Sentiment Analysis",
avatarType: "social",
icon: "\u{1F4AC}",
signal: "neutral",
signalText: "Neutral \u2014 Mixed Signals",
metrics: [
{ label: "Overall Score", value: "0.62 / 1.0" },
{ label: "Buzz Volume", value: "Very High" },
{ label: "Inst. Sentiment", value: "Positive" },
],
excerpt:
"Institutional sentiment strongly positive with multiple analyst upgrades. Retail shows FOMO \u2014 elevated put/call ratio suggests hedging. Insider selling noted.",
},
];
const avatarBg: Record<string, string> = {
market: "bg-cyan-dim border-cyan/20",
fundamentals: "bg-amber-dim border-amber/20",
news: "bg-blue-dim border-blue/20",
social: "bg-purple-dim border-purple/20",
};
const signalStyle: Record<string, string> = {
bullish: "bg-green-dim text-green",
bearish: "bg-red-dim text-red",
neutral: "bg-cyan-dim text-cyan",
};
export default function AnalystGrid() {
return (
<Panel
title="Analyst Reports"
badge="Updated"
badgeVariant="live"
className="col-span-2"
delay={0.25}
>
<div className="grid grid-cols-4 gap-2.5">
{analysts.map((a, i) => (
<motion.div
key={a.name}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.5,
delay: 0.3 + i * 0.08,
ease: [0.22, 1, 0.36, 1],
}}
className="bg-bg-elevated border border-border-subtle rounded-lg p-3.5
transition-all duration-300 cursor-default
hover:border-border-medium hover:-translate-y-0.5 hover:shadow-[0_8px_30px_rgba(0,0,0,0.3)]"
>
<div className="flex items-center gap-2.5 mb-3">
<div
className={`w-8 h-8 rounded-lg grid place-items-center text-sm shrink-0 border ${avatarBg[a.avatarType]}`}
>
{a.icon}
</div>
<div>
<div className="font-display font-semibold text-xs text-text-primary">
{a.name}
</div>
<div className="text-[9px] text-text-tertiary uppercase tracking-[1px]">
{a.role}
</div>
</div>
</div>
<div
className={`flex items-center gap-1.5 mb-2.5 px-2.5 py-1.5 rounded text-[11px] font-medium ${signalStyle[a.signal]}`}
>
{a.signalText}
</div>
<div className="flex flex-col gap-1.5">
{a.metrics.map((m) => (
<div key={m.label} className="flex justify-between items-center">
<span className="text-[10px] text-text-tertiary">{m.label}</span>
<span className="text-[11px] font-medium text-text-secondary">
{m.value}
</span>
</div>
))}
</div>
<div className="mt-2.5 pt-2.5 border-t border-border-subtle text-[11px] text-text-secondary leading-relaxed line-clamp-3">
{a.excerpt}
</div>
</motion.div>
))}
</div>
</Panel>
);
}

View File

@ -0,0 +1,102 @@
"use client";
import { motion } from "framer-motion";
const messages = [
{
side: "bull" as const,
author: "Bull Researcher",
text: "NVDA demonstrates exceptional fundamentals with 122% revenue growth and industry-defining margins. The AI infrastructure buildout is in early innings \u2014 data center revenue alone grew 409% YoY. The MACD bullish crossover confirms technical strength.",
},
{
side: "bear" as const,
author: "Bear Researcher",
text: "Valuation is stretched at 65x forward earnings with RSI approaching overbought territory. Insider selling has accelerated and China export restrictions pose material revenue risk. Social sentiment shows FOMO-driven buying \u2014 a contrarian red flag.",
},
{
side: "bull" as const,
author: "Bull Researcher",
text: "The premium valuation is justified by a near-monopoly in AI accelerators. Blackwell architecture orders extend visibility through 2026. The addressable market is expanding \u2014 sovereign AI alone represents a $100B+ opportunity.",
},
{
side: "bear" as const,
author: "Bear Researcher",
text: "Competition is intensifying from AMD MI300, Intel Gaudi, and custom silicon from Google, Amazon, and Microsoft. Customer concentration risk is real \u2014 top 4 hyperscalers represent 45% of data center revenue.",
},
{
side: "judge" as const,
author: "Research Manager",
text: "RECOMMENDATION: BUY with conviction. Bull arguments regarding early-cycle AI infrastructure and expanding TAM outweigh near-term valuation concerns. Position sizing should reflect elevated volatility. Entry at current levels with 3-month horizon.",
},
];
const sideStyles = {
bull: {
border: "border-l-2 border-l-green",
bg: "bg-gradient-to-r from-green-dim to-transparent",
author: "text-green",
},
bear: {
border: "border-l-2 border-l-red",
bg: "bg-gradient-to-r from-red-dim to-transparent",
author: "text-red",
},
judge: {
border: "border-l-2 border-l-amber",
bg: "bg-gradient-to-r from-amber-glow to-transparent",
author: "text-amber",
},
};
export default function DebatePanel() {
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3, ease: [0.22, 1, 0.36, 1] }}
className="bg-bg-surface border border-border-subtle rounded-xl overflow-hidden
transition-colors duration-300 hover:border-border-medium col-span-1 row-span-2 flex flex-col"
>
<div className="flex items-center justify-between px-4 pt-3.5 pb-2.5 border-b border-border-subtle">
<span className="font-display font-semibold text-[11px] tracking-[2px] uppercase text-text-secondary">
Investment Debate
</span>
<span className="text-[9px] px-2 py-0.5 rounded-full tracking-[1px] uppercase bg-amber-dim text-amber">
Round 1
</span>
</div>
<div className="flex items-center justify-center gap-4 px-4 py-3 border-b border-border-subtle">
<span className="flex items-center gap-1.5 text-[11px] font-medium text-green">
&#9650; Bull
</span>
<span className="font-serif italic text-sm text-text-tertiary">vs</span>
<span className="flex items-center gap-1.5 text-[11px] font-medium text-red">
&#9660; Bear
</span>
</div>
<div className="flex-1 overflow-y-auto max-h-[500px]">
{messages.map((m, i) => {
const style = sideStyles[m.side];
return (
<motion.div
key={i}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.4 + i * 0.1 }}
className={`px-4 py-3 border-b border-border-subtle ${style.border} ${style.bg}`}
>
<div className={`text-[10px] font-medium uppercase tracking-[1px] mb-1 ${style.author}`}>
{m.author}
</div>
<div className="text-[11px] leading-relaxed text-text-secondary">
{m.text}
</div>
</motion.div>
);
})}
</div>
</motion.div>
);
}

View File

@ -0,0 +1,114 @@
"use client";
import { motion } from "framer-motion";
import Panel from "./ui/Panel";
function RiskGauge({
label,
level,
color,
}: {
label: string;
level: number;
color: string;
}) {
return (
<div className="flex-1 flex flex-col items-center gap-1">
<div className="w-full h-1 bg-bg-panel rounded-sm overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${level}%` }}
transition={{ duration: 1, delay: 0.5, ease: [0.22, 1, 0.36, 1] }}
className="h-full rounded-sm"
style={{ background: color }}
/>
</div>
<span className="text-[9px] text-text-tertiary uppercase tracking-[1px]">
{label}
</span>
</div>
);
}
export default function DecisionPanel() {
return (
<Panel
title="Final Decision"
badge="Risk-Adjusted"
badgeVariant="amber"
className="col-span-2"
delay={0.35}
>
<div className="grid grid-cols-[200px_1fr_1fr_1fr] gap-4 items-center">
{/* Verdict */}
<div className="flex flex-col items-center gap-2 p-4 bg-bg-elevated rounded-lg border border-border-accent">
<div className="text-[9px] text-text-tertiary uppercase tracking-[2px]">
Final Verdict
</div>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{
duration: 0.6,
delay: 0.5,
ease: [0.34, 1.56, 0.64, 1],
}}
className="font-display font-extrabold text-[32px] tracking-[2px] text-green"
style={{ textShadow: "0 0 30px rgba(46, 204, 113, 0.15)" }}
>
BUY
</motion.div>
<div className="text-[11px] text-text-secondary">Confidence: 78%</div>
<div className="flex gap-3 w-full mt-1">
<RiskGauge label="Risk" level={60} color="var(--amber)" />
<RiskGauge label="Reward" level={85} color="var(--green)" />
</div>
</div>
{/* Position Size */}
<div className="p-3.5 bg-bg-elevated rounded-lg">
<div className="text-[9px] text-text-tertiary uppercase tracking-[1.5px] mb-2">
Position Size
</div>
<div className="font-display font-semibold text-lg text-text-primary mb-1">
12.5%
</div>
<div className="text-[10px] text-text-tertiary">of portfolio allocation</div>
<div className="mt-2 text-[10px] text-text-tertiary leading-relaxed">
Risk-adjusted by conservative analyst. Reduced from trader&apos;s initial 18%.
</div>
</div>
{/* Entry Target */}
<div className="p-3.5 bg-bg-elevated rounded-lg">
<div className="text-[9px] text-text-tertiary uppercase tracking-[1.5px] mb-2">
Entry Target
</div>
<div className="font-display font-semibold text-lg text-cyan mb-1">
$885 &ndash; $895
</div>
<div className="text-[10px] text-text-tertiary">current: $892.45</div>
<div className="mt-2 text-[10px] text-text-tertiary leading-relaxed">
Scale in on pullbacks to 50 SMA support zone at $842.
</div>
</div>
{/* Stop / Target */}
<div className="p-3.5 bg-bg-elevated rounded-lg">
<div className="text-[9px] text-text-tertiary uppercase tracking-[1.5px] mb-2">
Stop / Target
</div>
<div className="font-display font-semibold text-lg mb-1">
<span className="text-red">$820</span>
<span className="text-text-tertiary text-sm"> / </span>
<span className="text-green">$980</span>
</div>
<div className="text-[10px] text-text-tertiary">R:R ratio 1 : 2.4</div>
<div className="mt-2 text-[10px] text-text-tertiary leading-relaxed">
3-month horizon. Re-evaluate on earnings date.
</div>
</div>
</div>
</Panel>
);
}

View File

@ -0,0 +1,124 @@
"use client";
import { useMemo } from "react";
import {
AreaChart,
Area,
XAxis,
YAxis,
ResponsiveContainer,
Tooltip,
} from "recharts";
import Panel from "./ui/Panel";
function generatePriceData() {
const data = [];
let price = 870;
for (let i = 0; i < 60; i++) {
price += (Math.random() - 0.47) * 8;
data.push({
time: `${Math.floor(i / 4) + 9}:${String((i % 4) * 15).padStart(2, "0")}`,
price: Math.round(price * 100) / 100,
});
}
return data;
}
const stats = [
{ label: "Current Price", value: "$892.45", type: "up" as const },
{ label: "Day Change", value: "+$27.83 (+3.21%)", type: "up" as const },
{ label: "Volume", value: "48.2M", type: "neutral" as const },
{ label: "RSI (14)", value: "67.4", type: "neutral" as const },
{ label: "MACD Signal", value: "Bullish Cross", type: "up" as const },
{ label: "50 SMA", value: "$842.10", type: "up" as const },
{ label: "200 SMA", value: "$756.30", type: "up" as const },
{ label: "ATR (14)", value: "$18.92", type: "neutral" as const },
{ label: "Bollinger", value: "Upper Band", type: "down" as const },
];
const typeColors = {
up: "text-green border-l-green",
down: "text-red border-l-red",
neutral: "text-cyan border-l-cyan",
};
export default function MarketOverview() {
const data = useMemo(() => generatePriceData(), []);
return (
<Panel
title="Market Overview"
badge="Live"
badgeVariant="live"
className="col-span-2"
delay={0.15}
>
<div className="grid grid-cols-[1fr_280px] gap-4">
<div className="h-[200px] bg-bg-elevated rounded-lg overflow-hidden">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="priceGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#4ecdc4" stopOpacity={0.2} />
<stop offset="100%" stopColor="#4ecdc4" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="time"
tick={{ fill: "#5a5853", fontSize: 10 }}
axisLine={{ stroke: "rgba(255,255,255,0.04)" }}
tickLine={false}
interval={9}
/>
<YAxis
domain={["dataMin - 5", "dataMax + 5"]}
tick={{ fill: "#5a5853", fontSize: 10 }}
axisLine={false}
tickLine={false}
tickFormatter={(v: number) => `$${v.toFixed(0)}`}
width={48}
/>
<Tooltip
contentStyle={{
background: "#14161d",
border: "1px solid rgba(255,255,255,0.08)",
borderRadius: 8,
fontSize: 11,
color: "#e8e6e1",
}}
labelStyle={{ color: "#8a8780" }}
formatter={(value) => [`$${Number(value).toFixed(2)}`, "Price"]}
/>
<Area
type="monotone"
dataKey="price"
stroke="#4ecdc4"
strokeWidth={1.5}
fill="url(#priceGrad)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="flex flex-col gap-2 overflow-y-auto max-h-[200px]">
{stats.map((s) => (
<div
key={s.label}
className={`flex justify-between items-center px-3 py-2 bg-bg-elevated rounded border-l-2 ${typeColors[s.type]}`}
>
<span className="text-[10px] text-text-tertiary uppercase tracking-[1px]">
{s.label}
</span>
<span className={`font-display font-semibold text-[13px] ${typeColors[s.type].split(" ")[0]}`}>
{s.value}
</span>
</div>
))}
</div>
</div>
</Panel>
);
}

View File

@ -0,0 +1,56 @@
"use client";
import { motion } from "framer-motion";
const tickerData = [
{ symbol: "NVDA", price: 892.45, change: 3.21 },
{ symbol: "AAPL", price: 213.07, change: -0.45 },
{ symbol: "MSFT", price: 441.2, change: 1.87 },
{ symbol: "GOOGL", price: 178.92, change: 0.63 },
{ symbol: "TSLA", price: 248.5, change: -2.14 },
{ symbol: "META", price: 612.3, change: 4.5 },
{ symbol: "AMZN", price: 225.88, change: 1.02 },
{ symbol: "AMD", price: 178.34, change: 2.76 },
{ symbol: "INTC", price: 31.22, change: -1.55 },
{ symbol: "NFLX", price: 895.6, change: 5.12 },
];
function TickerItem({ symbol, price, change }: (typeof tickerData)[0]) {
const isUp = change >= 0;
return (
<div className="flex items-center gap-2 whitespace-nowrap text-[11px]">
<span className="font-display font-semibold text-text-primary">
{symbol}
</span>
<span className="text-text-secondary tabular-nums">
${price.toFixed(2)}
</span>
<span className={isUp ? "text-green" : "text-red"}>
{isUp ? "+" : ""}
{change.toFixed(2)}%
</span>
</div>
);
}
export default function TickerBar() {
const doubled = [...tickerData, ...tickerData];
return (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="overflow-hidden border-b border-border-subtle py-1.5"
>
<div
className="flex gap-10 w-max"
style={{ animation: "ticker-scroll 30s linear infinite" }}
>
{doubled.map((t, i) => (
<TickerItem key={`${t.symbol}-${i}`} {...t} />
))}
</div>
</motion.div>
);
}

86
ui/components/TopBar.tsx Normal file
View File

@ -0,0 +1,86 @@
"use client";
import { motion } from "framer-motion";
import { useState } from "react";
export default function TopBar() {
const [ticker, setTicker] = useState("NVDA");
const [running, setRunning] = useState(false);
const handleRun = () => {
setRunning(true);
setTimeout(() => setRunning(false), 2000);
};
return (
<motion.header
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className="flex items-center justify-between py-4 border-b border-border-subtle"
>
<div className="flex items-center gap-3.5">
<div
className="w-9 h-9 rounded-md grid place-items-center font-display font-extrabold text-base
text-bg-void tracking-tighter"
style={{
background: "linear-gradient(135deg, #d4af37, #b8941f)",
boxShadow: "0 0 20px rgba(212, 175, 55, 0.15)",
}}
>
TA
</div>
<div>
<div className="font-display font-bold text-lg tracking-tight text-text-primary">
TradingAgents
</div>
<div className="text-[11px] text-text-tertiary tracking-[2px] uppercase">
Multi-Agent Intelligence
</div>
</div>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 text-[11px] text-text-secondary">
<div
className="w-1.5 h-1.5 rounded-full bg-green"
style={{ animation: "pulse-dot 2s ease-in-out infinite" }}
/>
<span>System Online</span>
</div>
<div className="flex items-center gap-2 bg-bg-elevated border border-border-medium rounded-lg px-3 py-1.5">
<label className="text-[10px] text-text-tertiary uppercase tracking-[1px]">
Ticker
</label>
<input
type="text"
value={ticker}
onChange={(e) => setTicker(e.target.value.toUpperCase())}
onKeyDown={(e) => e.key === "Enter" && handleRun()}
maxLength={5}
spellCheck={false}
className="bg-transparent border-none outline-none text-amber font-display font-bold text-base w-20 tracking-wide"
/>
</div>
<button
onClick={handleRun}
className="border-none px-5 py-2 rounded-lg font-display font-bold text-xs tracking-[1px]
uppercase cursor-pointer transition-all duration-300 text-bg-void
hover:-translate-y-0.5"
style={{
background: running
? "linear-gradient(135deg, #4ecdc4, #3ab5ad)"
: "linear-gradient(135deg, #d4af37, #c9a020)",
boxShadow: running
? "0 0 20px rgba(78, 205, 196, 0.15)"
: "0 0 20px rgba(212, 175, 55, 0.15)",
}}
>
{running ? "Analyzing..." : "Run Analysis"}
</button>
</div>
</motion.header>
);
}

View File

@ -0,0 +1,51 @@
"use client";
import { motion } from "framer-motion";
import { ReactNode } from "react";
interface PanelProps {
title: string;
badge?: string;
badgeVariant?: "live" | "amber";
children: ReactNode;
className?: string;
delay?: number;
}
export default function Panel({
title,
badge,
badgeVariant = "amber",
children,
className = "",
delay = 0,
}: PanelProps) {
const badgeColors = {
live: "bg-green-dim text-green",
amber: "bg-amber-dim text-amber",
};
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay, ease: [0.22, 1, 0.36, 1] }}
className={`bg-bg-surface border border-border-subtle rounded-xl overflow-hidden
transition-colors duration-300 hover:border-border-medium ${className}`}
>
<div className="flex items-center justify-between px-4 pt-3.5 pb-2.5 border-b border-border-subtle">
<span className="font-display font-semibold text-[11px] tracking-[2px] uppercase text-text-secondary">
{title}
</span>
{badge && (
<span
className={`text-[9px] px-2 py-0.5 rounded-full tracking-[1px] uppercase ${badgeColors[badgeVariant]}`}
>
{badge}
</span>
)}
</div>
<div className="p-4">{children}</div>
</motion.div>
);
}

18
ui/eslint.config.mjs Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

7
ui/next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

7035
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
ui/package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "ui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"framer-motion": "^12.36.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"recharts": "^3.8.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}

7
ui/postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
ui/public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
ui/public/globe.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
ui/public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
ui/public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
ui/public/window.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

34
ui/tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}