feat: add ModelAvailabilityBadge component and retheme landing page

- Add compact ModelAvailabilityBadge with popover for model status
  monitoring, cooldown clearing, and auto-refresh polling
- Retheme landing page from warm orange tones to cool blue-grey palette
  across FlowAnimation, Footer, and related components
- Update accent color from orange (#f97815) to rose (#E54D5E)
This commit is contained in:
diegosouzapw
2026-02-15 16:26:01 -03:00
parent 14714aa9f5
commit 413a9f2a69
14 changed files with 335 additions and 134 deletions

View File

@@ -0,0 +1,194 @@
"use client";
/**
* ModelAvailabilityBadge — compact inline status indicator
*
* Replaces the full ModelAvailabilityPanel card with a small badge
* that shows green when all models are operational, or amber/red
* when there are issues, with a hover popover for details.
*/
import { useState, useEffect, useCallback, useRef } from "react";
import { Button } from "@/shared/components";
import { useNotificationStore } from "@/store/notificationStore";
const STATUS_CONFIG = {
available: { icon: "check_circle", color: "#22c55e", label: "Available" },
cooldown: { icon: "schedule", color: "#f59e0b", label: "Cooldown" },
unavailable: { icon: "error", color: "#ef4444", label: "Unavailable" },
unknown: { icon: "help", color: "#6b7280", label: "Unknown" },
};
export default function ModelAvailabilityBadge() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [expanded, setExpanded] = useState(false);
const [clearing, setClearing] = useState(null);
const ref = useRef(null);
const notify = useNotificationStore();
const fetchStatus = useCallback(async () => {
try {
const res = await fetch("/api/models/availability");
if (res.ok) {
const json = await res.json();
setData(json);
}
} catch {
// silent fail — will retry
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchStatus();
const interval = setInterval(fetchStatus, 30000);
return () => clearInterval(interval);
}, [fetchStatus]);
// Close popover on outside click
useEffect(() => {
const handleClick = (e) => {
if (ref.current && !ref.current.contains(e.target)) {
setExpanded(false);
}
};
if (expanded) document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [expanded]);
const handleClearCooldown = async (provider, model) => {
setClearing(`${provider}:${model}`);
try {
const res = await fetch("/api/models/availability", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "clearCooldown", provider, model }),
});
if (res.ok) {
notify.success(`Cooldown cleared for ${model}`);
await fetchStatus();
} else {
notify.error("Failed to clear cooldown");
}
} catch {
notify.error("Failed to clear cooldown");
} finally {
setClearing(null);
}
};
if (loading) return null;
const models = data?.models || [];
const unavailableCount =
data?.unavailableCount || models.filter((m) => m.status !== "available").length;
const isHealthy = unavailableCount === 0;
// Group unhealthy models by provider
const byProvider = {};
models.forEach((m) => {
if (m.status === "available") return;
const key = m.provider || "unknown";
if (!byProvider[key]) byProvider[key] = [];
byProvider[key].push(m);
});
return (
<div className="relative" ref={ref}>
<button
onClick={() => setExpanded(!expanded)}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-all ${
isHealthy
? "bg-emerald-500/10 border-emerald-500/20 text-emerald-500 hover:bg-emerald-500/15"
: "bg-amber-500/10 border-amber-500/20 text-amber-500 hover:bg-amber-500/15"
}`}
>
<span className="material-symbols-outlined text-[14px]">
{isHealthy ? "verified" : "warning"}
</span>
{isHealthy
? "All models operational"
: `${unavailableCount} model${unavailableCount !== 1 ? "s" : ""} with issues`}
</button>
{/* Expanded popover */}
{expanded && (
<div className="absolute top-full right-0 mt-2 w-80 bg-surface border border-border rounded-xl shadow-2xl z-50 overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-bg">
<div className="flex items-center gap-2">
<span
className="material-symbols-outlined text-[16px]"
style={{ color: isHealthy ? "#22c55e" : "#f59e0b" }}
>
{isHealthy ? "verified" : "warning"}
</span>
<span className="text-sm font-semibold text-text-main">Model Status</span>
</div>
<button
onClick={fetchStatus}
className="p-1 rounded-lg hover:bg-surface text-text-muted hover:text-text-main transition-colors"
title="Refresh"
>
<span className="material-symbols-outlined text-[14px]">refresh</span>
</button>
</div>
<div className="px-4 py-3 max-h-60 overflow-y-auto">
{isHealthy ? (
<p className="text-sm text-text-muted text-center py-2">
All models are responding normally.
</p>
) : (
<div className="flex flex-col gap-2.5">
{Object.entries(byProvider).map(([provider, provModels]) => (
<div key={provider}>
<p className="text-xs font-semibold text-text-main mb-1.5 capitalize">
{provider}
</p>
<div className="flex flex-col gap-1">
{provModels.map((m) => {
const status = STATUS_CONFIG[m.status] || STATUS_CONFIG.unknown;
const isClearing = clearing === `${m.provider}:${m.model}`;
return (
<div
key={`${m.provider}-${m.model}`}
className="flex items-center justify-between px-2.5 py-1.5 rounded-lg bg-surface/30"
>
<div className="flex items-center gap-1.5 min-w-0">
<span
className="material-symbols-outlined text-[14px] shrink-0"
style={{ color: status.color }}
>
{status.icon}
</span>
<span className="font-mono text-xs text-text-main truncate">
{m.model}
</span>
</div>
{m.status === "cooldown" && (
<Button
size="sm"
variant="ghost"
onClick={() => handleClearCooldown(m.provider, m.model)}
disabled={isClearing}
className="text-[10px] px-1.5! py-0.5! ml-2"
>
{isClearing ? "..." : "Clear"}
</Button>
)}
</div>
);
})}
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
);
}

View File

@@ -13,7 +13,7 @@ import {
import Link from "next/link";
import { getErrorCode, getRelativeTime } from "@/shared/utils";
import { useNotificationStore } from "@/store/notificationStore";
import ModelAvailabilityPanel from "./components/ModelAvailabilityPanel";
import ModelAvailabilityBadge from "./components/ModelAvailabilityBadge";
// Shared helper function to avoid code duplication between ProviderCard and ApiKeyProviderCard
function getStatusDisplay(connected, error, errorCode) {
@@ -203,22 +203,25 @@ export default function ProvidersPage() {
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">OAuth Providers</h2>
<button
onClick={() => handleBatchTest("oauth")}
disabled={!!testingMode}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
testingMode === "oauth"
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
: "bg-bg-subtle border-border text-text-muted hover:text-text-primary hover:border-primary/40"
}`}
title="Test all OAuth connections"
aria-label="Test all OAuth connections"
>
<span className="material-symbols-outlined text-[14px]">
{testingMode === "oauth" ? "sync" : "play_arrow"}
</span>
{testingMode === "oauth" ? "Testing..." : "Test All"}
</button>
<div className="flex items-center gap-2">
<ModelAvailabilityBadge />
<button
onClick={() => handleBatchTest("oauth")}
disabled={!!testingMode}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
testingMode === "oauth"
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
: "bg-bg-subtle border-border text-text-muted hover:text-text-primary hover:border-primary/40"
}`}
title="Test all OAuth connections"
aria-label="Test all OAuth connections"
>
<span className="material-symbols-outlined text-[14px]">
{testingMode === "oauth" ? "sync" : "play_arrow"}
</span>
{testingMode === "oauth" ? "Testing..." : "Test All"}
</button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{Object.entries(OAUTH_PROVIDERS).map(([key, info]) => (
@@ -400,9 +403,6 @@ export default function ProvidersPage() {
</div>
</div>
)}
{/* Model Availability */}
<ModelAvailabilityPanel />
</div>
);
}

View File

@@ -350,10 +350,10 @@ export default function ProviderLimits() {
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold cursor-pointer"
style={{
border: active
? "1px solid var(--primary, #f97815)"
? "1px solid var(--primary, #E54D5E)"
: "1px solid rgba(255,255,255,0.12)",
background: active ? "rgba(249,120,21,0.14)" : "transparent",
color: active ? "var(--primary, #f97815)" : "var(--text-muted)",
color: active ? "var(--primary, #E54D5E)" : "var(--text-muted)",
}}
>
<span>{tier.label}</span>

View File

@@ -3,40 +3,40 @@
@custom-variant dark (&:where(.dark, .dark *));
/* macOS-inspired Color Palette with Terracotta Primary */
/* OpenClaw × ClawHub Color Palette */
:root {
/* Primary - Warm Coral/Terracotta */
--color-primary: #d97757;
--color-primary-hover: #c56243;
/* Primary - Coral Red (OpenClaw) */
--color-primary: #e54d5e;
--color-primary-hover: #c93d4e;
/* Light theme */
--color-bg: #fbf9f6;
--color-bg-alt: #f5f1ed;
--color-bg: #f9f9fb;
--color-bg-alt: #f0f0f5;
--color-surface: #ffffff;
--color-sidebar: rgba(246, 246, 246, 0.8);
--color-border: rgba(0, 0, 0, 0.1);
--color-text-main: #383733;
--color-text-muted: #75736e;
--color-sidebar: rgba(245, 245, 250, 0.8);
--color-border: rgba(0, 0, 0, 0.08);
--color-text-main: #1a1a2e;
--color-text-muted: #71717a;
/* Shadows - subtle macOS style */
/* Shadows */
--shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.02), 0 4px 12px rgba(0, 0, 0, 0.015);
--shadow-warm: 0 2px 12px -2px rgba(217, 119, 87, 0.12);
--shadow-elevated: 0 12px 28px -4px rgba(60, 50, 45, 0.06);
--shadow-warm: 0 2px 12px -2px rgba(229, 77, 94, 0.12);
--shadow-elevated: 0 12px 28px -4px rgba(20, 20, 40, 0.06);
}
.dark {
/* Dark theme */
--color-bg: #191918;
--color-bg-alt: #1f1f1e;
--color-surface: #242423;
--color-sidebar: rgba(30, 30, 30, 0.8);
--color-border: rgba(255, 255, 255, 0.1);
--color-text-main: #ecebe8;
--color-text-muted: #9e9d99;
/* Dark theme (ClawHub deep) */
--color-bg: #0b0e14;
--color-bg-alt: #111520;
--color-surface: #161b22;
--color-sidebar: rgba(16, 20, 30, 0.8);
--color-border: rgba(255, 255, 255, 0.08);
--color-text-main: #e6e6ef;
--color-text-muted: #a1a1aa;
/* Dark shadows - subtle macOS style */
/* Dark shadows */
--shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-warm: 0 2px 12px -2px rgba(217, 119, 87, 0.15);
--shadow-warm: 0 2px 12px -2px rgba(229, 77, 94, 0.15);
--shadow-elevated: 0 12px 28px -4px rgba(0, 0, 0, 0.3);
}
@@ -54,18 +54,18 @@
--color-text-muted: var(--color-text-muted);
/* Static colors (for explicit light/dark usage) */
--color-bg-light: #fbf9f6;
--color-bg-dark: #191918;
--color-bg-light: #f9f9fb;
--color-bg-dark: #0b0e14;
--color-surface-light: #ffffff;
--color-surface-dark: #242423;
--color-sidebar-light: #f0efec;
--color-sidebar-dark: #1f1f1e;
--color-border-light: #e6e4dd;
--color-border-dark: #333331;
--color-text-main-light: #383733;
--color-text-main-dark: #ecebe8;
--color-text-muted-light: #75736e;
--color-text-muted-dark: #9e9d99;
--color-surface-dark: #161b22;
--color-sidebar-light: #ededf2;
--color-sidebar-dark: #111520;
--color-border-light: #e2e2ea;
--color-border-dark: #2d333b;
--color-text-main-light: #1a1a2e;
--color-text-main-dark: #e6e6ef;
--color-text-muted-light: #71717a;
--color-text-muted-dark: #a1a1aa;
/* Shadows */
--shadow-soft: var(--shadow-soft);
@@ -88,7 +88,7 @@ body {
/* Selection */
::selection {
background-color: rgba(217, 119, 87, 0.2);
background-color: rgba(229, 77, 94, 0.2);
color: var(--color-primary);
}
@@ -142,11 +142,11 @@ body {
/* Hero gradient */
.bg-hero-gradient {
background: linear-gradient(180deg, #f5f1ed 0%, #fefcfb 100%);
background: linear-gradient(180deg, #f0f0f5 0%, #f9f9fb 100%);
}
.dark .bg-hero-gradient {
background: linear-gradient(180deg, #1f1f1e 0%, #191918 100%);
background: linear-gradient(180deg, #111520 0%, #0b0e14 100%);
}
/* Material Symbols */
@@ -219,15 +219,15 @@ button .material-symbols-outlined,
0%,
100% {
box-shadow:
0 0 5px rgba(217, 119, 87, 0.3),
0 0 10px rgba(217, 119, 87, 0.2);
border-color: rgba(217, 119, 87, 0.5);
0 0 5px rgba(229, 77, 94, 0.3),
0 0 10px rgba(229, 77, 94, 0.2);
border-color: rgba(229, 77, 94, 0.5);
}
50% {
box-shadow:
0 0 10px rgba(217, 119, 87, 0.5),
0 0 20px rgba(217, 119, 87, 0.3);
border-color: rgba(217, 119, 87, 0.8);
0 0 10px rgba(229, 77, 94, 0.5),
0 0 20px rgba(229, 77, 94, 0.3);
border-color: rgba(229, 77, 94, 0.8);
}
}
@@ -243,7 +243,7 @@ button .material-symbols-outlined,
}
.dark .bg-vibrancy {
background: rgba(30, 30, 30, 0.72);
background: rgba(16, 20, 30, 0.72);
}
/* macOS Traffic Lights */

View File

@@ -9,13 +9,13 @@ export default function AnimatedBackground() {
<div
className="absolute inset-0 opacity-[0.08]"
style={{
backgroundImage: `linear-gradient(to right, #f97815 1px, transparent 1px), linear-gradient(to bottom, #f97815 1px, transparent 1px)`,
backgroundImage: `linear-gradient(to right, #E54D5E 1px, transparent 1px), linear-gradient(to bottom, #E54D5E 1px, transparent 1px)`,
backgroundSize: "50px 50px",
}}
/>
{/* Animated gradient orbs */}
<div className="absolute -top-20 left-1/4 w-[600px] h-[600px] bg-[#f97815]/20 rounded-full blur-[120px] animate-blob" />
<div className="absolute -top-20 left-1/4 w-[600px] h-[600px] bg-[#E54D5E]/20 rounded-full blur-[120px] animate-blob" />
<div className="absolute top-1/3 -right-20 w-[500px] h-[500px] bg-purple-500/15 rounded-full blur-[120px] animate-blob-delayed-1" />
<div className="absolute -bottom-20 left-1/2 w-[550px] h-[550px] bg-blue-500/12 rounded-full blur-[120px] animate-blob-delayed-2" />
@@ -24,7 +24,7 @@ export default function AnimatedBackground() {
className="absolute inset-0"
style={{
background:
"radial-gradient(circle at center, transparent 0%, rgba(24, 20, 17, 0.4) 100%)",
"radial-gradient(circle at center, transparent 0%, rgba(11, 14, 20, 0.4) 100%)",
}}
/>
</div>

View File

@@ -29,10 +29,10 @@ export default function FlowAnimation() {
return (
<div className="mt-16 w-full max-w-4xl relative h-[360px] hidden md:flex items-center justify-center animate-[float_6s_ease-in-out_infinite]">
{/* OmniRoute Hub - Center */}
<div className="relative z-20 w-32 h-32 rounded-full bg-[#23180f] border-2 border-[#f97815] shadow-[0_0_40px_rgba(249,120,21,0.3)] flex flex-col items-center justify-center gap-1 group cursor-pointer hover:scale-105 transition-transform duration-500">
<span className="material-symbols-outlined text-4xl text-[#f97815]">hub</span>
<div className="relative z-20 w-32 h-32 rounded-full bg-[#111520] border-2 border-[#E54D5E] shadow-[0_0_40px_rgba(229,77,94,0.3)] flex flex-col items-center justify-center gap-1 group cursor-pointer hover:scale-105 transition-transform duration-500">
<span className="material-symbols-outlined text-4xl text-[#E54D5E]">hub</span>
<span className="text-xs font-bold text-white tracking-widest uppercase">OmniRoute</span>
<div className="absolute inset-0 rounded-full border border-[#f97815]/30 animate-ping opacity-20"></div>
<div className="absolute inset-0 rounded-full border border-[#E54D5E]/30 animate-ping opacity-20"></div>
</div>
{/* CLI Tools - Left side */}
@@ -42,7 +42,7 @@ export default function FlowAnimation() {
key={tool.id}
className="flex items-center gap-3 opacity-70 hover:opacity-100 transition-opacity group"
>
<div className="w-16 h-16 rounded-2xl bg-[#23180f] border border-[#3a2f27] flex items-center justify-center overflow-hidden p-2 hover:border-[#f97815]/50 transition-all hover:scale-105">
<div className="w-16 h-16 rounded-2xl bg-[#111520] border border-[#2D333B] flex items-center justify-center overflow-hidden p-2 hover:border-[#E54D5E]/50 transition-all hover:scale-105">
<Image
src={tool.image}
alt={tool.name}
@@ -99,28 +99,28 @@ export default function FlowAnimation() {
<path
d="M 440 180 C 550 180, 550 50, 740 50"
fill="none"
stroke={activeFlow === 0 ? "#f97815" : "rgb(75, 85, 99)"}
stroke={activeFlow === 0 ? "#E54D5E" : "rgb(75, 85, 99)"}
strokeWidth={activeFlow === 0 ? "3" : "2"}
className={activeFlow === 0 ? "animate-pulse" : ""}
></path>
<path
d="M 440 180 C 550 180, 550 130, 740 130"
fill="none"
stroke={activeFlow === 1 ? "#f97815" : "rgb(75, 85, 99)"}
stroke={activeFlow === 1 ? "#E54D5E" : "rgb(75, 85, 99)"}
strokeWidth={activeFlow === 1 ? "3" : "2"}
className={activeFlow === 1 ? "animate-pulse" : ""}
></path>
<path
d="M 440 180 C 550 180, 550 230, 740 230"
fill="none"
stroke={activeFlow === 2 ? "#f97815" : "rgb(75, 85, 99)"}
stroke={activeFlow === 2 ? "#E54D5E" : "rgb(75, 85, 99)"}
strokeWidth={activeFlow === 2 ? "3" : "2"}
className={activeFlow === 2 ? "animate-pulse" : ""}
></path>
<path
d="M 440 180 C 550 180, 550 310, 740 310"
fill="none"
stroke={activeFlow === 3 ? "#f97815" : "rgb(75, 85, 99)"}
stroke={activeFlow === 3 ? "#E54D5E" : "rgb(75, 85, 99)"}
strokeWidth={activeFlow === 3 ? "3" : "2"}
className={activeFlow === 3 ? "animate-pulse" : ""}
></path>
@@ -132,7 +132,7 @@ export default function FlowAnimation() {
<div
key={provider.id}
className={`px-4 py-2 rounded-lg ${provider.color} ${provider.textColor} flex items-center justify-center font-bold text-xs shadow-lg hover:scale-110 transition-all cursor-help min-w-[140px] ${
activeFlow === idx ? "ring-4 ring-[#f97815]/50 scale-110" : ""
activeFlow === idx ? "ring-4 ring-[#E54D5E]/50 scale-110" : ""
}`}
title={provider.name}
>
@@ -142,7 +142,7 @@ export default function FlowAnimation() {
</div>
{/* Mobile fallback */}
<div className="md:hidden mt-8 w-full p-4 rounded-lg bg-[#23180f] border border-[#3a2f27]">
<div className="md:hidden mt-8 w-full p-4 rounded-lg bg-[#111520] border border-[#2D333B]">
<p className="text-sm text-center text-gray-400">Interactive diagram visible on desktop</p>
</div>
</div>

View File

@@ -2,13 +2,13 @@
export default function Footer() {
return (
<footer className="border-t border-[#3a2f27] bg-[#120f0d] pt-16 pb-8 px-6">
<footer className="border-t border-[#2D333B] bg-[#080A0F] pt-16 pb-8 px-6">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8 mb-16">
{/* Brand */}
<div className="col-span-2 lg:col-span-2">
<div className="flex items-center gap-3 mb-6">
<div className="size-6 rounded bg-[#f97815] flex items-center justify-center text-white">
<div className="size-6 rounded bg-[#E54D5E] flex items-center justify-center text-white">
<span className="material-symbols-outlined text-[16px]">hub</span>
</div>
<h3 className="text-white text-lg font-bold">OmniRoute</h3>
@@ -33,19 +33,19 @@ export default function Footer() {
<div className="flex flex-col gap-4">
<h4 className="font-bold text-white">Product</h4>
<a
className="text-gray-400 hover:text-[#f97815] text-sm transition-colors"
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="#features"
>
Features
</a>
<a
className="text-gray-400 hover:text-[#f97815] text-sm transition-colors"
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="/dashboard"
>
Dashboard
</a>
<a
className="text-gray-400 hover:text-[#f97815] text-sm transition-colors"
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="https://github.com/decolua/omniroute/releases"
target="_blank"
rel="noopener noreferrer"
@@ -58,13 +58,13 @@ export default function Footer() {
<div className="flex flex-col gap-4">
<h4 className="font-bold text-white">Resources</h4>
<a
className="text-gray-400 hover:text-[#f97815] text-sm transition-colors"
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="/docs"
>
Documentation
</a>
<a
className="text-gray-400 hover:text-[#f97815] text-sm transition-colors"
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="https://github.com/decolua/omniroute"
target="_blank"
rel="noopener noreferrer"
@@ -72,7 +72,7 @@ export default function Footer() {
GitHub
</a>
<a
className="text-gray-400 hover:text-[#f97815] text-sm transition-colors"
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="https://www.npmjs.com/package/omniroute"
target="_blank"
rel="noopener noreferrer"
@@ -85,7 +85,7 @@ export default function Footer() {
<div className="flex flex-col gap-4">
<h4 className="font-bold text-white">Legal</h4>
<a
className="text-gray-400 hover:text-[#f97815] text-sm transition-colors"
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="https://github.com/decolua/omniroute/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
@@ -96,7 +96,7 @@ export default function Footer() {
</div>
{/* Bottom */}
<div className="border-t border-[#3a2f27] pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<div className="border-t border-[#2D333B] pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-gray-600 text-sm">© 2025 OmniRoute. All rights reserved.</p>
<div className="flex gap-6">
<a

View File

@@ -11,7 +11,7 @@ export default function GetStarted() {
};
return (
<section className="py-24 px-6 bg-[#120f0d]">
<section className="py-24 px-6 bg-[#080A0F]">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col lg:flex-row gap-16 items-start">
{/* Left: Steps */}
@@ -24,7 +24,7 @@ export default function GetStarted() {
<div className="flex flex-col gap-6">
<div className="flex gap-4">
<div className="flex-none w-8 h-8 rounded-full bg-[#f97815]/20 text-[#f97815] flex items-center justify-center font-bold">
<div className="flex-none w-8 h-8 rounded-full bg-[#E54D5E]/20 text-[#E54D5E] flex items-center justify-center font-bold">
1
</div>
<div>
@@ -36,7 +36,7 @@ export default function GetStarted() {
</div>
<div className="flex gap-4">
<div className="flex-none w-8 h-8 rounded-full bg-[#f97815]/20 text-[#f97815] flex items-center justify-center font-bold">
<div className="flex-none w-8 h-8 rounded-full bg-[#E54D5E]/20 text-[#E54D5E] flex items-center justify-center font-bold">
2
</div>
<div>
@@ -48,7 +48,7 @@ export default function GetStarted() {
</div>
<div className="flex gap-4">
<div className="flex-none w-8 h-8 rounded-full bg-[#f97815]/20 text-[#f97815] flex items-center justify-center font-bold">
<div className="flex-none w-8 h-8 rounded-full bg-[#E54D5E]/20 text-[#E54D5E] flex items-center justify-center font-bold">
3
</div>
<div>
@@ -63,9 +63,9 @@ export default function GetStarted() {
{/* Right: Code block */}
<div className="flex-1 w-full">
<div className="rounded-xl overflow-hidden bg-[#1e1e1e] border border-[#3a2f27] shadow-2xl">
<div className="rounded-xl overflow-hidden bg-[#161B22] border border-[#2D333B] shadow-2xl">
{/* Terminal header */}
<div className="flex items-center gap-2 px-4 py-3 bg-[#252526] border-b border-gray-700">
<div className="flex items-center gap-2 px-4 py-3 bg-[#111520] border-b border-gray-700">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<div className="w-3 h-3 rounded-full bg-green-500"></div>
@@ -86,12 +86,12 @@ export default function GetStarted() {
</div>
<div className="text-gray-400 mb-6">
<span className="text-[#f97815]">&gt;</span> Starting OmniRoute...
<span className="text-[#E54D5E]">&gt;</span> Starting OmniRoute...
<br />
<span className="text-[#f97815]">&gt;</span> Server running on{" "}
<span className="text-[#E54D5E]">&gt;</span> Server running on{" "}
<span className="text-blue-400">http://localhost:20128</span>
<br />
<span className="text-[#f97815]">&gt;</span> Dashboard:{" "}
<span className="text-[#E54D5E]">&gt;</span> Dashboard:{" "}
<span className="text-blue-400">http://localhost:20128/dashboard</span>
<br />
<span className="text-green-400">&gt;</span> Ready to route!

View File

@@ -4,19 +4,19 @@ export default function HeroSection() {
return (
<section className="relative pt-32 pb-20 px-6 min-h-[90vh] flex flex-col items-center justify-center overflow-hidden">
{/* Glow effect */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[500px] bg-[#f97815]/10 rounded-full blur-[120px] pointer-events-none"></div>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[500px] bg-[#E54D5E]/10 rounded-full blur-[120px] pointer-events-none"></div>
<div className="relative z-10 max-w-4xl w-full text-center flex flex-col items-center gap-8">
{/* Version badge */}
<div className="inline-flex items-center gap-2 rounded-full border border-[#3a2f27] bg-[#23180f]/50 px-3 py-1 text-xs font-medium text-[#f97815]">
<span className="flex h-2 w-2 rounded-full bg-[#f97815] animate-pulse"></span>
<div className="inline-flex items-center gap-2 rounded-full border border-[#2D333B] bg-[#111520]/50 px-3 py-1 text-xs font-medium text-[#E54D5E]">
<span className="flex h-2 w-2 rounded-full bg-[#E54D5E] animate-pulse"></span>
v1.0 is now live
</div>
{/* Main heading */}
<h1 className="text-5xl md:text-7xl font-black leading-[1.1] tracking-tight">
One Endpoint for <br />
<span className="text-[#f97815]">All AI Providers</span>
<span className="text-[#E54D5E]">All AI Providers</span>
</h1>
{/* Description */}
@@ -27,7 +27,7 @@ export default function HeroSection() {
{/* CTA Buttons */}
<div className="flex flex-wrap items-center justify-center gap-4 w-full">
<button className="h-12 px-8 rounded-lg bg-[#f97815] hover:bg-[#e0650a] text-[#181411] text-base font-bold transition-all shadow-[0_0_15px_rgba(249,120,21,0.4)] flex items-center gap-2">
<button className="h-12 px-8 rounded-lg bg-[#E54D5E] hover:bg-[#C93D4E] text-white text-base font-bold transition-all shadow-[0_0_15px_rgba(229,77,94,0.4)] flex items-center gap-2">
<span className="material-symbols-outlined">rocket_launch</span>
Get Started
</button>
@@ -35,7 +35,7 @@ export default function HeroSection() {
href="https://github.com/decolua/omniroute"
target="_blank"
rel="noopener noreferrer"
className="h-12 px-8 rounded-lg border border-[#3a2f27] bg-[#23180f] hover:bg-[#3a2f27] text-white text-base font-bold transition-all flex items-center gap-2"
className="h-12 px-8 rounded-lg border border-[#2D333B] bg-[#111520] hover:bg-[#2D333B] text-white text-base font-bold transition-all flex items-center gap-2"
>
<span className="material-symbols-outlined">code</span>
View on GitHub

View File

@@ -2,7 +2,7 @@
export default function HowItWorks() {
return (
<section className="py-24 border-y border-[#3a2f27] bg-[#23180f]/30" id="how-it-works">
<section className="py-24 border-y border-[#2D333B] bg-[#111520]/30" id="how-it-works">
<div className="max-w-7xl mx-auto px-6">
<div className="mb-16">
<h2 className="text-3xl md:text-4xl font-bold mb-4">How OmniRoute Works</h2>
@@ -14,11 +14,11 @@ export default function HowItWorks() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative">
{/* Connection line */}
<div className="hidden md:block absolute top-12 left-[16%] right-[16%] h-[2px] bg-linear-to-r from-gray-700 via-[#f97815] to-gray-700 -z-10"></div>
<div className="hidden md:block absolute top-12 left-[16%] right-[16%] h-[2px] bg-linear-to-r from-gray-700 via-[#E54D5E] to-gray-700 -z-10"></div>
{/* Step 1: CLI & SDKs */}
<div className="flex flex-col gap-6 relative group">
<div className="w-24 h-24 rounded-2xl bg-[#181411] border border-[#3a2f27] flex items-center justify-center shadow-xl group-hover:border-gray-500 transition-colors z-10 mx-auto md:mx-0">
<div className="w-24 h-24 rounded-2xl bg-[#0B0E14] border border-[#2D333B] flex items-center justify-center shadow-xl group-hover:border-gray-500 transition-colors z-10 mx-auto md:mx-0">
<span className="material-symbols-outlined text-4xl text-gray-300">terminal</span>
</div>
<div>
@@ -32,13 +32,13 @@ export default function HowItWorks() {
{/* Step 2: OmniRoute Hub */}
<div className="flex flex-col gap-6 relative group md:items-center md:text-center">
<div className="w-24 h-24 rounded-2xl bg-[#181411] border-2 border-[#f97815] flex items-center justify-center shadow-[0_0_30px_rgba(249,120,21,0.2)] z-10 mx-auto">
<span className="material-symbols-outlined text-4xl text-[#f97815] animate-pulse">
<div className="w-24 h-24 rounded-2xl bg-[#0B0E14] border-2 border-[#E54D5E] flex items-center justify-center shadow-[0_0_30px_rgba(229,77,94,0.2)] z-10 mx-auto">
<span className="material-symbols-outlined text-4xl text-[#E54D5E] animate-pulse">
hub
</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-[#f97815]">2. OmniRoute Hub</h3>
<h3 className="text-xl font-bold mb-2 text-[#E54D5E]">2. OmniRoute Hub</h3>
<p className="text-sm text-gray-400">
Our engine analyzes the prompt, checks provider health, and routes for lowest
latency or cost.
@@ -48,7 +48,7 @@ export default function HowItWorks() {
{/* Step 3: AI Providers */}
<div className="flex flex-col gap-6 relative group md:items-end md:text-right">
<div className="w-24 h-24 rounded-2xl bg-[#181411] border border-[#3a2f27] flex items-center justify-center shadow-xl group-hover:border-gray-500 transition-colors z-10 mx-auto md:mx-0">
<div className="w-24 h-24 rounded-2xl bg-[#0B0E14] border border-[#2D333B] flex items-center justify-center shadow-xl group-hover:border-gray-500 transition-colors z-10 mx-auto md:mx-0">
<div className="grid grid-cols-2 gap-2">
<div className="w-6 h-6 rounded bg-white/10"></div>
<div className="w-6 h-6 rounded bg-white/10"></div>

View File

@@ -7,7 +7,7 @@ export default function Navigation() {
const router = useRouter();
return (
<nav className="fixed top-0 z-50 w-full bg-[#181411]/80 backdrop-blur-md border-b border-[#3a2f27]">
<nav className="fixed top-0 z-50 w-full bg-[#0B0E14]/80 backdrop-blur-md border-b border-[#2D333B]">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
{/* Logo */}
<button
@@ -16,7 +16,7 @@ export default function Navigation() {
onClick={() => router.push("/")}
aria-label="Navigate to home"
>
<div className="size-8 rounded bg-linear-to-br from-[#f97815] to-orange-700 flex items-center justify-center text-white">
<div className="size-8 rounded bg-linear-to-br from-[#E54D5E] to-[#C93D4E] flex items-center justify-center text-white">
<span className="material-symbols-outlined text-[20px]">hub</span>
</div>
<h2 className="text-white text-xl font-bold tracking-tight">OmniRoute</h2>
@@ -56,7 +56,7 @@ export default function Navigation() {
<div className="flex items-center gap-4">
<button
onClick={() => router.push("/dashboard")}
className="hidden sm:flex h-9 items-center justify-center rounded-lg px-4 bg-[#f97815] hover:bg-[#e0650a] transition-all text-[#181411] text-sm font-bold shadow-[0_0_15px_rgba(249,120,21,0.4)] hover:shadow-[0_0_20px_rgba(249,120,21,0.6)]"
className="hidden sm:flex h-9 items-center justify-center rounded-lg px-4 bg-[#E54D5E] hover:bg-[#C93D4E] transition-all text-white text-sm font-bold shadow-[0_0_15px_rgba(229,77,94,0.4)] hover:shadow-[0_0_20px_rgba(229,77,94,0.6)]"
>
Get Started
</button>
@@ -71,7 +71,7 @@ export default function Navigation() {
{/* Mobile menu dropdown */}
{mobileMenuOpen && (
<div className="md:hidden border-t border-[#3a2f27] bg-[#181411]/95 backdrop-blur-md">
<div className="md:hidden border-t border-[#2D333B] bg-[#0B0E14]/95 backdrop-blur-md">
<div className="flex flex-col gap-4 p-6">
<a
className="text-gray-300 hover:text-white text-sm font-medium transition-colors"
@@ -103,7 +103,7 @@ export default function Navigation() {
</a>
<button
onClick={() => router.push("/dashboard")}
className="h-9 rounded-lg bg-[#f97815] hover:bg-[#e0650a] text-[#181411] text-sm font-bold"
className="h-9 rounded-lg bg-[#E54D5E] hover:bg-[#C93D4E] text-white text-sm font-bold"
>
Get Started
</button>

View File

@@ -11,20 +11,20 @@ import Footer from "./components/Footer";
export default function LandingPage() {
const router = useRouter();
return (
<div className="relative text-white font-sans overflow-x-hidden antialiased selection:bg-[#f97815] selection:text-white">
<div className="relative text-white font-sans overflow-x-hidden antialiased selection:bg-[#E54D5E] selection:text-white">
{/* Animated Background */}
<div className="fixed inset-0 z-0 overflow-hidden pointer-events-none bg-[#181411]">
<div className="fixed inset-0 z-0 overflow-hidden pointer-events-none bg-[#0B0E14]">
{/* Grid pattern */}
<div
className="absolute inset-0 opacity-[0.06]"
style={{
backgroundImage: `linear-gradient(to right, #f97815 1px, transparent 1px), linear-gradient(to bottom, #f97815 1px, transparent 1px)`,
backgroundImage: `linear-gradient(to right, #E54D5E 1px, transparent 1px), linear-gradient(to bottom, #E54D5E 1px, transparent 1px)`,
backgroundSize: "50px 50px",
}}
></div>
{/* Animated gradient orbs */}
<div className="absolute top-0 left-1/4 w-[700px] h-[700px] bg-[#f97815]/12 rounded-full blur-[130px] animate-blob"></div>
<div className="absolute top-0 left-1/4 w-[700px] h-[700px] bg-[#E54D5E]/12 rounded-full blur-[130px] animate-blob"></div>
<div
className="absolute top-1/3 right-1/4 w-[600px] h-[600px] bg-purple-500/10 rounded-full blur-[130px] animate-blob"
style={{ animationDelay: "2s", animationDuration: "22s" }}
@@ -39,7 +39,7 @@ export default function LandingPage() {
className="absolute inset-0"
style={{
background:
"radial-gradient(circle at center, transparent 0%, rgba(24, 20, 17, 0.4) 100%)",
"radial-gradient(circle at center, transparent 0%, rgba(11, 14, 20, 0.4) 100%)",
}}
></div>
</div>
@@ -62,7 +62,7 @@ export default function LandingPage() {
{/* CTA Section */}
<section className="py-32 px-6 relative overflow-hidden">
<div className="absolute inset-0 bg-linear-to-t from-[#f97815]/5 to-transparent pointer-events-none"></div>
<div className="absolute inset-0 bg-linear-to-t from-[#E54D5E]/5 to-transparent pointer-events-none"></div>
<div className="max-w-4xl mx-auto text-center relative z-10">
<h2 className="text-4xl md:text-5xl font-black mb-6">
Ready to Simplify Your AI Infrastructure?
@@ -74,13 +74,13 @@ export default function LandingPage() {
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<button
onClick={() => router.push("/dashboard")}
className="w-full sm:w-auto h-14 px-10 rounded-lg bg-[#f97815] hover:bg-[#e0650a] text-[#181411] text-lg font-bold transition-all shadow-[0_0_20px_rgba(249,120,21,0.5)]"
className="w-full sm:w-auto h-14 px-10 rounded-lg bg-[#E54D5E] hover:bg-[#C93D4E] text-white text-lg font-bold transition-all shadow-[0_0_20px_rgba(229,77,94,0.5)]"
>
Start Free
</button>
<button
onClick={() => router.push("/docs")}
className="w-full sm:w-auto h-14 px-10 rounded-lg border border-[#3a2f27] hover:bg-[#23180f] text-white text-lg font-bold transition-all"
className="w-full sm:w-auto h-14 px-10 rounded-lg border border-[#2D333B] hover:bg-[#111520] text-white text-lg font-bold transition-all"
>
Read Documentation
</button>

View File

@@ -188,7 +188,7 @@ export default function Sidebar({ onClose, collapsed = false, onToggleCollapse }
href="/dashboard"
className={cn("flex items-center", collapsed ? "justify-center" : "gap-3")}
>
<div className="flex items-center justify-center size-9 rounded bg-linear-to-br from-[#f97815] to-[#c2590a] shrink-0">
<div className="flex items-center justify-center size-9 rounded bg-linear-to-br from-[#E54D5E] to-[#C93D4E] shrink-0">
<span className="material-symbols-outlined text-white text-[20px]">hub</span>
</div>
{!collapsed && (

View File

@@ -249,7 +249,10 @@ export function DailyTrendChart({ dailyTrend }) {
Token &amp; Cost Trend
</h3>
<ResponsiveContainer width="100%" height={140}>
<ComposedChart data={chartData} margin={{ top: 0, right: hasCost ? 40 : 0, left: 0, bottom: 0 }}>
<ComposedChart
data={chartData}
margin={{ top: 0, right: hasCost ? 40 : 0, left: 0, bottom: 0 }}
>
<XAxis
dataKey="date"
tick={{ fontSize: 9, fill: "var(--text-muted)" }}
@@ -268,10 +271,7 @@ export function DailyTrendChart({ dailyTrend }) {
width={36}
/>
)}
<Tooltip
content={<CostTooltip />}
cursor={{ fill: "rgba(255,255,255,0.04)" }}
/>
<Tooltip content={<CostTooltip />} cursor={{ fill: "rgba(255,255,255,0.04)" }} />
<Bar
dataKey="Input"
stackId="a"
@@ -782,7 +782,7 @@ export function WeeklySquares7d({ activityMap }) {
function getSquareStyle(intensity) {
if (intensity === 0) return { background: "rgba(255,255,255,0.04)" };
const opacity = 0.15 + intensity * 0.75;
return { background: `rgba(217, 119, 87, ${opacity.toFixed(2)})` };
return { background: `rgba(229, 77, 94, ${opacity.toFixed(2)})` };
}
return (
@@ -989,8 +989,16 @@ export function UsageDetail({ summary }) {
// ── ProviderCostDonut ──────────────────────────────────────────────────────
const PROVIDER_COLORS = [
"#f59e0b", "#ef4444", "#8b5cf6", "#10b981", "#06b6d4",
"#ec4899", "#f97316", "#6366f1", "#14b8a6", "#a855f7",
"#f59e0b",
"#ef4444",
"#8b5cf6",
"#10b981",
"#06b6d4",
"#ec4899",
"#f97316",
"#6366f1",
"#14b8a6",
"#a855f7",
];
export function ProviderCostDonut({ byProvider }) {
@@ -1066,4 +1074,3 @@ export function ProviderCostDonut({ byProvider }) {
</Card>
);
}