Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dark mode, theme and accent colors #1

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import Image from "next/image";
import Transcript from "./components/Transcript";
import Events from "./components/Events";
import BottomToolbar from "./components/BottomToolbar";
import { ThemeToggle } from "./components/ThemeToggle";
import { ThemeProvider } from "./components/ThemeProvider";

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

function App() {
return (
<ThemeProvider>
<AppContent />
<ThemeToggle />
</ThemeProvider>
);
}

function AppContent() {
const searchParams = useSearchParams();

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

return (
<div className="text-base flex flex-col h-screen bg-gray-100 text-gray-800 relative">
<div className="p-5 text-lg font-semibold flex justify-between items-center">
<div className="text-base flex flex-col h-screen bg-background dark:bg-[#1a1a1a] text-foreground dark:text-white relative">
<div className="p-5 text-lg font-semibold flex justify-between items-center bg-background dark:bg-[#202020] panel">
<div className="flex items-center">
<div onClick={() => window.location.reload()} style={{ cursor: 'pointer' }}>
<Image
src="/openai-logomark.svg"
alt="OpenAI Logo"
width={20}
height={20}
className="mr-2"
className="mr-2 dark:invert"
/>
</div>
<div>
Realtime API <span className="text-gray-500">Agents</span>
Realtime API <span className="text-muted dark:text-muted">Agents</span>
</div>
</div>
<div className="flex items-center">
Expand All @@ -428,7 +439,7 @@ function App() {
<select
value={agentSetKey}
onChange={handleAgentChange}
className="appearance-none border border-gray-300 rounded-lg text-base px-2 py-1 pr-8 cursor-pointer font-normal focus:outline-none"
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"
>
{Object.keys(allAgentSets).map((agentKey) => (
<option key={agentKey} value={agentKey}>
Expand Down Expand Up @@ -456,7 +467,7 @@ function App() {
<select
value={selectedAgentName}
onChange={handleSelectedAgentChange}
className="appearance-none border border-gray-300 rounded-lg text-base px-2 py-1 pr-8 cursor-pointer font-normal focus:outline-none"
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"
>
{selectedAgentConfigSet?.map(agent => (
<option key={agent.name} value={agent.name}>
Expand All @@ -483,7 +494,7 @@ function App() {
</div>
</div>

<div className="flex flex-1 gap-2 px-2 overflow-hidden relative">
<div className="flex flex-1 gap-4 px-4 py-4 overflow-hidden relative">
<Transcript
userText={userText}
setUserText={setUserText}
Expand Down
36 changes: 16 additions & 20 deletions src/app/components/BottomToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,13 @@ function BottomToolbar({
}

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

if (isConnected) {
// Connected -> label "Disconnect" -> red
return `bg-red-600 hover:bg-red-700 ${cursorClass} ${baseClasses}`;
}
// Disconnected or connecting -> label is either "Connect" or "Connecting" -> black
return `bg-black hover:bg-gray-900 ${cursorClass} ${baseClasses}`;
return `${cursorClass} ${baseClasses}`;
}

return (
<div className="p-4 flex flex-row items-center justify-center gap-x-8">
<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">
<button
onClick={onToggleConnection}
className={getConnectionButtonClasses()}
Expand All @@ -66,9 +60,9 @@ function BottomToolbar({
checked={isPTTActive}
onChange={e => setIsPTTActive(e.target.checked)}
disabled={!isConnected}
className="w-4 h-4"
className="w-4 h-4 accent-primary dark:accent-primary cursor-pointer"
/>
<label htmlFor="push-to-talk" className="flex items-center cursor-pointer">
<label htmlFor="push-to-talk" className="flex items-center cursor-pointer text-foreground dark:text-foreground/90">
Push to talk
</label>
<button
Expand All @@ -77,11 +71,13 @@ function BottomToolbar({
onTouchStart={handleTalkButtonDown}
onTouchEnd={handleTalkButtonUp}
disabled={!isPTTActive}
className={
(isPTTUserSpeaking ? "bg-gray-300" : "bg-gray-200") +
" py-1 px-4 cursor-pointer rounded-full" +
(!isPTTActive ? " bg-gray-100 text-gray-400" : "")
}
className={`
py-1 px-4 rounded-lg transition-all duration-200
${isPTTUserSpeaking
? 'bg-primary/20 dark:bg-primary/30'
: 'bg-surface dark:bg-surface hover:bg-surface/90 dark:hover:bg-surface/90'}
${!isPTTActive ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
>
Talk
</button>
Expand All @@ -94,9 +90,9 @@ function BottomToolbar({
checked={isAudioPlaybackEnabled}
onChange={e => setIsAudioPlaybackEnabled(e.target.checked)}
disabled={!isConnected}
className="w-4 h-4"
className="w-4 h-4 accent-primary dark:accent-primary cursor-pointer"
/>
<label htmlFor="audio-playback" className="flex items-center cursor-pointer">
<label htmlFor="audio-playback" className="flex items-center cursor-pointer text-foreground dark:text-foreground/90">
Audio playback
</label>
</div>
Expand All @@ -107,9 +103,9 @@ function BottomToolbar({
type="checkbox"
checked={isEventsPaneExpanded}
onChange={e => setIsEventsPaneExpanded(e.target.checked)}
className="w-4 h-4"
className="w-4 h-4 accent-primary dark:accent-primary cursor-pointer"
/>
<label htmlFor="logs" className="flex items-center cursor-pointer">
<label htmlFor="logs" className="flex items-center cursor-pointer text-foreground dark:text-foreground/90">
Logs
</label>
</div>
Expand Down
37 changes: 20 additions & 17 deletions src/app/components/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ function Events({ isExpanded }: EventsProps) {
const { loggedEvents, toggleExpand } = useEvent();

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

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

return (
<div
className={
(isExpanded ? "w-1/2 overflow-auto" : "w-0 overflow-hidden opacity-0") +
" transition-all rounded-xl duration-200 ease-in-out flex flex-col bg-white"
}
className={`
${isExpanded ? "w-1/2 overflow-auto" : "w-0 overflow-hidden opacity-0"}
transition-all rounded-lg duration-200 ease-in-out flex flex-col
bg-background dark:bg-[#202020] border border-border dark:border-panel-border panel
`}
ref={eventLogsContainerRef}
>
{isExpanded && (
<div>
<div className="font-semibold px-6 py-4 sticky top-0 z-10 text-base border-b bg-white">
<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">
Logs
</div>
<div>
Expand All @@ -54,7 +55,7 @@ function Events({ isExpanded }: EventsProps) {
return (
<div
key={log.id}
className="border-t border-gray-200 py-2 px-6 font-mono"
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"
>
<div
onClick={() => toggleExpand(log.id)}
Expand All @@ -65,25 +66,27 @@ function Events({ isExpanded }: EventsProps) {
style={{ color: arrowInfo.color }}
className="ml-1 mr-2"
>
{arrowInfo.symbol}
{arrowInfo.symbol}
</span>
<span
className={
"flex-1 text-sm " +
(isError ? "text-red-600" : "text-gray-800")
}
className={`
flex-1 text-sm
${isError
? "text-red-600 dark:text-red-400"
: "text-foreground dark:text-foreground/90"}
`}
>
{log.eventName}
</span>
</div>
<div className="text-gray-500 ml-1 text-xs whitespace-nowrap">
<div className="text-muted dark:text-muted ml-1 text-xs whitespace-nowrap">
{log.timestamp}
</div>
</div>

{log.expanded && log.eventData && (
<div className="text-gray-800 text-left">
<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">
<div className="text-foreground dark:text-foreground/90 text-left">
<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">
{JSON.stringify(log.eventData, null, 2)}
</pre>
</div>
Expand Down
76 changes: 76 additions & 0 deletions src/app/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client';

import React, { createContext, useContext, useEffect, useState } from 'react';

type Theme = 'blue' | 'green' | 'purple' | 'default';
type Mode = 'light' | 'dark';

interface ThemeContextType {
theme: Theme;
mode: Mode;
setTheme: (theme: Theme) => void;
setMode: (mode: Mode) => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('default');
const [mode, setMode] = useState<Mode>('light');

useEffect(() => {
// Load saved preferences
const savedTheme = localStorage.getItem('theme') as Theme;
const savedMode = localStorage.getItem('mode') as Mode;

if (savedTheme) setTheme(savedTheme);
if (savedMode) setMode(savedMode);
}, []);

useEffect(() => {
// Update document classes and data attributes
const root = document.documentElement;

// Update dark mode class
if (mode === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}

// Update theme
if (theme !== 'default') {
root.setAttribute('data-theme', `${theme}-${mode}`);
} else {
root.removeAttribute('data-theme');
}

// Apply background color transition
document.body.style.transition = 'background-color 0.3s ease';

// Save preferences
localStorage.setItem('theme', theme);
localStorage.setItem('mode', mode);
}, [theme, mode]);

const value = {
theme,
mode,
setTheme: (newTheme: Theme) => setTheme(newTheme),
setMode: (newMode: Mode) => setMode(newMode),
};

return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}

export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
83 changes: 83 additions & 0 deletions src/app/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use client';

import React, { useState } from 'react';
import { useTheme } from './ThemeProvider';

export function ThemeToggle() {
const { theme, mode, setTheme, setMode } = useTheme();
const [showThemeSelector, setShowThemeSelector] = useState(false);

const themes = [
{ id: 'default', name: 'Default' },
{ id: 'blue', name: 'Blue' },
{ id: 'green', name: 'Green' },
{ id: 'purple', name: 'Purple' },
];

return (
<>
<div className="theme-toggle flex gap-2">
{/* Mode Toggle */}
<button
onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}
className="p-2 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors dark:bg-primary/20 dark:hover:bg-primary/30"
aria-label="Toggle dark mode"
>
{mode === 'light' ? (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
)}
</button>

{/* Theme Selector Toggle */}
<button
onClick={() => setShowThemeSelector(!showThemeSelector)}
className="p-2 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors dark:bg-primary/20 dark:hover:bg-primary/30"
aria-label="Select theme"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="4"></circle>
<line x1="12" y1="2" x2="12" y2="4"></line>
<line x1="12" y1="20" x2="12" y2="22"></line>
<line x1="2" y1="12" x2="4" y2="12"></line>
<line x1="20" y1="12" x2="22" y2="12"></line>
</svg>
</button>
</div>

{/* Theme Selector Dropdown */}
{showThemeSelector && (
<div className="theme-selector dark:border-primary/20 dark:bg-background/95 backdrop-blur-sm">
{themes.map((t) => (
<button
key={t.id}
onClick={() => {
setTheme(t.id as any);
setShowThemeSelector(false);
}}
className={`block w-full text-left px-4 py-2 rounded hover:bg-primary/10 dark:hover:bg-primary/20 transition-colors ${
theme === t.id ? 'text-primary dark:text-primary/90' : ''
}`}
>
{t.name}
</button>
))}
</div>
)}
</>
);
}
Loading