Skip to content

Commit a12388f

Browse files
committed
implement theme provider and toggle for dark mode support
1 parent 6f5f0de commit a12388f

9 files changed

+424
-66
lines changed

src/app/App.tsx

+18-7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import Image from "next/image";
1010
import Transcript from "./components/Transcript";
1111
import Events from "./components/Events";
1212
import BottomToolbar from "./components/BottomToolbar";
13+
import { ThemeToggle } from "./components/ThemeToggle";
14+
import { ThemeProvider } from "./components/ThemeProvider";
1315

1416
// Types
1517
import { AgentConfig, SessionStatus } from "@/app/types";
@@ -26,6 +28,15 @@ import { createRealtimeConnection } from "./lib/realtimeConnection";
2628
import { allAgentSets, defaultAgentSetKey } from "@/app/agentConfigs";
2729

2830
function App() {
31+
return (
32+
<ThemeProvider>
33+
<AppContent />
34+
<ThemeToggle />
35+
</ThemeProvider>
36+
);
37+
}
38+
39+
function AppContent() {
2940
const searchParams = useSearchParams();
3041

3142
const { transcriptItems, addTranscriptMessage, addTranscriptBreadcrumb } =
@@ -404,20 +415,20 @@ function App() {
404415
const agentSetKey = searchParams.get("agentConfig") || "default";
405416

406417
return (
407-
<div className="text-base flex flex-col h-screen bg-gray-100 text-gray-800 relative">
408-
<div className="p-5 text-lg font-semibold flex justify-between items-center">
418+
<div className="text-base flex flex-col h-screen bg-background dark:bg-[#1a1a1a] text-foreground dark:text-white relative">
419+
<div className="p-5 text-lg font-semibold flex justify-between items-center bg-background dark:bg-[#202020] panel">
409420
<div className="flex items-center">
410421
<div onClick={() => window.location.reload()} style={{ cursor: 'pointer' }}>
411422
<Image
412423
src="/openai-logomark.svg"
413424
alt="OpenAI Logo"
414425
width={20}
415426
height={20}
416-
className="mr-2"
427+
className="mr-2 dark:invert"
417428
/>
418429
</div>
419430
<div>
420-
Realtime API <span className="text-gray-500">Agents</span>
431+
Realtime API <span className="text-muted dark:text-muted">Agents</span>
421432
</div>
422433
</div>
423434
<div className="flex items-center">
@@ -428,7 +439,7 @@ function App() {
428439
<select
429440
value={agentSetKey}
430441
onChange={handleAgentChange}
431-
className="appearance-none border border-gray-300 rounded-lg text-base px-2 py-1 pr-8 cursor-pointer font-normal focus:outline-none"
442+
className="appearance-none border border-[#acacac] dark:border-[#404040] rounded-lg text-base px-2 py-1 pr-8 cursor-pointer font-normal focus:outline-none bg-[#2a2a2a]/5 dark:bg-[#2a2a2a]/20"
432443
>
433444
{Object.keys(allAgentSets).map((agentKey) => (
434445
<option key={agentKey} value={agentKey}>
@@ -456,7 +467,7 @@ function App() {
456467
<select
457468
value={selectedAgentName}
458469
onChange={handleSelectedAgentChange}
459-
className="appearance-none border border-gray-300 rounded-lg text-base px-2 py-1 pr-8 cursor-pointer font-normal focus:outline-none"
470+
className="appearance-none border border-[#acacac] dark:border-[#404040] rounded-lg text-base px-2 py-1 pr-8 cursor-pointer font-normal focus:outline-none bg-[#2a2a2a]/5 dark:bg-[#2a2a2a]/20"
460471
>
461472
{selectedAgentConfigSet?.map(agent => (
462473
<option key={agent.name} value={agent.name}>
@@ -483,7 +494,7 @@ function App() {
483494
</div>
484495
</div>
485496

486-
<div className="flex flex-1 gap-2 px-2 overflow-hidden relative">
497+
<div className="flex flex-1 gap-4 px-4 py-4 overflow-hidden relative">
487498
<Transcript
488499
userText={userText}
489500
setUserText={setUserText}

src/app/components/BottomToolbar.tsx

+17-17
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,17 @@ function BottomToolbar({
3838
}
3939

4040
function getConnectionButtonClasses() {
41-
const baseClasses = "text-white text-base p-2 w-36 rounded-full h-full";
41+
const baseClasses = "text-white text-base p-2 w-36 rounded-lg h-full transition-all duration-200 disconnect-btn";
4242
const cursorClass = isConnecting ? "cursor-not-allowed" : "cursor-pointer";
4343

4444
if (isConnected) {
45-
// Connected -> label "Disconnect" -> red
46-
return `bg-red-600 hover:bg-red-700 ${cursorClass} ${baseClasses}`;
45+
return `${cursorClass} ${baseClasses}`;
4746
}
48-
// Disconnected or connecting -> label is either "Connect" or "Connecting" -> black
49-
return `bg-black hover:bg-gray-900 ${cursorClass} ${baseClasses}`;
47+
return `bg-surface dark:bg-surface hover:bg-surface/90 dark:hover:bg-surface/90 ${cursorClass} ${baseClasses}`;
5048
}
5149

5250
return (
53-
<div className="p-4 flex flex-row items-center justify-center gap-x-8">
51+
<div className="p-4 flex flex-row items-center justify-center gap-x-8 bg-background dark:bg-[#202020] panel border-t border-border dark:border-panel-border">
5452
<button
5553
onClick={onToggleConnection}
5654
className={getConnectionButtonClasses()}
@@ -66,9 +64,9 @@ function BottomToolbar({
6664
checked={isPTTActive}
6765
onChange={e => setIsPTTActive(e.target.checked)}
6866
disabled={!isConnected}
69-
className="w-4 h-4"
67+
className="w-4 h-4 accent-primary dark:accent-primary cursor-pointer"
7068
/>
71-
<label htmlFor="push-to-talk" className="flex items-center cursor-pointer">
69+
<label htmlFor="push-to-talk" className="flex items-center cursor-pointer text-foreground dark:text-foreground/90">
7270
Push to talk
7371
</label>
7472
<button
@@ -77,11 +75,13 @@ function BottomToolbar({
7775
onTouchStart={handleTalkButtonDown}
7876
onTouchEnd={handleTalkButtonUp}
7977
disabled={!isPTTActive}
80-
className={
81-
(isPTTUserSpeaking ? "bg-gray-300" : "bg-gray-200") +
82-
" py-1 px-4 cursor-pointer rounded-full" +
83-
(!isPTTActive ? " bg-gray-100 text-gray-400" : "")
84-
}
78+
className={`
79+
py-1 px-4 rounded-lg transition-all duration-200
80+
${isPTTUserSpeaking
81+
? 'bg-primary/20 dark:bg-primary/30'
82+
: 'bg-surface dark:bg-surface hover:bg-surface/90 dark:hover:bg-surface/90'}
83+
${!isPTTActive ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
84+
`}
8585
>
8686
Talk
8787
</button>
@@ -94,9 +94,9 @@ function BottomToolbar({
9494
checked={isAudioPlaybackEnabled}
9595
onChange={e => setIsAudioPlaybackEnabled(e.target.checked)}
9696
disabled={!isConnected}
97-
className="w-4 h-4"
97+
className="w-4 h-4 accent-primary dark:accent-primary cursor-pointer"
9898
/>
99-
<label htmlFor="audio-playback" className="flex items-center cursor-pointer">
99+
<label htmlFor="audio-playback" className="flex items-center cursor-pointer text-foreground dark:text-foreground/90">
100100
Audio playback
101101
</label>
102102
</div>
@@ -107,9 +107,9 @@ function BottomToolbar({
107107
type="checkbox"
108108
checked={isEventsPaneExpanded}
109109
onChange={e => setIsEventsPaneExpanded(e.target.checked)}
110-
className="w-4 h-4"
110+
className="w-4 h-4 accent-primary dark:accent-primary cursor-pointer"
111111
/>
112-
<label htmlFor="logs" className="flex items-center cursor-pointer">
112+
<label htmlFor="logs" className="flex items-center cursor-pointer text-foreground dark:text-foreground/90">
113113
Logs
114114
</label>
115115
</div>

src/app/components/Events.tsx

+20-17
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ function Events({ isExpanded }: EventsProps) {
1515
const { loggedEvents, toggleExpand } = useEvent();
1616

1717
const getDirectionArrow = (direction: string) => {
18-
if (direction === "client") return { symbol: "▲", color: "#7f5af0" };
19-
if (direction === "server") return { symbol: "▼", color: "#2cb67d" };
20-
return { symbol: "•", color: "#555" };
18+
if (direction === "client") return { symbol: "▲", color: "var(--primary)" };
19+
if (direction === "server") return { symbol: "▼", color: "var(--accent)" };
20+
return { symbol: "•", color: "var(--muted)" };
2121
};
2222

2323
useEffect(() => {
@@ -33,15 +33,16 @@ function Events({ isExpanded }: EventsProps) {
3333

3434
return (
3535
<div
36-
className={
37-
(isExpanded ? "w-1/2 overflow-auto" : "w-0 overflow-hidden opacity-0") +
38-
" transition-all rounded-xl duration-200 ease-in-out flex flex-col bg-white"
39-
}
36+
className={`
37+
${isExpanded ? "w-1/2 overflow-auto" : "w-0 overflow-hidden opacity-0"}
38+
transition-all rounded-lg duration-200 ease-in-out flex flex-col
39+
bg-background dark:bg-[#202020] border border-border dark:border-panel-border panel
40+
`}
4041
ref={eventLogsContainerRef}
4142
>
4243
{isExpanded && (
4344
<div>
44-
<div className="font-semibold px-6 py-4 sticky top-0 z-10 text-base border-b bg-white">
45+
<div className="font-semibold px-6 py-4 sticky top-0 z-10 text-base border-divider bg-[#2a2a2a]/5 dark:bg-[#2a2a2a] text-foreground dark:text-foreground/90">
4546
Logs
4647
</div>
4748
<div>
@@ -54,7 +55,7 @@ function Events({ isExpanded }: EventsProps) {
5455
return (
5556
<div
5657
key={log.id}
57-
className="border-t border-gray-200 py-2 px-6 font-mono"
58+
className="border-t-[0.5px] border-t-gray-400 dark:border-t-gray-500 border-panel-border/50 py-2 px-6 font-mono hover:bg-surface/50 dark:hover:bg-surface/20 transition-colors duration-200"
5859
>
5960
<div
6061
onClick={() => toggleExpand(log.id)}
@@ -65,25 +66,27 @@ function Events({ isExpanded }: EventsProps) {
6566
style={{ color: arrowInfo.color }}
6667
className="ml-1 mr-2"
6768
>
68-
{arrowInfo.symbol}
69+
{arrowInfo.symbol}
6970
</span>
7071
<span
71-
className={
72-
"flex-1 text-sm " +
73-
(isError ? "text-red-600" : "text-gray-800")
74-
}
72+
className={`
73+
flex-1 text-sm
74+
${isError
75+
? "text-red-600 dark:text-red-400"
76+
: "text-foreground dark:text-foreground/90"}
77+
`}
7578
>
7679
{log.eventName}
7780
</span>
7881
</div>
79-
<div className="text-gray-500 ml-1 text-xs whitespace-nowrap">
82+
<div className="text-muted dark:text-muted ml-1 text-xs whitespace-nowrap">
8083
{log.timestamp}
8184
</div>
8285
</div>
8386

8487
{log.expanded && log.eventData && (
85-
<div className="text-gray-800 text-left">
86-
<pre className="border-l-2 ml-1 border-gray-200 whitespace-pre-wrap break-words font-mono text-xs mb-2 mt-2 pl-2">
88+
<div className="text-foreground dark:text-foreground/90 text-left">
89+
<pre className="border-l-2 ml-1 border-panel-border whitespace-pre-wrap break-words font-mono text-xs mb-2 mt-2 pl-2">
8790
{JSON.stringify(log.eventData, null, 2)}
8891
</pre>
8992
</div>

src/app/components/ThemeProvider.tsx

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use client';
2+
3+
import React, { createContext, useContext, useEffect, useState } from 'react';
4+
5+
type Theme = 'blue' | 'green' | 'purple' | 'default';
6+
type Mode = 'light' | 'dark';
7+
8+
interface ThemeContextType {
9+
theme: Theme;
10+
mode: Mode;
11+
setTheme: (theme: Theme) => void;
12+
setMode: (mode: Mode) => void;
13+
}
14+
15+
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
16+
17+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
18+
const [theme, setTheme] = useState<Theme>('default');
19+
const [mode, setMode] = useState<Mode>('light');
20+
21+
useEffect(() => {
22+
// Load saved preferences
23+
const savedTheme = localStorage.getItem('theme') as Theme;
24+
const savedMode = localStorage.getItem('mode') as Mode;
25+
26+
if (savedTheme) setTheme(savedTheme);
27+
if (savedMode) setMode(savedMode);
28+
}, []);
29+
30+
useEffect(() => {
31+
// Update document classes and data attributes
32+
const root = document.documentElement;
33+
34+
// Update dark mode class
35+
if (mode === 'dark') {
36+
root.classList.add('dark');
37+
} else {
38+
root.classList.remove('dark');
39+
}
40+
41+
// Update theme
42+
if (theme !== 'default') {
43+
root.setAttribute('data-theme', `${theme}-${mode}`);
44+
} else {
45+
root.removeAttribute('data-theme');
46+
}
47+
48+
// Apply background color transition
49+
document.body.style.transition = 'background-color 0.3s ease';
50+
51+
// Save preferences
52+
localStorage.setItem('theme', theme);
53+
localStorage.setItem('mode', mode);
54+
}, [theme, mode]);
55+
56+
const value = {
57+
theme,
58+
mode,
59+
setTheme: (newTheme: Theme) => setTheme(newTheme),
60+
setMode: (newMode: Mode) => setMode(newMode),
61+
};
62+
63+
return (
64+
<ThemeContext.Provider value={value}>
65+
{children}
66+
</ThemeContext.Provider>
67+
);
68+
}
69+
70+
export function useTheme() {
71+
const context = useContext(ThemeContext);
72+
if (context === undefined) {
73+
throw new Error('useTheme must be used within a ThemeProvider');
74+
}
75+
return context;
76+
}

src/app/components/ThemeToggle.tsx

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
import { useTheme } from './ThemeProvider';
5+
6+
export function ThemeToggle() {
7+
const { theme, mode, setTheme, setMode } = useTheme();
8+
const [showThemeSelector, setShowThemeSelector] = useState(false);
9+
10+
const themes = [
11+
{ id: 'default', name: 'Default' },
12+
{ id: 'blue', name: 'Blue' },
13+
{ id: 'green', name: 'Green' },
14+
{ id: 'purple', name: 'Purple' },
15+
];
16+
17+
return (
18+
<>
19+
<div className="theme-toggle flex gap-2">
20+
{/* Mode Toggle */}
21+
<button
22+
onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}
23+
className="p-2 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors dark:bg-primary/20 dark:hover:bg-primary/30"
24+
aria-label="Toggle dark mode"
25+
>
26+
{mode === 'light' ? '🌙' : '☀️'}
27+
</button>
28+
29+
{/* Theme Selector Toggle */}
30+
<button
31+
onClick={() => setShowThemeSelector(!showThemeSelector)}
32+
className="p-2 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors dark:bg-primary/20 dark:hover:bg-primary/30"
33+
aria-label="Select theme"
34+
>
35+
🎨
36+
</button>
37+
</div>
38+
39+
{/* Theme Selector Dropdown */}
40+
{showThemeSelector && (
41+
<div className="theme-selector dark:border-primary/20 dark:bg-background/95 backdrop-blur-sm">
42+
{themes.map((t) => (
43+
<button
44+
key={t.id}
45+
onClick={() => {
46+
setTheme(t.id as any);
47+
setShowThemeSelector(false);
48+
}}
49+
className={`block w-full text-left px-4 py-2 rounded hover:bg-primary/10 dark:hover:bg-primary/20 transition-colors ${
50+
theme === t.id ? 'text-primary dark:text-primary/90' : ''
51+
}`}
52+
>
53+
{t.name}
54+
</button>
55+
))}
56+
</div>
57+
)}
58+
</>
59+
);
60+
}

0 commit comments

Comments
 (0)