Skip to content

Commit 96a0aec

Browse files
nk-agNiveditJaingemini-code-assist[bot]
authored
feat: implement light mode and theme toggle functionality (#406)
* feat: implement light mode and theme toggle functionality - Added light mode CSS variables to globals.css for improved UI flexibility. - Introduced ThemeProvider context to manage theme state across the application. - Integrated ThemeToggle component for users to switch between light and dark modes. - Updated layout and components to support theme changes, ensuring consistent styling. - Enhanced GraphTemplateBuilder and GraphVisualization components to reflect theme changes. These changes enhance user experience by providing a customizable interface that adapts to user preferences. auth: @nk-ag * fix: update execution_summary initialization in get_graph_structure.py - Modified the initialization of execution_summary to use a dictionary comprehension that sets the status values from StateStatusEnum to 0. This change ensures that the execution summary accurately reflects the initial state of all statuses. This update improves the clarity and correctness of the graph structure's execution summary. * Update dashboard/src/components/GraphTemplateDetail.tsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update dashboard/src/components/GraphTemplateDetail.tsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * feat: enhance theme management with automatic theme detection and toggle functionality - Added a script in the layout to automatically apply the user's preferred theme from localStorage or system settings. - Refactored ThemeToggle component to use setTheme for toggling between light and dark modes. - Updated ThemeContext to initialize theme based on the applied class from the document, ensuring consistent theme application. - Prevented rendering of children in ThemeProvider until the theme is mounted to avoid hydration issues. These changes improve user experience by providing a seamless theme management system that adapts to user preferences. * feat: add close button to GraphTemplateDetail component - Introduced a close button in the GraphTemplateDetail component to enhance user interaction and allow users to easily dismiss the modal. - The button utilizes a ghost variant for a subtle appearance, improving the overall UI experience. This addition improves usability by providing a clear method for closing the graph template detail view. * fix: improve theme handling and back navigation logic - Ensured that duplicate theme classes are removed from the document element to prevent conflicts. - Enhanced the back navigation logic in the GraphPage component to handle cases where the window is opened from an external link, providing a fallback to router.back() for server-side rendering. - Updated ThemeProvider to initialize with the dark theme and set the mounted state correctly, improving hydration consistency. These changes enhance user experience by ensuring proper theme application and navigation behavior. * feat: add responsive logo component with theme-based image selection - Introduced a new Logo component that dynamically selects the logo image based on the current theme (light or dark). - Replaced the static image in the Dashboard component with the new Logo component for improved theme consistency. - Added a new dark logo image to the public assets. These changes enhance the visual consistency of the application by ensuring the appropriate logo is displayed according to the user's theme preference. --------- Co-authored-by: NiveditJain <nivedit@aikin.club> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 0f92411 commit 96a0aec

22 files changed

Lines changed: 1620 additions & 895 deletions
18 KB
Loading
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
const API_BASE_URL = process.env.EXOSPHERE_STATE_MANAGER_URI || 'http://localhost:8000';
4+
const API_KEY = process.env.EXOSPHERE_API_KEY;
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const { searchParams } = new URL(request.url);
9+
const namespace = searchParams.get('namespace');
10+
const stateId = searchParams.get('stateId');
11+
12+
if (!namespace || !stateId) {
13+
return NextResponse.json({ error: 'Namespace and stateId are required' }, { status: 400 });
14+
}
15+
16+
if (!API_KEY) {
17+
return NextResponse.json({ error: 'API key not configured' }, { status: 500 });
18+
}
19+
20+
const body = await request.json();
21+
22+
if (!body.fanout_id) {
23+
return NextResponse.json({ error: 'fanout_id is required in request body' }, { status: 400 });
24+
}
25+
26+
const response = await fetch(`${API_BASE_URL}/v0/namespace/${namespace}/state/${stateId}/manual-retry`, {
27+
method: 'POST',
28+
headers: {
29+
'X-API-Key': API_KEY,
30+
'Content-Type': 'application/json',
31+
},
32+
body: JSON.stringify(body),
33+
});
34+
35+
if (!response.ok) {
36+
const errorText = await response.text();
37+
throw new Error(`State manager API error: ${response.status} ${response.statusText} - ${errorText}`);
38+
}
39+
40+
const data = await response.json();
41+
return NextResponse.json(data);
42+
} catch (error) {
43+
console.error('Error retrying state:', error);
44+
return NextResponse.json(
45+
{ error: 'Failed to retry state' },
46+
{ status: 500 }
47+
);
48+
}
49+
}

