mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]">></span> Starting OmniRoute...
|
||||
<span className="text-[#E54D5E]">></span> Starting OmniRoute...
|
||||
<br />
|
||||
<span className="text-[#f97815]">></span> Server running on{" "}
|
||||
<span className="text-[#E54D5E]">></span> Server running on{" "}
|
||||
<span className="text-blue-400">http://localhost:20128</span>
|
||||
<br />
|
||||
<span className="text-[#f97815]">></span> Dashboard:{" "}
|
||||
<span className="text-[#E54D5E]">></span> Dashboard:{" "}
|
||||
<span className="text-blue-400">http://localhost:20128/dashboard</span>
|
||||
<br />
|
||||
<span className="text-green-400">></span> Ready to route! ✓
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -249,7 +249,10 @@ export function DailyTrendChart({ dailyTrend }) {
|
||||
Token & 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user