Skip to content

Commit e631cb7

Browse files
authored
feat(agents-chat-starter): dark mode toggle + docs fixes (#4227)
1 parent 5fec5f1 commit e631cb7

7 files changed

Lines changed: 428 additions & 123 deletions

File tree

‎examples/agents-chat-starter/src/ui/components/MembersSidebar.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ export function MembersSidebar({
6363
style={{
6464
background:
6565
agent.status === `running`
66-
? `#3b82f6`
66+
? `var(--blue-9)`
6767
: agent.status === `idle`
68-
? `#22c55e`
68+
? `var(--green-9)`
6969
: agent.status === `spawning`
70-
? `#eab308`
71-
: `#cbd5e1`,
70+
? `var(--yellow-9)`
71+
: `var(--gray-7)`,
7272
}}
7373
/>
7474
<Box style={{ minWidth: 0 }}>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {
2+
createContext,
3+
useCallback,
4+
useContext,
5+
useEffect,
6+
useMemo,
7+
useState,
8+
} from 'react'
9+
10+
const STORAGE_KEY = `electric-agents-chat.dark-mode`
11+
12+
export type ThemePreference = `light` | `dark` | `system`
13+
14+
type DarkModeContextValue = {
15+
darkMode: boolean
16+
preference: ThemePreference
17+
cyclePreference: () => void
18+
}
19+
20+
const DarkModeContext = createContext<DarkModeContextValue | null>(null)
21+
22+
function readInitialPreference(): ThemePreference {
23+
if (typeof window === `undefined`) return `system`
24+
const stored = window.localStorage.getItem(STORAGE_KEY)
25+
if (stored === `true` || stored === `dark`) return `dark`
26+
if (stored === `false` || stored === `light`) return `light`
27+
return `system`
28+
}
29+
30+
function systemPrefersDark(): boolean {
31+
if (typeof window === `undefined`) return false
32+
return window.matchMedia?.(`(prefers-color-scheme: dark)`).matches ?? false
33+
}
34+
35+
const cycleOrder: Record<ThemePreference, ThemePreference> = {
36+
light: `dark`,
37+
dark: `system`,
38+
system: `light`,
39+
}
40+
41+
export function DarkModeProvider({
42+
children,
43+
}: {
44+
children: React.ReactNode
45+
}): React.ReactElement {
46+
const [preference, setPreference] = useState<ThemePreference>(
47+
readInitialPreference
48+
)
49+
const [systemDark, setSystemDark] = useState<boolean>(systemPrefersDark)
50+
51+
useEffect(() => {
52+
if (typeof window === `undefined`) return
53+
const mql = window.matchMedia(`(prefers-color-scheme: dark)`)
54+
const onChange = (e: MediaQueryListEvent): void => setSystemDark(e.matches)
55+
mql.addEventListener(`change`, onChange)
56+
return () => mql.removeEventListener(`change`, onChange)
57+
}, [])
58+
59+
const darkMode = preference === `system` ? systemDark : preference === `dark`
60+
61+
useEffect(() => {
62+
document.documentElement.classList.toggle(`dark`, darkMode)
63+
}, [darkMode])
64+
65+
const cyclePreference = useCallback(() => {
66+
setPreference((current) => {
67+
const next = cycleOrder[current]
68+
window.localStorage.setItem(STORAGE_KEY, next)
69+
return next
70+
})
71+
}, [])
72+
73+
const value = useMemo(
74+
() => ({ darkMode, preference, cyclePreference }),
75+
[darkMode, preference, cyclePreference]
76+
)
77+
78+
return (
79+
<DarkModeContext.Provider value={value}>
80+
{children}
81+
</DarkModeContext.Provider>
82+
)
83+
}
84+
85+
export function useDarkModeContext(): DarkModeContextValue {
86+
const value = useContext(DarkModeContext)
87+
if (!value) {
88+
throw new Error(`useDarkModeContext must be used inside DarkModeProvider`)
89+
}
90+
return value
91+
}

‎examples/agents-chat-starter/src/ui/main.tsx‎

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
import { StrictMode, useState, useCallback, useEffect } from 'react'
22
import { createRoot } from 'react-dom/client'
3-
import { Theme, Flex, Text, Box, Heading } from '@radix-ui/themes'
3+
import { Theme, Flex, Text, Heading, IconButton } from '@radix-ui/themes'
4+
import { SunIcon, MoonIcon, DesktopIcon } from '@radix-ui/react-icons'
45
import '@radix-ui/themes/styles.css'
56
import { useChatroom } from './hooks/useChatroom.js'
67
import { useEntityTypes } from './hooks/useEntityTypes.js'
8+
import {
9+
DarkModeProvider,
10+
useDarkModeContext,
11+
type ThemePreference,
12+
} from './hooks/useDarkMode.js'
713
import { RoomsSidebar } from './components/RoomsSidebar.js'
814
import { ChatArea } from './components/ChatArea.js'
915
import { MembersSidebar } from './components/MembersSidebar.js'
1016
import './main.css'
1117

18+
function themeButtonIcon(preference: ThemePreference) {
19+
if (preference === `light`) return <SunIcon />
20+
if (preference === `dark`) return <MoonIcon />
21+
return <DesktopIcon />
22+
}
23+
24+
function themeButtonAriaLabel(preference: ThemePreference): string {
25+
if (preference === `light`) return `Switch to dark mode`
26+
if (preference === `dark`) return `Switch to system theme`
27+
return `Switch to light mode`
28+
}
29+
1230
interface Room {
1331
id: string
1432
name: string
@@ -21,6 +39,35 @@ function getRoomFromHash(): string | null {
2139
return hash || null
2240
}
2341

42+
function ThemeToggle() {
43+
const { preference, cyclePreference } = useDarkModeContext()
44+
return (
45+
<IconButton
46+
variant="ghost"
47+
size="2"
48+
color="gray"
49+
onClick={cyclePreference}
50+
aria-label={themeButtonAriaLabel(preference)}
51+
>
52+
{themeButtonIcon(preference)}
53+
</IconButton>
54+
)
55+
}
56+
57+
function ThemedApp() {
58+
const { darkMode } = useDarkModeContext()
59+
return (
60+
<Theme
61+
accentColor="indigo"
62+
grayColor="mauve"
63+
radius="medium"
64+
appearance={darkMode ? `dark` : `light`}
65+
>
66+
<App />
67+
</Theme>
68+
)
69+
}
70+
2471
function App() {
2572
const [config, setConfig] = useState<{ agentsUrl: string } | null>(null)
2673
const [rooms, setRooms] = useState<Room[]>([])
@@ -137,7 +184,13 @@ function App() {
137184
creating={creating}
138185
/>
139186
<Flex direction="column" flexGrow="1" style={{ minWidth: 0 }}>
140-
<Box px="3" py="3" className="chat-header">
187+
<Flex
188+
px="3"
189+
py="3"
190+
align="center"
191+
justify="between"
192+
className="chat-header"
193+
>
141194
{activeRoom ? (
142195
<Heading size="3">
143196
<Text color="gray"># </Text>
@@ -148,7 +201,8 @@ function App() {
148201
Chat
149202
</Heading>
150203
)}
151-
</Box>
204+
<ThemeToggle />
205+
</Flex>
152206
<Flex flexGrow="1" style={{ minHeight: 0 }}>
153207
<ChatArea
154208
messagesCollection={messagesCollection}
@@ -173,8 +227,8 @@ function App() {
173227

174228
createRoot(document.getElementById(`root`)!).render(
175229
<StrictMode>
176-
<Theme accentColor="indigo" grayColor="mauve" radius="medium">
177-
<App />
178-
</Theme>
230+
<DarkModeProvider>
231+
<ThemedApp />
232+
</DarkModeProvider>
179233
</StrictMode>
180234
)

‎examples/deep-survey/README.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,8 @@ pnpm install && pnpm --filter @electric-ax/agents-runtime build
8888
Create a `.env` file in this directory:
8989

9090
```bash
91-
ANTHROPIC_API_KEY=sk-ant-... # https://console.anthropic.com/ (optional if MOONSHOT_API_KEY is set)
92-
MOONSHOT_API_KEY=sk-... # https://platform.moonshot.ai/ (optional if ANTHROPIC_API_KEY is set)
93-
BRAVE_SEARCH_API_KEY=... # https://brave.com/search/api/ (optional)
91+
ANTHROPIC_API_KEY=sk-ant-...
92+
BRAVE_SEARCH_API_KEY=... # optional
9493
```
9594

9695
### 3. Start the Electric Agents server

0 commit comments

Comments
 (0)