dashboard/src/app/globals.css

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
--input: #1a2a5a; /* Navy input background */
3939
--ring: #87ceeb; /* Sky blue focus ring */
4040
--chart-1: #87ceeb; /* Sky blue */
41-
--chart-2: #4ade80; /* Green accent */
42-
--chart-3: #fbbf24; /* Yellow accent */
41+
--chart-2: #66d1b5; /* Green accent */
42+
--chart-3: #ffed9e; /* Yellow accent */
4343
--chart-4: #ff6b8a; /* Pink accent */
4444
--chart-5: #a78bfa; /* Purple accent */
4545
--sidebar: #0a1a4a;
@@ -52,6 +52,44 @@
5252
--sidebar-ring: #87ceeb;
5353
}
5454

55+
/* Light Mode Variables */
56+
.light {
57+
--background: #ffffff; /* White background */
58+
--foreground: #031035; /* Dark navy text */
59+
--card: #f2f7fb; /* Light card background */
60+
--card-foreground: #031035;
61+
--popover: #ffffff;
62+
--popover-foreground: #031035;
63+
--primary: #031035; /* Dark navy primary (keeping dark blue accent) */
64+
--primary-foreground: #ffffff;
65+
--secondary: #f1f5f9; /* Light gray secondary */
66+
--secondary-foreground: #031035;
67+
--muted: #f1f5f9; /* Light muted background */
68+
--muted-foreground: #64748b; /* Medium gray text */
69+
--accent: #031035; /* Keep dark blue accent */
70+
--accent-light: #0a1a4a; /* Keep dark blue accent */
71+
--accent-lighter: #1a2a5a; /* Keep dark blue accent */
72+
--accent-lightest: #2a3a6a; /* Keep dark blue accent */
73+
--accent-foreground: #ffffff;
74+
--destructive: #dc2626; /* Red for errors in light mode */
75+
--border: #e2e8f0; /* Light border */
76+
--input: #ffffff; /* White input background */
77+
--ring: #87ceeb; /* Keep sky blue focus ring */
78+
--chart-1: #87ceeb; /* Sky blue */
79+
--chart-2: #4ade80; /* Green accent */
80+
--chart-3: #cca301; /* Yellow accent */
81+
--chart-4: #ff6b8a; /* Pink accent */
82+
--chart-5: #a78bfa; /* Purple accent */
83+
--sidebar: #f8fafc;
84+
--sidebar-foreground: #031035;
85+
--sidebar-primary: #031035; /* Dark navy for sidebar primary in light mode */
86+
--sidebar-primary-foreground: #ffffff;
87+
--sidebar-accent: #f1f5f9;
88+
--sidebar-accent-foreground: #031035;
89+
--sidebar-border: #e2e8f0;
90+
--sidebar-ring: #87ceeb;
91+
}
92+
5593
/* Custom Scrollbar Styling */
5694
::-webkit-scrollbar {
5795
width: 8px;
@@ -113,7 +151,18 @@
113151
}
114152
}
115153

116-
154+
/* Light mode react-flow nodes */
155+
.light .react-flow__node{
156+
background-color: var(--card);
157+
color: var(--card-foreground);
158+
border: 1px solid var(--border);
159+
&:hover{
160+
background-color: var(--muted);
161+
}
162+
&:active{
163+
background-color: var(--muted);
164+
}
165+
}
117166

