Merge pull request #11 from Sanaullah-Turab/feat/theme-switcher
This commit is contained in:
@@ -19,7 +19,7 @@ The platform is designed local-first, allowing full local development and testin
|
|||||||
- **Provider Abstraction**: Decoupled adapters for Mock (simulation), Ollama (local), OpenAI, Anthropic, and Google Gemini.
|
- **Provider Abstraction**: Decoupled adapters for Mock (simulation), Ollama (local), OpenAI, Anthropic, and Google Gemini.
|
||||||
- **Resilience and Tiered Fallback**: Automated retry classification for transient errors (timeouts, HTTP 429, 503) and tiered fallback to local/mock alternatives.
|
- **Resilience and Tiered Fallback**: Automated retry classification for transient errors (timeouts, HTTP 429, 503) and tiered fallback to local/mock alternatives.
|
||||||
- **Budget Control Guards**: Real-time spend tracking with automated threshold interventions (80% cost optimization, 95% local-only saver, 100% block).
|
- **Budget Control Guards**: Real-time spend tracking with automated threshold interventions (80% cost optimization, 95% local-only saver, 100% block).
|
||||||
- **Traffic Control Room UI**: Real-time dark operational interface featuring live topology graphs, playground inspector, SSE live request stream, visual rules builder, and cost savings simulator.
|
- **Traffic Control Room UI**: Real-time operational interface with seamless dark/light mode switching, featuring live topology graphs, playground inspector, SSE live request stream, visual rules builder, and cost savings simulator.
|
||||||
- **Developer CLI**: Terminal diagnostics (`doctor`), routing dry-run (`route`), execution (`run`), model catalog (`models`), and analytics (`analytics`).
|
- **Developer CLI**: Terminal diagnostics (`doctor`), routing dry-run (`route`), execution (`run`), model catalog (`models`), and analytics (`analytics`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: Theme;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [theme, setTheme] = useState<Theme>(() => {
|
||||||
|
const saved = localStorage.getItem('theme');
|
||||||
|
if (saved === 'light' || saved === 'dark') return saved;
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
|
||||||
|
return 'light';
|
||||||
|
}
|
||||||
|
return 'dark';
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
root.classList.remove('light', 'dark');
|
||||||
|
root.classList.add(theme);
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(prev => (prev === 'dark' ? 'light' : 'dark'));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useTheme must be used within a ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
Compass,
|
Compass,
|
||||||
@@ -6,8 +6,11 @@ import {
|
|||||||
Cpu,
|
Cpu,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Sliders,
|
Sliders,
|
||||||
Radio
|
Radio,
|
||||||
|
Sun,
|
||||||
|
Moon
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useTheme } from '../ThemeContext';
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
@@ -16,6 +19,7 @@ interface NavbarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineStatus = true }) => {
|
export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineStatus = true }) => {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ id: 'dashboard', label: 'Control Room', icon: Activity },
|
{ id: 'dashboard', label: 'Control Room', icon: Activity },
|
||||||
{ id: 'playground', label: 'Playground', icon: Compass },
|
{ id: 'playground', label: 'Playground', icon: Compass },
|
||||||
@@ -27,7 +31,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 w-full border-b border-[#1F2833] bg-[#07090C]/90 backdrop-blur-md">
|
<header className="sticky top-0 z-50 w-full border-b border-border-subtle bg-bg-primary/90 backdrop-blur-md">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between gap-4">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between gap-4">
|
||||||
|
|
||||||
{/* Brand / Logo */}
|
{/* Brand / Logo */}
|
||||||
@@ -35,7 +39,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
|
|||||||
onClick={() => setActiveTab('dashboard')}
|
onClick={() => setActiveTab('dashboard')}
|
||||||
className="flex items-center gap-3 cursor-pointer select-none shrink-0 group"
|
className="flex items-center gap-3 cursor-pointer select-none shrink-0 group"
|
||||||
>
|
>
|
||||||
<div className="w-10 h-10 rounded-xl bg-[#0D1117] border border-accent-primary/40 flex items-center justify-center overflow-hidden shadow-[0_0_18px_rgba(56,148,255,0.25)] group-hover:border-accent-primary transition-all p-1">
|
<div className="w-10 h-10 rounded-xl bg-bg-secondary border border-accent-primary/40 flex items-center justify-center overflow-hidden shadow-[0_0_18px_rgba(56,148,255,0.25)] group-hover:border-accent-primary transition-all p-1">
|
||||||
<img
|
<img
|
||||||
src="/logo.png"
|
src="/logo.png"
|
||||||
alt="Model Router Logo"
|
alt="Model Router Logo"
|
||||||
@@ -59,7 +63,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Tabs */}
|
{/* Navigation Tabs */}
|
||||||
<nav className="hidden lg:flex items-center gap-1.5 p-1 rounded-xl bg-[#0D1117]/80 border border-[#1F2833]/80">
|
<nav className="hidden lg:flex items-center gap-1.5 p-1 rounded-xl bg-bg-secondary/80 border border-border-subtle/80">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = activeTab === item.id;
|
const isActive = activeTab === item.id;
|
||||||
@@ -70,7 +74,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
|
|||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-mono font-semibold transition-all duration-150 whitespace-nowrap ${
|
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-mono font-semibold transition-all duration-150 whitespace-nowrap ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-accent-primary text-bg-primary shadow-[0_0_12px_rgba(56,148,255,0.4)] font-bold'
|
? 'bg-accent-primary text-bg-primary shadow-[0_0_12px_rgba(56,148,255,0.4)] font-bold'
|
||||||
: 'text-text-muted hover:text-text-primary hover:bg-[#141920]'
|
: 'text-text-muted hover:text-text-primary hover:bg-bg-card'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className={`w-3.5 h-3.5 ${isActive ? 'text-bg-primary stroke-[2.5]' : 'text-text-muted stroke-[2]'}`} />
|
<Icon className={`w-3.5 h-3.5 ${isActive ? 'text-bg-primary stroke-[2.5]' : 'text-text-muted stroke-[2]'}`} />
|
||||||
@@ -104,7 +108,15 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
|
|||||||
|
|
||||||
{/* Status Indicator */}
|
{/* Status Indicator */}
|
||||||
<div className="hidden sm:flex items-center gap-2 shrink-0">
|
<div className="hidden sm:flex items-center gap-2 shrink-0">
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-[#0D1117] border border-[#1F2833] font-mono text-xs shadow-inner">
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="p-1.5 rounded-xl bg-bg-secondary border border-border-subtle text-text-muted hover:text-text-primary hover:border-border-active transition-all"
|
||||||
|
title="Toggle theme"
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-bg-secondary border border-border-subtle font-mono text-xs shadow-inner">
|
||||||
<span className={`w-2 h-2 rounded-full ${onlineStatus ? 'bg-accent-success shadow-[0_0_8px_#10B981]' : 'bg-accent-error'}`} />
|
<span className={`w-2 h-2 rounded-full ${onlineStatus ? 'bg-accent-success shadow-[0_0_8px_#10B981]' : 'bg-accent-error'}`} />
|
||||||
<span className="text-[11px] font-bold tracking-wide text-text-primary whitespace-nowrap">
|
<span className="text-[11px] font-bold tracking-wide text-text-primary whitespace-nowrap">
|
||||||
ROUTER READY
|
ROUTER READY
|
||||||
|
|||||||
+43
-19
@@ -1,17 +1,17 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-bg-primary: #07090C;
|
--color-bg-primary: var(--theme-bg-primary);
|
||||||
--color-bg-secondary: #0D1117;
|
--color-bg-secondary: var(--theme-bg-secondary);
|
||||||
--color-bg-card: #121820;
|
--color-bg-card: var(--theme-bg-card);
|
||||||
--color-bg-card-hover: #17202B;
|
--color-bg-card-hover: var(--theme-bg-card-hover);
|
||||||
|
|
||||||
--color-border-subtle: #1F2833;
|
--color-border-subtle: var(--theme-border-subtle);
|
||||||
--color-border-active: #324050;
|
--color-border-active: var(--theme-border-active);
|
||||||
|
|
||||||
--color-text-primary: #F0F4F8;
|
--color-text-primary: var(--theme-text-primary);
|
||||||
--color-text-muted: #8B98A5;
|
--color-text-muted: var(--theme-text-muted);
|
||||||
--color-text-dim: #546270;
|
--color-text-dim: var(--theme-text-dim);
|
||||||
|
|
||||||
--color-accent-primary: #3894FF;
|
--color-accent-primary: #3894FF;
|
||||||
--color-accent-secondary: #F59E0B;
|
--color-accent-secondary: #F59E0B;
|
||||||
@@ -24,19 +24,43 @@
|
|||||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--theme-bg-primary: #F8FAFC;
|
||||||
|
--theme-bg-secondary: #F1F5F9;
|
||||||
|
--theme-bg-card: #FFFFFF;
|
||||||
|
--theme-bg-card-hover: #F8FAFC;
|
||||||
|
--theme-border-subtle: #E2E8F0;
|
||||||
|
--theme-border-active: #CBD5E1;
|
||||||
|
--theme-text-primary: #0F172A;
|
||||||
|
--theme-text-muted: #475569;
|
||||||
|
--theme-text-dim: #64748B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--theme-bg-primary: #07090C;
|
||||||
|
--theme-bg-secondary: #0D1117;
|
||||||
|
--theme-bg-card: #121820;
|
||||||
|
--theme-bg-card-hover: #17202B;
|
||||||
|
--theme-border-subtle: #1F2833;
|
||||||
|
--theme-border-active: #324050;
|
||||||
|
--theme-text-primary: #F0F4F8;
|
||||||
|
--theme-text-muted: #8B98A5;
|
||||||
|
--theme-text-dim: #546270;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #07090C;
|
background-color: var(--theme-bg-primary);
|
||||||
color: #F0F4F8;
|
color: var(--theme-text-primary);
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom glow borders and glass effects */
|
/* Custom glow borders and glass effects */
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
background: rgba(18, 24, 32, 0.75);
|
background: color-mix(in srgb, var(--theme-bg-card) 75%, transparent);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
border: 1px solid rgba(40, 52, 68, 0.6);
|
border: 1px solid color-mix(in srgb, var(--theme-border-active) 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-panel-hover:hover {
|
.glass-panel-hover:hover {
|
||||||
@@ -48,8 +72,8 @@ body {
|
|||||||
.grid-bg {
|
.grid-bg {
|
||||||
background-size: 28px 28px;
|
background-size: 28px 28px;
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(to right, rgba(31, 40, 51, 0.4) 1px, transparent 1px),
|
linear-gradient(to right, color-mix(in srgb, var(--theme-border-subtle) 40%, transparent) 1px, transparent 1px),
|
||||||
linear-gradient(to bottom, rgba(31, 40, 51, 0.4) 1px, transparent 1px);
|
linear-gradient(to bottom, color-mix(in srgb, var(--theme-border-subtle) 40%, transparent) 1px, transparent 1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radar-glow {
|
.radar-glow {
|
||||||
@@ -83,12 +107,12 @@ body {
|
|||||||
height: 6px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #07090C;
|
background: var(--theme-bg-primary);
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #1F2833;
|
background: var(--theme-border-subtle);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #324050;
|
background: var(--theme-border-active);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import { ThemeProvider } from './ThemeContext'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Sliders, Shield, DollarSign, Cpu, CheckCircle } from 'lucide-react';
|
import { Sliders, Shield, DollarSign, Cpu, CheckCircle, Sun, Moon } from 'lucide-react';
|
||||||
import { fetchApi } from '../lib/api';
|
import { fetchApi } from '../lib/api';
|
||||||
|
import { useTheme } from '../ThemeContext';
|
||||||
|
|
||||||
export const SettingsPage: React.FC = () => {
|
export const SettingsPage: React.FC = () => {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
const [budget, setBudget] = useState<{
|
const [budget, setBudget] = useState<{
|
||||||
daily_limit: number;
|
daily_limit: number;
|
||||||
monthly_limit: number;
|
monthly_limit: number;
|
||||||
@@ -17,14 +19,24 @@ export const SettingsPage: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 max-w-4xl mx-auto pb-16 font-mono">
|
<div className="space-y-8 max-w-4xl mx-auto pb-16 font-mono">
|
||||||
<div className="border-b border-border-subtle pb-4">
|
<div className="border-b border-border-subtle pb-4 flex justify-between items-start">
|
||||||
<h2 className="text-xl font-bold text-text-primary tracking-wide flex items-center gap-2">
|
<div>
|
||||||
<Sliders className="w-5 h-5 text-accent-primary" />
|
<h2 className="text-xl font-bold text-text-primary tracking-wide flex items-center gap-2">
|
||||||
SYSTEM CONFIGURATION & BUDGET GUARDS
|
<Sliders className="w-5 h-5 text-accent-primary" />
|
||||||
</h2>
|
SYSTEM CONFIGURATION & BUDGET GUARDS
|
||||||
<p className="text-xs text-text-muted mt-1">
|
</h2>
|
||||||
Configure daily and monthly spend limits, routing threshold interventions, and environment parameters.
|
<p className="text-xs text-text-muted mt-1">
|
||||||
</p>
|
Configure daily and monthly spend limits, routing threshold interventions, and environment parameters.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-secondary border border-border-subtle text-text-muted hover:text-text-primary hover:border-border-active transition-all text-xs font-bold"
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||||
|
{theme === 'dark' ? 'LIGHT MODE' : 'DARK MODE'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Budget Management */}
|
{/* Budget Management */}
|
||||||
|
|||||||
Reference in New Issue
Block a user