Add light mode support.

This commit is contained in:
Jaret Burkett
2026-03-31 16:54:55 -06:00
parent ad474e3d06
commit 853ffaf207
23 changed files with 183 additions and 79 deletions

View File

@@ -21,7 +21,7 @@ export default function CPUWidget({ cpu }: CPUWidgetProps) {
if (!cpu) {
return (
<div className="bg-gray-900 rounded-xl shadow-lg overflow-hidden hover:shadow-2xl transition-all duration-300 border border-gray-800">
<div className="bg-gray-900 rounded-xl shadow-lg overflow-hidden border border-gray-800">
<div className="bg-gray-800 px-4 py-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<h2 className="font-semibold text-gray-100">CPU Info</h2>
@@ -35,7 +35,7 @@ export default function CPUWidget({ cpu }: CPUWidgetProps) {
}
return (
<div className="bg-gray-900 rounded-xl shadow-lg overflow-hidden hover:shadow-2xl transition-all duration-300 border border-gray-800">
<div className="bg-gray-900 rounded-xl shadow-lg overflow-hidden border border-gray-800">
<div className="bg-gray-800 px-4 py-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<h2 className="font-semibold text-gray-100">{cpu.name}</h2>
@@ -61,7 +61,7 @@ export default function CPUWidget({ cpu }: CPUWidgetProps) {
</div>
<div>
<div className="flex items-center space-x-2 mb-1 mt-1">
<HardDrive className="w-4 h-4 text-blue-400" />
<HardDrive className="w-4 h-4 text-blue-600 dark:text-blue-400" />
<p className="text-xs text-gray-400">Memory</p>
<span className="text-xs text-gray-300 ml-auto">
{(((cpu.totalMemory - cpu.availableMemory) / cpu.totalMemory) * 100).toFixed(1)}%

View File

@@ -19,10 +19,10 @@ export default function FilesWidget({ jobID }: { jobID: string }) {
};
return (
<div className="col-span-2 bg-gray-900 rounded-xl shadow-lg overflow-hidden hover:shadow-2xl transition-all duration-300 border border-gray-800">
<div className="col-span-2 bg-gray-900 rounded-xl shadow-lg overflow-hidden border border-gray-800">
<div className="bg-gray-800 px-4 py-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<Brain className="w-5 h-5 text-purple-400" />
<Brain className="w-5 h-5 text-purple-600 dark:text-purple-400" />
<h2 className="font-semibold text-gray-100">Checkpoints</h2>
<span className="px-2 py-0.5 bg-gray-700 rounded-full text-xs text-gray-300">{files.length}</span>
</div>
@@ -55,7 +55,7 @@ export default function FilesWidget({ jobID }: { jobID: string }) {
className="group flex items-center justify-between px-2 py-1.5 rounded-lg hover:bg-gray-800 transition-all duration-200"
>
<div className="flex items-center space-x-2 min-w-0">
<Box className="w-4 h-4 text-purple-400 flex-shrink-0" />
<Box className="w-4 h-4 text-purple-600 dark:text-purple-400 flex-shrink-0" />
<div className="flex flex-col min-w-0">
<div className="flex text-sm text-gray-200">
<span className="overflow-hidden text-ellipsis direction-rtl whitespace-nowrap">
@@ -68,7 +68,7 @@ export default function FilesWidget({ jobID }: { jobID: string }) {
<div className="flex items-center space-x-3 flex-shrink-0">
<span className="text-xs text-gray-400">{cleanSize(file.size)}</span>
<div className="bg-purple-500 bg-opacity-0 group-hover:bg-opacity-10 rounded-full p-1 transition-all">
<Download className="w-3 h-3 text-purple-400" />
<Download className="w-3 h-3 text-purple-600 dark:text-purple-400" />
</div>
</div>
</a>

View File

@@ -20,7 +20,7 @@ export default function GPUWidget({ gpu }: GPUWidgetProps) {
};
return (
<div className="bg-gray-900 rounded-xl shadow-lg overflow-hidden hover:shadow-2xl transition-all duration-300 border border-gray-800">
<div className="bg-gray-900 rounded-xl shadow-lg overflow-hidden border border-gray-800">
<div className="bg-gray-800 px-4 py-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<h2 className="font-semibold text-gray-100">{gpu.name}</h2>
@@ -40,10 +40,10 @@ export default function GPUWidget({ gpu }: GPUWidgetProps) {
</div>
</div>
<div className="flex items-center space-x-2">
<Fan className="w-4 h-4 text-blue-400" />
<Fan className="w-4 h-4 text-blue-600 dark:text-blue-400" />
<div>
<p className="text-xs text-gray-400">Fan Speed</p>
<p className="text-sm font-medium text-blue-400">{gpu.fan.speed}%</p>
<p className="text-sm font-medium text-blue-600 dark:text-blue-400">{gpu.fan.speed}%</p>
</div>
</div>
</div>
@@ -60,7 +60,7 @@ export default function GPUWidget({ gpu }: GPUWidgetProps) {
/>
</div>
<div className="flex items-center space-x-2 mb-1 mt-3">
<HardDrive className="w-4 h-4 text-blue-400" />
<HardDrive className="w-4 h-4 text-blue-600 dark:text-blue-400" />
<p className="text-xs text-gray-400">Memory</p>
<span className="text-xs text-gray-300 ml-auto">
{((gpu.memory.used / gpu.memory.total) * 100).toFixed(1)}%
@@ -81,14 +81,14 @@ export default function GPUWidget({ gpu }: GPUWidgetProps) {
{/* Power and Clocks Section */}
<div className="grid grid-cols-2 gap-4 pt-2 border-t border-gray-800">
<div className="flex items-start space-x-2">
<Clock className="w-4 h-4 text-purple-400" />
<Clock className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<div>
<p className="text-xs text-gray-400">Clock Speed</p>
<p className="text-sm text-gray-200">{gpu.clocks.graphics} MHz</p>
</div>
</div>
<div className="flex items-start space-x-2">
<Zap className="w-4 h-4 text-amber-400" />
<Zap className="w-4 h-4 text-amber-600 dark:text-amber-400" />
<div>
<p className="text-xs text-gray-400">Power Draw</p>
<p className="text-sm text-gray-200">

View File

@@ -93,7 +93,7 @@ export default function JobOverview({ job }: JobOverviewProps) {
<div className="col-span-2 bg-gray-900 rounded-xl shadow-lg overflow-hidden border border-gray-800 flex flex-col">
<div className="bg-gray-800 px-4 py-3 flex items-center justify-between">
<h2 className="text-gray-100">
<Info className="w-5 h-5 mr-2 -mt-1 text-amber-400 inline-block" /> {job.info}
<Info className="w-5 h-5 mr-2 -mt-1 text-amber-600 dark:text-amber-400 inline-block" /> {job.info}
</h2>
<span className={`px-3 py-1 rounded-full text-sm ${getStatusColor(job.status)}`}>{job.status}</span>
</div>
@@ -115,7 +115,7 @@ export default function JobOverview({ job }: JobOverviewProps) {
{/* Job Info Grid */}
<div className="grid gap-4 grid-cols-1 md:grid-cols-3">
<div className="flex items-center space-x-4">
<HardDrive className="w-5 h-5 text-blue-400" />
<HardDrive className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<div>
<p className="text-xs text-gray-400">Job Name</p>
<p className="text-sm font-medium text-gray-200">{job.name}</p>
@@ -123,7 +123,7 @@ export default function JobOverview({ job }: JobOverviewProps) {
</div>
<div className="flex items-center space-x-4">
<Cpu className="w-5 h-5 text-purple-400" />
<Cpu className="w-5 h-5 text-purple-600 dark:text-purple-400" />
<div>
<p className="text-xs text-gray-400">Assigned GPUs</p>
<p className="text-sm font-medium text-gray-200">GPUs: {job.gpu_ids}</p>
@@ -131,7 +131,7 @@ export default function JobOverview({ job }: JobOverviewProps) {
</div>
<div className="flex items-center space-x-4">
<Gauge className="w-5 h-5 text-green-400" />
<Gauge className="w-5 h-5 text-green-600 dark:text-green-400" />
<div>
<p className="text-xs text-gray-400">Speed</p>
<p className="text-sm font-medium text-gray-200">{job.speed_string == '' ? '?' : job.speed_string}</p>

View File

@@ -144,37 +144,37 @@ export default function JobsTable({ onlyActive = false }: JobsTableProps) {
<div
className={classNames(
'text-md flex px-4 py-1 rounded-t-lg',
{ 'bg-green-900': queue?.is_running },
{ 'bg-red-900': !queue?.is_running },
{ 'bg-green-600 dark:bg-green-900': queue?.is_running },
{ 'bg-red-600 dark:bg-red-900': !queue?.is_running },
)}
>
<div className="flex items-center space-x-2 flex-1 py-2">
<h2 className="font-semibold text-gray-100">{jobsDict[gpuKey].name}</h2>
<h2 className="font-semibold text-white">{jobsDict[gpuKey].name}</h2>
<span className="px-2 py-0.5 bg-gray-700 rounded-full text-xs text-gray-300"># {queue?.gpu_ids}</span>
</div>
<div className="text-sm text-gray-300 italic flex items-center">
{queue?.is_running ? (
<>
<span className="text-green-400 mr-2">Queue Running</span>
<span className="text-green-100 dark:text-green-400 mr-2">Queue Running</span>
<button
onClick={async () => {
await stopQueue(queue.gpu_ids as string);
refresh();
}}
className="ml-4 text-xs bg-red-900 hover:bg-red-800 px-2 py-1 rounded"
className="ml-4 text-xs text-white bg-red-600 hover:bg-red-700 px-2 py-1 rounded"
>
STOP
</button>
</>
) : (
<>
<span className="text-red-400 mr-2">Queue Stopped</span>
<span className="text-red-100 dark:text-red-400 mr-2">Queue Stopped</span>
<button
onClick={async () => {
await startQueue(gpuKey);
refresh();
}}
className="ml-4 text-xs bg-green-700 hover:bg-green-600 px-2 py-1 rounded"
className="ml-4 text-xs text-white bg-green-600 hover:bg-green-700 px-2 py-1 rounded"
>
START
</button>
@@ -187,7 +187,11 @@ export default function JobsTable({ onlyActive = false }: JobsTableProps) {
rows={jobsDict[gpuKey].jobs}
isLoading={isLoading}
onRefresh={refresh}
theadClassName={queue?.is_running ? 'bg-green-950' : 'bg-red-950'}
theadClassName={
queue?.is_running
? 'bg-green-700 dark:bg-green-950 text-white dark:text-gray-400'
: 'bg-red-700 dark:bg-red-950 text-white dark:text-gray-400'
}
/>
</div>
);

View File

@@ -1,6 +1,8 @@
import Link from 'next/link';
import { Home, Settings, BrainCircuit, Images, Plus } from 'lucide-react';
import { FaXTwitter, FaDiscord, FaYoutube } from 'react-icons/fa6';
import ThemeToggle from './ThemeToggle';
import ThemeLogo from './ThemeLogo';
const Sidebar = () => {
const navigation = [
@@ -19,7 +21,7 @@ const Sidebar = () => {
<div className="flex flex-col w-59 bg-gray-900 text-gray-100">
<div className="px-4 py-3">
<h1 className="text-l">
<img src="/ostris_logo.png" alt="Ostris AI Toolkit" className="w-auto h-7 mr-3 inline" />
<ThemeLogo />
<span className="font-bold uppercase">Ostris</span>
<span className="ml-2 uppercase text-gray-300">AI-Toolkit</span>
</h1>
@@ -60,7 +62,7 @@ const Sidebar = () => {
{/* Social links grid */}
<div className="px-1 py-1 border-t border-gray-800">
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-4 gap-4">
<a href="https://discord.gg/VXmU2f5WEU" target="_blank" rel="noreferrer" className={socialsBoxClass}>
<FaDiscord className={socialIconClass} />
{/* <span className="text-xs text-gray-500 mt-1">Discord</span> */}
@@ -73,6 +75,7 @@ const Sidebar = () => {
<FaXTwitter className={socialIconClass} />
{/* <span className="text-xs text-gray-500 mt-1">X</span> */}
</a>
<ThemeToggle />
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
'use client';
import { useTheme } from './ThemeProvider';
const ThemeLogo = () => {
const { theme } = useTheme();
const src = theme === 'dark' ? '/ostris_logo.png' : '/ostris_logo_black.png';
return <img src={src} alt="Ostris AI Toolkit" className="w-auto h-7 mr-3 inline" />;
};
export default ThemeLogo;

View File

@@ -2,10 +2,40 @@
import { createContext, useContext, useEffect, useState } from 'react';
const ThemeContext = createContext({ isDark: true });
type Theme = 'light' | 'dark';
interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue>({
theme: 'dark',
toggleTheme: () => {},
});
export const useTheme = () => useContext(ThemeContext);
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [isDark, setIsDark] = useState(true);
const [theme, setTheme] = useState<Theme>('dark');
const [mounted, setMounted] = useState(false);
return <ThemeContext.Provider value={{ isDark }}>{children}</ThemeContext.Provider>;
useEffect(() => {
const stored = localStorage.getItem('theme') as Theme | null;
const initial = stored || 'dark';
setTheme(initial);
document.documentElement.classList.toggle('dark', initial === 'dark');
setMounted(true);
}, []);
const toggleTheme = () => {
const next = theme === 'dark' ? 'light' : 'dark';
setTheme(next);
localStorage.setItem('theme', next);
document.documentElement.classList.toggle('dark', next === 'dark');
};
if (!mounted) return null;
return <ThemeContext.Provider value={{ theme, toggleTheme }}>{children}</ThemeContext.Provider>;
};

View File

@@ -0,0 +1,26 @@
'use client';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from './ThemeProvider';
const ThemeToggle = () => {
const { theme, toggleTheme } = useTheme();
// Button styled as the opposite theme so it stands out
const buttonClass =
theme === 'dark'
? 'bg-[rgb(215,215,215)] hover:bg-[rgb(195,195,195)] text-[rgb(82,82,82)]'
: 'bg-[rgb(23,23,23)] hover:bg-[rgb(38,38,38)] text-[rgb(163,163,163)]';
return (
<button
onClick={toggleTheme}
className={`flex items-center justify-center p-1 rounded-lg transition-colors ${buttonClass}`}
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</button>
);
};
export default ThemeToggle;

View File

@@ -232,7 +232,7 @@ export const Checkbox = (props: CheckboxProps) => {
onClick={() => !disabled && onChange(!checked)}
className={classNames(
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
checked ? 'bg-blue-600' : 'bg-gray-700',
checked ? 'bg-blue-500' : 'bg-gray-600',
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-opacity-80',
)}
>
@@ -408,7 +408,7 @@ export const SliderInput: React.FC<SliderInputProps> = props => {
{/* Fill */}
<div
className="pointer-events-none absolute left-0 top-1/2 -translate-y-1/2 h-3 rounded-sm bg-blue-600"
className="pointer-events-none absolute left-0 top-1/2 -translate-y-1/2 h-3 rounded-sm bg-blue-500"
style={{ width: `${percent}%` }}
/>

View File

@@ -9,7 +9,7 @@ export const TopBar: React.FC<Props> = ({ children, className }) => {
return (
<div
className={classNames(
'absolute top-0 left-0 w-full h-12 dark:bg-gray-900 shadow-sm z-10 flex items-center px-2',
'absolute top-0 left-0 w-full h-12 bg-gray-900 shadow-sm z-10 flex items-center px-2',
className,
)}
>