diff --git a/frontend/src/components/charts/backtest-folds-chart.tsx b/frontend/src/components/charts/backtest-folds-chart.tsx index 98b7500d..68cacc7d 100644 --- a/frontend/src/components/charts/backtest-folds-chart.tsx +++ b/frontend/src/components/charts/backtest-folds-chart.tsx @@ -1,7 +1,9 @@ -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Legend, Cell } from 'recharts' +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Cell } from 'recharts' import { ChartConfig, ChartContainer, + ChartLegend, + ChartLegendContent, ChartTooltip, ChartTooltipContent, } from '@/components/ui/chart' @@ -76,10 +78,11 @@ export function BacktestFoldsChart({ } /> - + } /> {formattedData.map((_, index) => ( diff --git a/frontend/src/components/charts/kpi-card.tsx b/frontend/src/components/charts/kpi-card.tsx index 7f9ff6c3..d734fa2c 100644 --- a/frontend/src/components/charts/kpi-card.tsx +++ b/frontend/src/components/charts/kpi-card.tsx @@ -47,16 +47,16 @@ export function KPICard({ {Icon && } -
{value}
+
{value}
{trend && ( 0 - ? 'text-green-600 dark:text-green-400' + ? 'text-success' : trend.value < 0 - ? 'text-red-600 dark:text-red-400' + ? 'text-destructive' : '' )} > diff --git a/frontend/src/components/charts/time-series-chart.tsx b/frontend/src/components/charts/time-series-chart.tsx index bb999458..767da19a 100644 --- a/frontend/src/components/charts/time-series-chart.tsx +++ b/frontend/src/components/charts/time-series-chart.tsx @@ -1,7 +1,9 @@ -import { Area, CartesianGrid, ComposedChart, Legend, Line, XAxis, YAxis } from 'recharts' +import { Area, CartesianGrid, ComposedChart, Line, XAxis, YAxis } from 'recharts' import { ChartConfig, ChartContainer, + ChartLegend, + ChartLegendContent, ChartTooltip, ChartTooltipContent, } from '@/components/ui/chart' @@ -88,7 +90,7 @@ export function TimeSeriesChart({ /> } /> - + } /> {/* Prediction-interval band — drawn first so the forecast line sits on top. A function dataKey returns the [lower, upper] tuple recharts renders as a range area. */} diff --git a/frontend/src/components/chat/chat-input.tsx b/frontend/src/components/chat/chat-input.tsx index b4ad2876..74005673 100644 --- a/frontend/src/components/chat/chat-input.tsx +++ b/frontend/src/components/chat/chat-input.tsx @@ -87,7 +87,7 @@ export function ApprovalPrompt({ isLoading = false, }: ApprovalPromptProps) { return ( -
+

Approval Required

The agent wants to perform: {action} diff --git a/frontend/src/components/chat/tool-call-display.tsx b/frontend/src/components/chat/tool-call-display.tsx index 05e35b90..22a53713 100644 --- a/frontend/src/components/chat/tool-call-display.tsx +++ b/frontend/src/components/chat/tool-call-display.tsx @@ -18,9 +18,9 @@ export function ToolCallDisplay({ toolCall, className }: ToolCallDisplayProps) { const statusIcon = { pending: , - running: , - completed: , - failed: , + running: , + completed: , + failed: , } return ( @@ -77,9 +77,9 @@ export function ToolCallProgress({ toolName, status }: ToolCallProgressProps) { {status === 'running' || status === 'starting' ? ( ) : status === 'completed' ? ( - + ) : ( - + )} diff --git a/frontend/src/components/common/json-block.tsx b/frontend/src/components/common/json-block.tsx index 656f894c..245961eb 100644 --- a/frontend/src/components/common/json-block.tsx +++ b/frontend/src/components/common/json-block.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react' import { cn } from '@/lib/utils' interface JsonBlockProps { @@ -5,10 +6,52 @@ interface JsonBlockProps { className?: string } +// Matches a JSON string (group 1) with an optional key colon (group 2), a +// literal (group 3), or a number (group 4) in pretty-printed JSON. +const JSON_TOKEN = + /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g + +/** + * Tokenises pretty-printed JSON into syntax-highlighted spans. Token colours use + * the semantic status tokens — each verified to clear WCAG AA on the muted block + * background in both light and dark themes. + */ +function highlightJson(json: string): ReactNode[] { + const out: ReactNode[] = [] + let lastIndex = 0 + let key = 0 + let match: RegExpExecArray | null + JSON_TOKEN.lastIndex = 0 + while ((match = JSON_TOKEN.exec(json)) !== null) { + if (match.index > lastIndex) { + out.push(json.slice(lastIndex, match.index)) + } + const [token, str, colon, literal, num] = match + let className = '' + if (str !== undefined) { + className = colon ? 'text-info' : 'text-success' + } else if (literal !== undefined) { + className = 'text-destructive' + } else if (num !== undefined) { + className = 'text-warning' + } + out.push( + + {token} + , + ) + lastIndex = match.index + token.length + } + if (lastIndex < json.length) { + out.push(json.slice(lastIndex)) + } + return out +} + /** * Read-only formatted-JSON viewer. Renders a muted em-dash for null/undefined, - * otherwise a scrollable, pretty-printed

 block. Intentionally has no
- * syntax-highlighter dependency — it surfaces run/job JSONB payloads as-is.
+ * otherwise a scrollable, syntax-highlighted 
 block surfacing run/job
+ * JSONB payloads.
  */
 export function JsonBlock({ value, className }: JsonBlockProps) {
   if (value === null || value === undefined) {
@@ -22,7 +65,7 @@ export function JsonBlock({ value, className }: JsonBlockProps) {
         className,
       )}
     >
-      {JSON.stringify(value, null, 2)}
+      {highlightJson(JSON.stringify(value, null, 2))}
     
) } diff --git a/frontend/src/components/common/status-badge.tsx b/frontend/src/components/common/status-badge.tsx index 5340ce7d..ee4de6fc 100644 --- a/frontend/src/components/common/status-badge.tsx +++ b/frontend/src/components/common/status-badge.tsx @@ -7,11 +7,11 @@ const statusBadgeVariants = cva( variants: { variant: { default: 'bg-secondary text-secondary-foreground', - success: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', - warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400', - error: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400', - info: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400', - pending: 'bg-gray-100 text-gray-800 dark:bg-gray-700/30 dark:text-gray-400', + success: 'bg-success text-success-foreground', + warning: 'bg-warning text-warning-foreground', + error: 'bg-destructive text-destructive-foreground', + info: 'bg-info text-info-foreground', + pending: 'bg-muted text-muted-foreground', }, }, defaultVariants: { diff --git a/frontend/src/components/data-table/data-table.tsx b/frontend/src/components/data-table/data-table.tsx index 08379a78..858ff375 100644 --- a/frontend/src/components/data-table/data-table.tsx +++ b/frontend/src/components/data-table/data-table.tsx @@ -77,7 +77,9 @@ export function DataTable({
)} -
+ {/* Light: elevated white panel. dark: overrides keep the existing + borderless-on-canvas look unchanged. */} +
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/frontend/src/components/demo/demo-step-card.tsx b/frontend/src/components/demo/demo-step-card.tsx index 6d230066..e24b8e9d 100644 --- a/frontend/src/components/demo/demo-step-card.tsx +++ b/frontend/src/components/demo/demo-step-card.tsx @@ -15,11 +15,11 @@ const STATUS_GLYPH: Record = { // Left-border accent colour per status. const STATUS_ACCENT: Record = { idle: 'border-l-border', - running: 'border-l-blue-500', - pass: 'border-l-green-500', - fail: 'border-l-red-500', + running: 'border-l-info', + pass: 'border-l-success', + fail: 'border-l-destructive', skip: 'border-l-muted-foreground/40', - warn: 'border-l-yellow-500', + warn: 'border-l-warning', } function formatDuration(ms: number): string { @@ -50,7 +50,7 @@ function BacktestBreakdown({ data }: { data: Record }) { className={cn( 'flex items-center justify-between rounded-md px-2 py-1 text-xs', row.model === winner - ? 'bg-green-100 font-semibold dark:bg-green-900/30' + ? 'bg-success/10 font-semibold' : 'bg-muted' )} > diff --git a/frontend/src/components/layout/top-nav.tsx b/frontend/src/components/layout/top-nav.tsx index 6e2b5758..041263e5 100644 --- a/frontend/src/components/layout/top-nav.tsx +++ b/frontend/src/components/layout/top-nav.tsx @@ -59,7 +59,8 @@ export function TopNav() { to={subItem.href} className={cn( 'block select-none rounded-md p-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground', - isActive(subItem.href) && 'bg-accent/50' + isActive(subItem.href) && + 'bg-accent font-medium text-accent-foreground' )} > {subItem.label} @@ -75,7 +76,8 @@ export function TopNav() { to={item.href} className={cn( navigationMenuTriggerStyle(), - isActive(item.href) && 'bg-accent/50' + isActive(item.href) && + 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground' )} > {item.label} @@ -121,7 +123,8 @@ export function TopNav() { onClick={() => setMobileMenuOpen(false)} className={cn( 'block rounded-md px-2 py-1.5 text-sm hover:bg-accent', - isActive(subItem.href) && 'bg-accent/50 font-medium' + isActive(subItem.href) && + 'bg-accent font-medium text-accent-foreground' )} > {subItem.label} @@ -135,7 +138,8 @@ export function TopNav() { onClick={() => setMobileMenuOpen(false)} className={cn( 'block rounded-md px-2 py-1.5 text-sm font-medium hover:bg-accent', - isActive(item.href) && 'bg-accent/50' + isActive(item.href) && + 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground' )} > {item.label} diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index 681ad980..f5c4d945 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
) { return (
) diff --git a/frontend/src/index.css b/frontend/src/index.css index 3b8289ce..c2eeafad 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,25 +3,6 @@ @custom-variant dark (&:is(.dark *)); -/* shadcn/ui chart color variables */ -@layer base { - :root { - --chart-1: 221.2 83.2% 53.3%; - --chart-2: 142.1 76.2% 36.3%; - --chart-3: 47.9 95.8% 53.1%; - --chart-4: 24.6 95% 53.1%; - --chart-5: 280.1 93.6% 53.1%; - } - - .dark { - --chart-1: 217.2 91.2% 59.8%; - --chart-2: 142.1 70.6% 45.3%; - --chart-3: 47.9 95.8% 53.1%; - --chart-4: 24.6 95% 53.1%; - --chart-5: 280.1 93.6% 53.1%; - } -} - @theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); @@ -30,6 +11,10 @@ --radius-2xl: calc(var(--radius) + 8px); --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); + /* Soft, cool-tinted elevation shadow — gives white cards real lift. */ + --shadow-card: + 0 1px 2px -1px oklch(0.45 0.05 245 / 0.1), + 0 4px 12px -3px oklch(0.45 0.05 245 / 0.12); --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); @@ -45,6 +30,13 @@ --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-info: var(--info); + --color-info-foreground: var(--info-foreground); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); @@ -63,73 +55,97 @@ --color-sidebar-ring: var(--sidebar-ring); } +/* + * ForecastLabAI theme — ocean-blue brand (OKLCH hue 245). + * All foreground/background and chart/card pairs verified to clear + * WCAG AA (body text >= 4.5:1, large text & UI >= 3:1) in both themes. + */ :root { --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); + /* Cool blue-gray canvas so the pure-white cards visibly lift off it. */ + --background: oklch(0.96 0.013 245); + --foreground: oklch(0.21 0.02 245); --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); + --card-foreground: oklch(0.21 0.02 245); --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); + --popover-foreground: oklch(0.21 0.02 245); + --primary: oklch(0.5 0.18 245); --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); + --secondary: oklch(0.948 0.014 245); + --secondary-foreground: oklch(0.28 0.03 245); + --muted: oklch(0.948 0.014 245); + --muted-foreground: oklch(0.47 0.03 245); + --accent: oklch(0.92 0.05 245); + --accent-foreground: oklch(0.32 0.09 245); --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); + --destructive-foreground: oklch(0.985 0 0); + --success: oklch(0.55 0.13 150); + --success-foreground: oklch(0.985 0 0); + --warning: oklch(0.55 0.13 70); + --warning-foreground: oklch(0.99 0.01 75); + --info: oklch(0.54 0.15 245); + --info-foreground: oklch(0.985 0 0); + --border: oklch(0.87 0.02 245); + --input: oklch(0.87 0.02 245); + --ring: oklch(0.5 0.18 245); + /* Light-only muted table-header fill (dark overrides this to transparent). */ + --table-header: var(--muted); + --chart-1: oklch(0.5 0.18 245); + --chart-2: oklch(0.55 0.11 195); + --chart-3: oklch(0.63 0.16 65); + --chart-4: oklch(0.55 0.2 12); + --chart-5: oklch(0.5 0.19 305); + --sidebar: oklch(0.985 0.004 245); + --sidebar-foreground: oklch(0.21 0.02 245); + --sidebar-primary: oklch(0.5 0.18 245); --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --sidebar-accent: oklch(0.92 0.05 245); + --sidebar-accent-foreground: oklch(0.32 0.09 245); + --sidebar-border: oklch(0.87 0.02 245); + --sidebar-ring: oklch(0.5 0.18 245); } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); + --background: oklch(0.18 0.012 245); + --foreground: oklch(0.97 0.004 245); + --card: oklch(0.22 0.014 245); + --card-foreground: oklch(0.97 0.004 245); + --popover: oklch(0.22 0.014 245); + --popover-foreground: oklch(0.97 0.004 245); + --primary: oklch(0.52 0.18 245); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.27 0.015 245); + --secondary-foreground: oklch(0.97 0.004 245); + --muted: oklch(0.27 0.015 245); + --muted-foreground: oklch(0.72 0.015 245); + --accent: oklch(0.31 0.03 245); + --accent-foreground: oklch(0.97 0.004 245); --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); + --destructive-foreground: oklch(0.18 0.02 25); + --success: oklch(0.7 0.14 155); + --success-foreground: oklch(0.18 0.02 155); + --warning: oklch(0.8 0.14 80); + --warning-foreground: oklch(0.2 0.03 80); + --info: oklch(0.65 0.15 245); + --info-foreground: oklch(0.18 0.02 245); + --border: oklch(0.32 0.012 245); + --input: oklch(0.34 0.014 245); + --ring: oklch(0.55 0.16 245); + /* Preserves the existing dark table header (no fill) — do not change. */ + --table-header: transparent; + --chart-1: oklch(0.65 0.16 245); + --chart-2: oklch(0.7 0.11 195); + --chart-3: oklch(0.78 0.15 70); + --chart-4: oklch(0.68 0.18 12); + --chart-5: oklch(0.66 0.16 305); + --sidebar: oklch(0.22 0.014 245); + --sidebar-foreground: oklch(0.97 0.004 245); + --sidebar-primary: oklch(0.65 0.16 245); --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --sidebar-accent: oklch(0.31 0.03 245); + --sidebar-accent-foreground: oklch(0.97 0.004 245); + --sidebar-border: oklch(0.32 0.012 245); + --sidebar-ring: oklch(0.55 0.16 245); } @layer base { diff --git a/frontend/src/pages/chat.tsx b/frontend/src/pages/chat.tsx index 1a987401..cc22a9d5 100644 --- a/frontend/src/pages/chat.tsx +++ b/frontend/src/pages/chat.tsx @@ -240,9 +240,9 @@ export default function ChatPage() {

{agentType === 'rag_assistant' ? 'RAG Assistant' : 'Experiment Agent'} •{' '} {wsStatus === 'connected' ? ( - Connected + Connected ) : ( - {wsStatus} + {wsStatus} )}

diff --git a/frontend/src/pages/explorer/run-detail.tsx b/frontend/src/pages/explorer/run-detail.tsx index 1a41ca5e..15038b4e 100644 --- a/frontend/src/pages/explorer/run-detail.tsx +++ b/frontend/src/pages/explorer/run-detail.tsx @@ -243,8 +243,8 @@ export default function RunDetailPage() { !verifyQuery.isFetching && verifyQuery.data && (verifyQuery.data.verified ? ( -
- +
+ Artifact verified — the stored checksum matches. {verifyQuery.data.computed_hash && ( diff --git a/frontend/src/pages/showcase.tsx b/frontend/src/pages/showcase.tsx index 2b1d8687..34035812 100644 --- a/frontend/src/pages/showcase.tsx +++ b/frontend/src/pages/showcase.tsx @@ -110,7 +110,7 @@ export default function ShowcasePage() { @@ -118,7 +118,7 @@ export default function ShowcasePage() { {summary.overallStatus === 'pass'