118167
@theme inline {
119168
--color-background: var(--background);
@@ -169,9 +218,13 @@
169218
@apply bg-background text-foreground font-sans;
170219
}
171220

172-
/* Custom select dropdown styling for better dark theme support */
221+
/* Custom select dropdown styling for better theme support */
173222
select {
174-
color-scheme: dark;
223+
color-scheme: light dark;
224+
}
225+
226+
.light select {
227+
color-scheme: light;
175228
}
176229

177230
select option {
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
'use client';
2+
3+
import React, { useState, useCallback } from 'react';
4+
import { useParams, useRouter } from 'next/navigation';
5+
import { GraphVisualization } from '@/components/GraphVisualization';
6+
import { GraphTemplateDetail } from '@/components/GraphTemplateDetail';
7+
import { ThemeToggle } from '@/components/ThemeToggle';
8+
import { Button } from '@/components/ui/button';
9+
import { ArrowLeft } from 'lucide-react';
10+
import { clientApiService } from '@/services/clientApi';
11+
import { UpsertGraphTemplateResponse } from '@/types/state-manager';
12+
13+
export default function GraphPage() {
14+
const router = useRouter();
15+
const params = useParams();
16+
17+
const namespace = params?.namespace as string;
18+
const runId = params?.runId as string;
19+
20+
// Graph template state
21+
const [graphTemplate, setGraphTemplate] = useState<UpsertGraphTemplateResponse | null>(null);
22+
const [isLoadingTemplate, setIsLoadingTemplate] = useState(false);
23+
const [templateError, setTemplateError] = useState<string | null>(null);
24+
25+
const handleBack = () => {
26+
// Go back to the previous page or close the tab if opened from external link
27+
if (typeof window !== 'undefined') {
28+
if (window.history.length > 1) {
29+
router.back();
30+
} else {
31+
window.close();
32+
}
33+
} else {
34+
// Fallback for SSR
35+
router.back();
36+
}
37+
};
38+
39+
const handleOpenGraphTemplate = useCallback(async (graphName: string) => {
40+
if (!graphName || !namespace) return;
41+
42+
try {
43+
setIsLoadingTemplate(true);
44+
setTemplateError(null);
45+
const template = await clientApiService.getGraphTemplate(namespace, graphName);
46+
// Add name and namespace to the template
47+
template.name = graphName;
48+
template.namespace = namespace;
49+
setGraphTemplate(template);
50+
} catch (err) {
51+
setTemplateError(err instanceof Error ? err.message : 'Failed to load graph template');
52+
} finally {
53+
setIsLoadingTemplate(false);
54+
}
55+
}, [namespace]);
56+
57+
const handleCloseGraphTemplate = () => {
58+
setGraphTemplate(null);
59+
setTemplateError(null);
60+
};
61+
62+
if (!namespace || !runId) {
63+
return (
64+
<div className="min-h-screen bg-background flex items-center justify-center">
65+
<div className="text-center">
66+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
67+
<p className="text-muted-foreground">Loading...</p>
68+
</div>
69+
</div>
70+
);
71+
}
72+
73+
return (
74+
<div className="min-h-screen bg-background">
75+
{/* Header */}
76+
<header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-10">
77+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
78+
<div className="flex items-center justify-between h-16">
79+
<div className="flex items-center space-x-4">
80+
<Button
81+
onClick={handleBack}
82+
variant="ghost"
83+
size="sm"
84+
className="flex items-center space-x-2"
85+
>
86+
<ArrowLeft className="w-4 h-4" />
87+
<span>Back</span>
88+
</Button>
89+
<div className="h-6 w-px bg-border" />
90+
<div>
91+
<h1 className="text-xl font-semibold text-foreground">
92+
Graph Visualization
93+
</h1>
94+
<p className="text-sm text-muted-foreground">
95+
Namespace: {namespace} | Run: {runId}
96+
</p>
97+
</div>
98+
</div>
99+
<ThemeToggle />
100+
</div>
101+
</div>
102+
</header>
103+
104+
{/* Main Content */}
105+
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
106+
<GraphVisualization
107+
namespace={namespace}
108+
runId={runId}
109+
onGraphTemplateRequest={handleOpenGraphTemplate}
110+
/>
111+
</main>
112+
113+
{/* Graph Template Detail Modal - Inline at bottom */}
114+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-8">
115+
{templateError && (
116+
<div className="mb-4 p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
117+
<p className="text-sm text-destructive">{templateError}</p>
118+
</div>
119+
)}
120+
121+
{isLoadingTemplate && (
122+
<div className="mb-4 p-4 bg-muted rounded-lg">
123+
<div className="flex items-center">
124+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary mr-2"></div>
125+
<p className="text-sm text-muted-foreground">Loading graph template...</p>
126+
</div>
127+
</div>
128+
)}
129+
130+
<GraphTemplateDetail
131+
graphTemplate={graphTemplate}
132+
isOpen={!!graphTemplate}
133+
onClose={handleCloseGraphTemplate}
134+
/>
135+
</div>
136+
</div>
137+
);
138+
}

dashboard/src/app/layout.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
3+
import { ThemeProvider } from "@/contexts/ThemeContext";
34
import "./globals.css";
45

56
const geistSans = Geist({
@@ -24,10 +25,38 @@ export default function RootLayout({
2425
}>) {
2526
return (
2627
<html lang="en">
28+
<head>
29+
<script
30+
dangerouslySetInnerHTML={{
31+
__html: `
32+
(function() {
33+
try {
34+
// Ensure we don't add duplicate classes
35+
document.documentElement.classList.remove('light', 'dark');
36+
37+
var theme = localStorage.getItem('theme');
38+
if (theme && (theme === 'light' || theme === 'dark')) {
39+
document.documentElement.classList.add(theme);
40+
} else {
41+
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
42+
document.documentElement.classList.add(prefersDark ? 'dark' : 'light');
43+
}
44+
} catch (e) {
45+
// Fallback to dark theme if there's any error
46+
document.documentElement.classList.remove('light', 'dark');
47+
document.documentElement.classList.add('dark');
48+
}
49+
})();
50+
`,
51+
}}
52+
/>
53+
</head>
2754
<body
2855
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
2956
>
30-
{children}
57+
<ThemeProvider>
58+
{children}
59+
</ThemeProvider>
3160
</body>
3261
</html>
3362
);

dashboard/src/app/page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use client';
22

33
import React, { useState, useEffect } from 'react';
4-
import Image from 'next/image';
54
import { GraphTemplateBuilder } from '@/components/GraphTemplateBuilder';
65
import { NamespaceOverview } from '@/components/NamespaceOverview';
76
import { RunsTable } from '@/components/RunsTable';
87
import { NodeDetailModal } from '@/components/NodeDetailModal';
9-
import { GraphTemplateDetailModal } from '@/components/GraphTemplateDetailModal';
8+
import { GraphTemplateDetailModal} from '@/components/GraphTemplateDetailModal';
9+
import { ThemeToggle } from '@/components/ThemeToggle';
10+
import { Logo } from '@/components/Logo';
1011
import { clientApiService } from '@/services/clientApi';
1112
import {
1213
NodeRegistration,
@@ -139,7 +140,7 @@ export default function Dashboard() {
139140
<div className="flex items-center justify-between h-16">
140141
<div className="flex items-center space-x-4">
141142
<div className="flex items-center space-x-3">
142-
<Image src="/exospheresmall.png" alt="Exosphere Logo" width={32} height={32} />
143+
<Logo width={32} height={32} />
143144
<div>
144145
<h1 className="text-xl font-bold text-foreground">Exosphere Dashboard</h1>
145146
<p className="text-sm text-muted-foreground">AI Workflow State Manager</p>
@@ -168,6 +169,7 @@ export default function Dashboard() {
168169
)}
169170
</Select>
170171
</div>
172+
<ThemeToggle />
171173
</div>
172174
</div>
173175
</div>

0 commit comments

Comments
 (0)