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.
|
||||
- **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).
|
||||
- **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`).
|
||||
|
||||
---
|
||||
|
||||
@@ -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 {
|
||||
Activity,
|
||||
Compass,
|
||||
@@ -6,8 +6,11 @@ import {
|
||||
Cpu,
|
||||
BarChart3,
|
||||
Sliders,
|
||||
Radio
|
||||
Radio,
|
||||
Sun,
|
||||
Moon
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '../ThemeContext';
|
||||
|
||||
interface NavbarProps {
|
||||
activeTab: string;
|
||||
@@ -16,6 +19,7 @@ interface NavbarProps {
|
||||
}
|
||||
|
||||
export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineStatus = true }) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const navItems = [
|
||||
{ id: 'dashboard', label: 'Control Room', icon: Activity },
|
||||
{ id: 'playground', label: 'Playground', icon: Compass },
|
||||
@@ -27,7 +31,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
|
||||
];
|
||||
|
||||
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">
|
||||
|
||||
{/* Brand / Logo */}
|
||||
@@ -35,7 +39,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
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
|
||||
src="/logo.png"
|
||||
alt="Model Router Logo"
|
||||
@@ -59,7 +63,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
|
||||
</div>
|
||||
|
||||
{/* 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) => {
|
||||
const Icon = item.icon;
|
||||
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 ${
|
||||
isActive
|
||||
? '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]'}`} />
|
||||
@@ -104,7 +108,15 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineS
|
||||
|
||||
{/* Status Indicator */}
|
||||
<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="text-[11px] font-bold tracking-wide text-text-primary whitespace-nowrap">
|
||||
ROUTER READY
|
||||
|
||||
+43
-19
@@ -1,17 +1,17 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-bg-primary: #07090C;
|
||||
--color-bg-secondary: #0D1117;
|
||||
--color-bg-card: #121820;
|
||||
--color-bg-card-hover: #17202B;
|
||||
--color-bg-primary: var(--theme-bg-primary);
|
||||
--color-bg-secondary: var(--theme-bg-secondary);
|
||||
--color-bg-card: var(--theme-bg-card);
|
||||
--color-bg-card-hover: var(--theme-bg-card-hover);
|
||||
|
||||
--color-border-subtle: #1F2833;
|
||||
--color-border-active: #324050;
|
||||
--color-border-subtle: var(--theme-border-subtle);
|
||||
--color-border-active: var(--theme-border-active);
|
||||
|
||||
--color-text-primary: #F0F4F8;
|
||||
--color-text-muted: #8B98A5;
|
||||
--color-text-dim: #546270;
|
||||
--color-text-primary: var(--theme-text-primary);
|
||||
--color-text-muted: var(--theme-text-muted);
|
||||
--color-text-dim: var(--theme-text-dim);
|
||||
|
||||
--color-accent-primary: #3894FF;
|
||||
--color-accent-secondary: #F59E0B;
|
||||
@@ -24,19 +24,43 @@
|
||||
--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 {
|
||||
background-color: #07090C;
|
||||
color: #F0F4F8;
|
||||
background-color: var(--theme-bg-primary);
|
||||
color: var(--theme-text-primary);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Custom glow borders and glass effects */
|
||||
.glass-panel {
|
||||
background: rgba(18, 24, 32, 0.75);
|
||||
background: color-mix(in srgb, var(--theme-bg-card) 75%, transparent);
|
||||
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 {
|
||||
@@ -48,8 +72,8 @@ body {
|
||||
.grid-bg {
|
||||
background-size: 28px 28px;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(31, 40, 51, 0.4) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, 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, color-mix(in srgb, var(--theme-border-subtle) 40%, transparent) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.radar-glow {
|
||||
@@ -83,12 +107,12 @@ body {
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #07090C;
|
||||
background: var(--theme-bg-primary);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #1F2833;
|
||||
background: var(--theme-border-subtle);
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-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 App from './App'
|
||||
import { ThemeProvider } from './ThemeContext'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Sliders, Shield, DollarSign, Cpu, CheckCircle } from 'lucide-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Sliders, Shield, DollarSign, Cpu, CheckCircle, Sun, Moon } from 'lucide-react';
|
||||
import { fetchApi } from '../lib/api';
|
||||
import { useTheme } from '../ThemeContext';
|
||||
|
||||
export const SettingsPage: React.FC = () => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const [budget, setBudget] = useState<{
|
||||
daily_limit: number;
|
||||
monthly_limit: number;
|
||||
@@ -17,14 +19,24 @@ export const SettingsPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-4xl mx-auto pb-16 font-mono">
|
||||
<div className="border-b border-border-subtle pb-4">
|
||||
<h2 className="text-xl font-bold text-text-primary tracking-wide flex items-center gap-2">
|
||||
<Sliders className="w-5 h-5 text-accent-primary" />
|
||||
SYSTEM CONFIGURATION & BUDGET GUARDS
|
||||
</h2>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Configure daily and monthly spend limits, routing threshold interventions, and environment parameters.
|
||||
</p>
|
||||
<div className="border-b border-border-subtle pb-4 flex justify-between items-start">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-text-primary tracking-wide flex items-center gap-2">
|
||||
<Sliders className="w-5 h-5 text-accent-primary" />
|
||||
SYSTEM CONFIGURATION & BUDGET GUARDS
|
||||
</h2>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
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>
|
||||
|
||||
{/* Budget Management */}
|
||||
|
||||
Reference in New Issue
Block a user