Add light mode support.
This commit is contained in:
parent
ad474e3d06
commit
853ffaf207
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -55,27 +55,27 @@ export default function DatasetPage({ params }: { params: { datasetName: string
|
|||
text = 'Loading Images';
|
||||
subtitle = 'Please wait while we fetch your dataset images...';
|
||||
showIt = true;
|
||||
bgColor = 'bg-gray-50 dark:bg-gray-800/50';
|
||||
textColor = 'text-gray-900 dark:text-gray-100';
|
||||
iconColor = 'text-gray-500 dark:text-gray-400';
|
||||
bgColor = 'bg-gray-800/50';
|
||||
textColor = 'text-gray-100';
|
||||
iconColor = 'text-gray-400';
|
||||
}
|
||||
if (status == 'error') {
|
||||
icon = <LuBan className="w-8 h-8" />;
|
||||
text = 'Error Loading Images';
|
||||
subtitle = 'There was a problem fetching the images. Please try refreshing the page.';
|
||||
showIt = true;
|
||||
bgColor = 'bg-red-50 dark:bg-red-950/20';
|
||||
textColor = 'text-red-900 dark:text-red-100';
|
||||
iconColor = 'text-red-600 dark:text-red-400';
|
||||
bgColor = 'bg-red-600/20';
|
||||
textColor = 'text-red-100';
|
||||
iconColor = 'text-red-400';
|
||||
}
|
||||
if (status == 'success' && imgList.length === 0) {
|
||||
icon = <LuImageOff className="w-8 h-8" />;
|
||||
text = 'No Images Found';
|
||||
subtitle = 'This dataset is empty. Click "Add Images" to get started.';
|
||||
showIt = true;
|
||||
bgColor = 'bg-gray-50 dark:bg-gray-800/50';
|
||||
textColor = 'text-gray-900 dark:text-gray-100';
|
||||
iconColor = 'text-gray-500 dark:text-gray-400';
|
||||
bgColor = 'bg-gray-800/50';
|
||||
textColor = 'text-gray-100';
|
||||
iconColor = 'text-gray-400';
|
||||
}
|
||||
|
||||
if (!showIt) return null;
|
||||
|
|
@ -106,7 +106,7 @@ export default function DatasetPage({ params }: { params: { datasetName: string
|
|||
<div className="flex-1"></div>
|
||||
<div>
|
||||
<Button
|
||||
className="text-gray-200 bg-slate-600 px-3 py-1 rounded-md"
|
||||
className="text-white bg-slate-600 px-3 py-1 rounded-md"
|
||||
onClick={() => openImagesModal(datasetName, () => refreshImageList(datasetName))}
|
||||
>
|
||||
Add Images
|
||||
|
|
|
|||
|
|
@ -114,12 +114,12 @@ export default function Datasets() {
|
|||
<>
|
||||
<TopBar>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-100">Datasets</h1>
|
||||
<h1 className="text-lg">Datasets</h1>
|
||||
</div>
|
||||
<div className="flex-1"></div>
|
||||
<div>
|
||||
<Button
|
||||
className="text-gray-200 bg-slate-600 px-4 py-2 rounded-md hover:bg-slate-500 transition-colors"
|
||||
className="text-white bg-slate-600 px-3 py-1 rounded-md hover:bg-slate-500 transition-colors"
|
||||
onClick={() => openNewDatasetModal()}
|
||||
>
|
||||
New Dataset
|
||||
|
|
|
|||
|
|
@ -2,16 +2,39 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Light mode (default root values, overridden by .dark) */
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--background: #fafafa;
|
||||
--foreground: #171717;
|
||||
|
||||
/* Gray scale: inverted for light mode so bg-gray-950 = light bg, text-gray-100 = dark text */
|
||||
--gray-950: 235 235 235;
|
||||
--gray-900: 215 215 215;
|
||||
--gray-800: 195 195 195;
|
||||
--gray-700: 175 175 175;
|
||||
--gray-600: 163 163 163;
|
||||
--gray-500: 115 115 115;
|
||||
--gray-400: 82 82 82;
|
||||
--gray-300: 64 64 64;
|
||||
--gray-200: 38 38 38;
|
||||
--gray-100: 23 23 23;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
/* Dark mode */
|
||||
.dark {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
|
||||
--gray-950: 10 10 10;
|
||||
--gray-900: 23 23 23;
|
||||
--gray-800: 38 38 38;
|
||||
--gray-700: 64 64 64;
|
||||
--gray-600: 82 82 82;
|
||||
--gray-500: 115 115 115;
|
||||
--gray-400: 163 163 163;
|
||||
--gray-300: 212 212 212;
|
||||
--gray-200: 229 229 229;
|
||||
--gray-100: 245 245 245;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
@ -28,12 +51,12 @@ body {
|
|||
|
||||
/* selected label */
|
||||
.aitk-react-select-container .aitk-react-select__single-value {
|
||||
@apply flex-1 min-w-0 truncate text-sm text-neutral-200;
|
||||
@apply flex-1 min-w-0 truncate text-sm text-gray-200;
|
||||
}
|
||||
|
||||
/* invisible input (keeps focus & typing, never wraps) */
|
||||
.aitk-react-select-container .aitk-react-select__input-container {
|
||||
@apply text-neutral-200;
|
||||
@apply text-gray-200;
|
||||
}
|
||||
|
||||
/* focus */
|
||||
|
|
@ -48,7 +71,7 @@ body {
|
|||
|
||||
/* options */
|
||||
.aitk-react-select-container .aitk-react-select__option {
|
||||
@apply text-sm text-neutral-200 bg-gray-800 hover:bg-gray-700;
|
||||
@apply text-sm text-gray-200 bg-gray-800 hover:bg-gray-700;
|
||||
}
|
||||
|
||||
/* indicator separator */
|
||||
|
|
@ -64,9 +87,6 @@ body {
|
|||
|
||||
/* placeholder */
|
||||
.aitk-react-select-container .aitk-react-select__placeholder {
|
||||
@apply text-sm text-neutral-200;
|
||||
@apply text-sm text-gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export default function JobPage({ params }: { params: { jobID: string } }) {
|
|||
<Button
|
||||
key={page.value}
|
||||
onClick={() => setPageKey(page.value)}
|
||||
className={`px-4 py-1 h-8 flex items-center gap-1.5 ${page.value === pageKey ? 'bg-gray-300 dark:bg-gray-700' : ''}`}
|
||||
className={`px-4 py-1 h-8 flex items-center gap-1.5 ${page.value === pageKey ? 'bg-gray-300 dark:bg-gray-700 text-white' : ''}`}
|
||||
>
|
||||
<page.icon className="text-sm" />
|
||||
{page.name}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import Editor, { OnMount } from '@monaco-editor/react';
|
|||
import type { editor } from 'monaco-editor';
|
||||
import { Settings } from '@/hooks/useSettings';
|
||||
import { migrateJobConfig } from './jobConfig';
|
||||
import { useTheme } from '@/components/ThemeProvider';
|
||||
|
||||
type Props = {
|
||||
jobConfig: JobConfig;
|
||||
|
|
@ -35,6 +36,7 @@ const yamlConfig: YAML.DocumentOptions &
|
|||
};
|
||||
|
||||
export default function AdvancedJob({ jobConfig, setJobConfig, settings }: Props) {
|
||||
const { theme } = useTheme();
|
||||
const [editorValue, setEditorValue] = useState<string>('');
|
||||
const lastJobConfigUpdateStringRef = useRef('');
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||
|
|
@ -132,7 +134,7 @@ export default function AdvancedJob({ jobConfig, setJobConfig, settings }: Props
|
|||
width="100%"
|
||||
defaultLanguage="yaml"
|
||||
value={editorValue}
|
||||
theme="vs-dark"
|
||||
theme={theme === 'dark' ? 'vs-dark' : 'light'}
|
||||
onChange={handleChange}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{
|
||||
|
|
|
|||
|
|
@ -783,7 +783,7 @@ export default function SimpleJob({
|
|||
'config.process[0].datasets',
|
||||
)
|
||||
}
|
||||
className="bg-red-800 hover:bg-red-700 rounded-full p-2 text-sm transition-colors"
|
||||
className="bg-red-600 hover:bg-red-700 text-white rounded-full p-2 text-sm transition-colors"
|
||||
title="Remove Dataset"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
|
|
|
|||
|
|
@ -210,10 +210,7 @@ export default function TrainingForm() {
|
|||
</div>
|
||||
<div className="mx-4 bg-gray-200 dark:bg-gray-800 w-1 h-6"></div>
|
||||
<div>
|
||||
<Button
|
||||
className="text-gray-200 bg-gray-800 px-3 py-1 rounded-md"
|
||||
onClick={handleImportConfig}
|
||||
>
|
||||
<Button className="text-gray-200 bg-gray-800 px-3 py-1 rounded-md" onClick={handleImportConfig}>
|
||||
Import Config
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -263,7 +260,7 @@ export default function TrainingForm() {
|
|||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className="text-gray-200 bg-green-800 px-3 py-1 rounded-md"
|
||||
className="text-white bg-green-600 hover:bg-green-700 px-3 py-1 rounded-md"
|
||||
onClick={() => saveJob()}
|
||||
disabled={status === 'saving'}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default function Dashboard() {
|
|||
</div>
|
||||
<div className="flex-1"></div>
|
||||
<div>
|
||||
<Link href="/jobs/new" className="text-gray-200 bg-slate-600 px-3 py-1 rounded-md">
|
||||
<Link href="/jobs/new" className="text-white bg-slate-600 px-3 py-1 rounded-md">
|
||||
New Training Job
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,9 +25,19 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||
const platform = os.platform();
|
||||
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-title" content="AI-Toolkit" />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
var theme = localStorage.getItem('theme') || 'dark';
|
||||
if (theme === 'dark') document.documentElement.classList.add('dark');
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<script dangerouslySetInnerHTML={{ __html: `window.server_platform = "${platform}";` }} />
|
||||
|
|
|
|||
|
|
@ -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)}%
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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}%` }}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -11,16 +11,16 @@ const config: Config = {
|
|||
extend: {
|
||||
colors: {
|
||||
gray: {
|
||||
950: '#0a0a0a',
|
||||
900: '#171717',
|
||||
800: '#262626',
|
||||
700: '#404040',
|
||||
600: '#525252',
|
||||
500: '#737373',
|
||||
400: '#a3a3a3',
|
||||
300: '#d4d4d4',
|
||||
200: '#e5e5e5',
|
||||
100: '#f5f5f5',
|
||||
950: 'rgb(var(--gray-950) / <alpha-value>)',
|
||||
900: 'rgb(var(--gray-900) / <alpha-value>)',
|
||||
800: 'rgb(var(--gray-800) / <alpha-value>)',
|
||||
700: 'rgb(var(--gray-700) / <alpha-value>)',
|
||||
600: 'rgb(var(--gray-600) / <alpha-value>)',
|
||||
500: 'rgb(var(--gray-500) / <alpha-value>)',
|
||||
400: 'rgb(var(--gray-400) / <alpha-value>)',
|
||||
300: 'rgb(var(--gray-300) / <alpha-value>)',
|
||||
200: 'rgb(var(--gray-200) / <alpha-value>)',
|
||||
100: 'rgb(var(--gray-100) / <alpha-value>)',
|
||||
},
|
||||
|
||||
yellow: {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
VERSION = "0.8.1"
|
||||
VERSION = "0.8.2"
|
||||
|
|
|
|||
Loading…
Reference in New Issue