diff --git a/src/app/App.tsx b/src/app/App.tsx
index e785ca7..d5593d9 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -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";
@@ -26,6 +28,15 @@ import { createRealtimeConnection } from "./lib/realtimeConnection";
import { allAgentSets, defaultAgentSetKey } from "@/app/agentConfigs";
function App() {
+ return (
+
++diff --git a/src/app/components/ThemeProvider.tsx b/src/app/components/ThemeProvider.tsx new file mode 100644 index 0000000..e603546 --- /dev/null +++ b/src/app/components/ThemeProvider.tsx @@ -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{JSON.stringify(log.eventData, null, 2)}(undefined); + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useState ('default'); + const [mode, setMode] = useState ('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 ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/app/components/ThemeToggle.tsx b/src/app/components/ThemeToggle.tsx new file mode 100644 index 0000000..8f40f1c --- /dev/null +++ b/src/app/components/ThemeToggle.tsx @@ -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 ( + <> ++ {/* Mode Toggle */} ++ + {/* Theme Selector Dropdown */} + {showThemeSelector && ( +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' ? ( + + ) : ( + + )} + + + {/* Theme Selector Toggle */} +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" + > + + ++ {themes.map((t) => ( ++ )} + > + ); +} diff --git a/src/app/components/Transcript.tsx b/src/app/components/Transcript.tsx index bf64c67..c178c6f 100644 --- a/src/app/components/Transcript.tsx +++ b/src/app/components/Transcript.tsx @@ -48,7 +48,6 @@ function Transcript({ setPrevLogs(transcriptItems); }, [transcriptItems]); - // Autofocus on text box input on load useEffect(() => { if (canSend && inputRef.current) { inputRef.current.focus(); @@ -67,11 +66,11 @@ function Transcript({ }; return ( -{ + 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} + + ))} ++{justCopied ? "Copied!" : "Copy"} @@ -91,15 +90,19 @@ function Transcript({ const isUser = role === "user"; const baseContainer = "flex justify-end flex-col"; const containerClasses = `${baseContainer} ${isUser ? "items-end" : "items-start"}`; - const bubbleBase = `max-w-lg p-3 rounded-xl ${isUser ? "bg-gray-900 text-gray-100" : "bg-gray-100 text-black"}`; + const bubbleBase = `max-w-lg p-3 rounded-lg transition-all duration-200 shadow-md ${ + isUser + ? "bg-[#4a4a4a] dark:bg-accent/20 text-white dark:text-white border border-[#2a2a2a] dark:border-accent/20" + : "bg-[#4a2a2a]/10 bg-accent/20 text-foreground dark:text-foreground border border-[#C4C4C4] dark:border-[#404040]" + }`; const isBracketedMessage = title.startsWith("[") && title.endsWith("]"); - const messageStyle = isBracketedMessage ? "italic text-gray-400" : ""; + const messageStyle = isBracketedMessage ? "italic text-muted dark:text-muted" : ""; const displayTitle = isBracketedMessage ? title.slice(1, -1) : title; return (-+{timestamp}@@ -112,18 +115,18 @@ function Transcript({ return ({timestamp}-data && toggleTranscriptItemExpand(itemId)} > {data && ( @@ -133,8 +136,8 @@ function Transcript({ {title}{expanded && data && ( --); } else { - // Fallback if type is neither MESSAGE nor BREADCRUMB return (++@@ -142,11 +145,10 @@ function Transcript({{JSON.stringify(data, null, 2)}Unknown item type: {type}{" "} {timestamp} @@ -157,7 +159,7 @@ function Transcript({+diff --git a/src/app/globals.css b/src/app/globals.css index 30f9f4b..8f32dd2 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,21 +2,191 @@ @tailwind components; @tailwind utilities; +/* Default Light Theme */ :root { --background: #fafafa; --foreground: #171717; + --primary: #2563eb; + --secondary: #4f46e5; + --accent: #0ea5e9; + --muted: #6b7280; + --border: #6b7280; + --divider: #282828; + --disconnect: #dc2626; + --disconnect-hover: #b91c1c; + --surface: #f1f5f9; } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +/* Default Dark Theme */ +/* Default Dark Theme */ +.dark { + --background: #1a1a1a; + --foreground: #fffff; + --primary: #60a5fa; + --secondary: #818cf8; + --accent: #38bdf8; + --muted: #9ca3af; + --border: #2a2a2a; + --panel-bg: #202020; + --panel-border: #303030; + --panel-border-light: #6a2a2a; + --panel-border-dark: #404040; + --disconnect: #dc2626; + --disconnect-hover: #b91c1c; + --surface: #242424; + --divider: #282828; +} + +/* Material Design Shadows and Transitions */ +.dark .panel { + background: var(--panel-bg); + border: 1px solid var(--panel-border); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +/* Custom shadows for dark mode */ +.dark .shadow-md { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), + 0 4px 8px rgba(0, 0, 0, 0.2); +} + +/* Custom shadows for light mode */ +.shadow-md { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), + 0 4px 8px rgba(0, 0, 0, 0.1); +} + +/* Smooth transitions */ +* { + transition: background-color 0.1s ease-in-out, + color 0.1s ease-in-out, + border-color 0.1s ease-in-out, + box-shadow 0.1s ease-in-out; +} + +/* Disconnect button styles */ +.disconnect-btn { + background: var(--disconnect); + border: 1px solid var(--border); +} + +.disconnect-btn:hover { + background: var(--disconnect-hover); +} +/* Blue Theme - Light */ +[data-theme="blue-light"] { + --background: #f0f9ff; + --foreground: #0c4a6e; + --primary: #0284c7; + --secondary: #0369a1; + --accent: #0ea5e9; + --muted: #64748b; +} + +/* Green Theme - Light */ +[data-theme="green-light"] { + --background: #f0fdf4; + --foreground: #14532d; + --primary: #16a34a; + --secondary: #15803d; + --accent: #22c55e; + --muted: #64748b; +} + +/* Purple Theme - Light */ +[data-theme="purple-light"] { + --background: #faf5ff; + --foreground: #581c87; + --primary: #9333ea; + --secondary: #7e22ce; + --accent: #a855f7; + --muted: #64748b; +} + +/* Dark Theme Variants */ +.dark { + --background: #0a0a0a; + --foreground: #ededed; + --primary: #60a5fa; + --secondary: #818cf8; + --accent: #38bdf8; + --muted: #9ca3af; +} + +/* Blue Theme - Dark */ +.dark[data-theme="blue-dark"] { + --background: #0c4a6e; + --foreground: #e0f2fe; + --primary: #38bdf8; + --secondary: #0ea5e9; + --accent: #7dd3fc; + --muted: #94a3b8; +} + +/* Green Theme - Dark */ +.dark[data-theme="green-dark"] { + --background: #14532d; + --foreground: #dcfce7; + --primary: #4ade80; + --secondary: #22c55e; + --accent: #86efac; + --muted: #94a3b8; +} + +/* Purple Theme - Dark */ +.dark[data-theme="purple-dark"] { + --background: #581c87; + --foreground: #f3e8ff; + --primary: #c084fc; + --secondary: #a855f7; + --accent: #d8b4fe; + --muted: #94a3b8; +} + +@layer base { + body { + @apply text-foreground bg-background transition-colors duration-100; + } + + /* Improved dark mode transitions */ + * { + @apply transition-[background-color,color,border-color,box-shadow] duration-100; } } body { - color: var(--foreground); - background: var(--background); font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; + min-height: 100vh; + margin: 0; +} + +/* Dark mode scrollbar */ +.dark ::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.dark ::-webkit-scrollbar-track { + background: var(--background); +} + +.dark ::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 5px; +} + +.dark ::-webkit-scrollbar-thumb:hover { + background: var(--accent); +} + +/* Responsive Design Adjustments */ +@layer utilities { + .theme-toggle { + @apply fixed bottom-4 right-4 md:bottom-8 md:right-8; + } + + .theme-selector { + @apply fixed bottom-16 right-4 md:bottom-20 md:right-8 bg-background border border-muted rounded-lg p-2 shadow-lg; + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index acb7c79..d4086be 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,19 +1,47 @@ import type { Metadata } from "next"; import "./globals.css"; +import { ThemeProvider } from "./components/ThemeProvider"; +import { ThemeToggle } from "./components/ThemeToggle"; export const metadata: Metadata = { title: "Realtime API Agents", description: "A demo app from OpenAI.", }; +function ThemeScript() { + return ( + + ); +} + export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( - - {children} + + +- + + + + + {children} + + ); } diff --git a/tailwind.config.ts b/tailwind.config.ts index 109807b..32ace5d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -6,11 +6,16 @@ export default { "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], + darkMode: 'class', theme: { extend: { colors: { background: "var(--background)", foreground: "var(--foreground)", + primary: "var(--primary)", + secondary: "var(--secondary)", + accent: "var(--accent)", + muted: "var(--muted)", }, }, },+