@genui/a3-create 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +123 -0
  2. package/dist/index.js +684 -0
  3. package/package.json +52 -0
  4. package/template/.cursor/rules/example-app.mdc +9 -0
  5. package/template/CLAUDE.md +121 -0
  6. package/template/README.md +20 -0
  7. package/template/_gitignore +36 -0
  8. package/template/app/ThemeProvider.tsx +17 -0
  9. package/template/app/agents/age.ts +25 -0
  10. package/template/app/agents/greeting.ts +30 -0
  11. package/template/app/agents/index.ts +57 -0
  12. package/template/app/agents/onboarding/index.ts +15 -0
  13. package/template/app/agents/onboarding/prompt.ts +59 -0
  14. package/template/app/agents/registry.ts +17 -0
  15. package/template/app/agents/state.ts +10 -0
  16. package/template/app/api/agui/route.ts +56 -0
  17. package/template/app/api/chat/route.ts +35 -0
  18. package/template/app/api/stream/route.ts +57 -0
  19. package/template/app/apple-icon-dark.png +0 -0
  20. package/template/app/apple-icon.png +0 -0
  21. package/template/app/components/atoms/AgentNode.tsx +56 -0
  22. package/template/app/components/atoms/AppLogo.tsx +44 -0
  23. package/template/app/components/atoms/ChatContainer.tsx +13 -0
  24. package/template/app/components/atoms/ChatHeader.tsx +49 -0
  25. package/template/app/components/atoms/MarkdownRenderer.tsx +134 -0
  26. package/template/app/components/atoms/MessageBubble.tsx +21 -0
  27. package/template/app/components/atoms/TransitionEdge.tsx +49 -0
  28. package/template/app/components/atoms/index.ts +7 -0
  29. package/template/app/components/molecules/ChatInput.tsx +94 -0
  30. package/template/app/components/molecules/ChatMessage.tsx +45 -0
  31. package/template/app/components/molecules/index.ts +2 -0
  32. package/template/app/components/organisms/AgentGraph.tsx +75 -0
  33. package/template/app/components/organisms/AguiChat.tsx +133 -0
  34. package/template/app/components/organisms/Chat.tsx +88 -0
  35. package/template/app/components/organisms/ChatMessageList.tsx +35 -0
  36. package/template/app/components/organisms/ExamplePageLayout.tsx +118 -0
  37. package/template/app/components/organisms/OnboardingChat.tsx +24 -0
  38. package/template/app/components/organisms/Sidebar.tsx +147 -0
  39. package/template/app/components/organisms/SidebarLayout.tsx +58 -0
  40. package/template/app/components/organisms/StateViewer.tsx +126 -0
  41. package/template/app/components/organisms/StreamChat.tsx +173 -0
  42. package/template/app/components/organisms/index.ts +10 -0
  43. package/template/app/constants/chat.ts +52 -0
  44. package/template/app/constants/paths.ts +1 -0
  45. package/template/app/constants/ui.ts +61 -0
  46. package/template/app/examples/agui/page.tsx +26 -0
  47. package/template/app/examples/chat/page.tsx +26 -0
  48. package/template/app/examples/page.tsx +106 -0
  49. package/template/app/examples/stream/page.tsx +26 -0
  50. package/template/app/favicon-dark.ico +0 -0
  51. package/template/app/favicon.ico +0 -0
  52. package/template/app/icon.svg +13 -0
  53. package/template/app/layout.tsx +36 -0
  54. package/template/app/lib/actions/restartSession.ts +10 -0
  55. package/template/app/lib/getAgentGraphData.ts +43 -0
  56. package/template/app/lib/getGraphLayout.ts +99 -0
  57. package/template/app/lib/hooks/useRestart.ts +33 -0
  58. package/template/app/lib/parseTransitionTargets.ts +140 -0
  59. package/template/app/lib/providers/anthropic.ts +12 -0
  60. package/template/app/lib/providers/bedrock.ts +12 -0
  61. package/template/app/lib/providers/openai.ts +10 -0
  62. package/template/app/onboarding/page.tsx +21 -0
  63. package/template/app/page.tsx +16 -0
  64. package/template/app/styled.d.ts +6 -0
  65. package/template/app/theme.ts +22 -0
  66. package/template/docs/A3-README.md +121 -0
  67. package/template/docs/API-REFERENCE.md +85 -0
  68. package/template/docs/ARCHITECTURE.md +84 -0
  69. package/template/docs/CORE-CONCEPTS.md +347 -0
  70. package/template/docs/CUSTOM_LOGGING.md +36 -0
  71. package/template/docs/CUSTOM_PROVIDERS.md +642 -0
  72. package/template/docs/CUSTOM_STORES.md +228 -0
  73. package/template/docs/PROVIDER-ANTHROPIC.md +45 -0
  74. package/template/docs/PROVIDER-BEDROCK.md +45 -0
  75. package/template/docs/PROVIDER-OPENAI.md +47 -0
  76. package/template/docs/PROVIDERS.md +124 -0
  77. package/template/docs/QUICK-START-EXAMPLES.md +197 -0
  78. package/template/docs/RESILIENCE.md +226 -0
  79. package/template/docs/TRANSITIONS.md +245 -0
  80. package/template/docs/WIDGETS.md +331 -0
  81. package/template/docs/contributing/LOGGING.md +104 -0
  82. package/template/docs/designs/a3-gtm-strategy.md +280 -0
  83. package/template/docs/designs/a3-platform-vision.md +276 -0
  84. package/template/next-env.d.ts +6 -0
  85. package/template/next.config.mjs +15 -0
  86. package/template/package.json +41 -0
  87. package/template/public/android-chrome-192x192.png +0 -0
  88. package/template/public/android-chrome-512x512.png +0 -0
  89. package/template/public/site.webmanifest +11 -0
  90. package/template/scripts/dev.mjs +29 -0
  91. package/template/tsconfig.json +47 -0
@@ -0,0 +1,133 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback, useRef, useMemo } from 'react'
4
+ import { ChatMessageList } from '@organisms/ChatMessageList'
5
+ import { ChatContainer, ChatHeader } from '@atoms'
6
+ import { ChatInput } from '@molecules'
7
+ import { MessageSender } from '@genui/a3'
8
+ import type { Message } from '@genui/a3'
9
+ import { HttpAgent, EventType } from '@ag-ui/client'
10
+ import { CHAT_ERROR, CHAT_ERROR_SHORT } from '@constants/ui'
11
+ import { useRestart, type RestartResult } from '@lib/hooks/useRestart'
12
+
13
+ interface AguiChatProps {
14
+ sessionId: string
15
+ initialMessages?: Message[]
16
+ onSessionUpdate?: (update: { activeAgentId: string | null; state: Record<string, unknown> }) => void
17
+ onRestart?: () => Promise<RestartResult>
18
+ }
19
+
20
+ export function AguiChat({ sessionId, initialMessages, onSessionUpdate, onRestart }: AguiChatProps) {
21
+ const agent = useMemo(() => new HttpAgent({ url: '/api/agui', threadId: sessionId }), [sessionId])
22
+ const [messages, setMessages] = useState<Message[]>(initialMessages ?? [])
23
+ const [isLoading, setIsLoading] = useState(false)
24
+ const assistantIdRef = useRef<string>('')
25
+ const { isRestarting, handleRestart } = useRestart({ onRestart, setMessages, onSessionUpdate })
26
+
27
+ const handleSubmit = useCallback(
28
+ async (text: string) => {
29
+ const userMsg: Message = {
30
+ messageId: crypto.randomUUID(),
31
+ text,
32
+ metadata: { source: MessageSender.USER },
33
+ }
34
+ setMessages((prev) => [...prev, userMsg])
35
+ setIsLoading(true)
36
+
37
+ let assistantId = crypto.randomUUID()
38
+ assistantIdRef.current = assistantId
39
+
40
+ const streamingMsg: Message = {
41
+ messageId: assistantId,
42
+ text: '',
43
+ metadata: { source: MessageSender.ASSISTANT },
44
+ isStreaming: true,
45
+ }
46
+ setMessages((prev) => [...prev, streamingMsg])
47
+
48
+ try {
49
+ const runId = crypto.randomUUID()
50
+
51
+ // Add the user message to the agent's internal state BEFORE running.
52
+ agent.addMessage({ id: crypto.randomUUID(), role: 'user', content: text })
53
+
54
+ await agent.runAgent(
55
+ {
56
+ runId,
57
+ tools: [],
58
+ context: [],
59
+ forwardedProps: {},
60
+ },
61
+ {
62
+ onEvent({ event }) {
63
+ if (event.type === EventType.TEXT_MESSAGE_CONTENT && 'delta' in event) {
64
+ const delta = (event as unknown as { delta: string }).delta
65
+ setMessages((prev) =>
66
+ prev.map((m) => (m.messageId === assistantId ? { ...m, text: m.text + delta } : m)),
67
+ )
68
+ } else if (event.type === EventType.CUSTOM && 'name' in event) {
69
+ const customEvent = event as unknown as {
70
+ name: string
71
+ value?: { toAgentId?: string; state?: Record<string, unknown> }
72
+ }
73
+ if (customEvent.name === 'AgentTransition') {
74
+ onSessionUpdate?.({
75
+ activeAgentId: customEvent.value?.toAgentId ?? null,
76
+ state: customEvent.value?.state ?? {},
77
+ })
78
+ const prevAssistantId = assistantId
79
+ assistantId = crypto.randomUUID()
80
+ assistantIdRef.current = assistantId
81
+ setMessages((prev) => {
82
+ const updated = prev.map((m) =>
83
+ m.messageId === prevAssistantId ? { ...m, isStreaming: false } : m,
84
+ )
85
+ return [
86
+ ...updated,
87
+ {
88
+ messageId: assistantId,
89
+ text: '',
90
+ metadata: { source: MessageSender.ASSISTANT },
91
+ isStreaming: true,
92
+ },
93
+ ]
94
+ })
95
+ }
96
+ } else if (event.type === EventType.RUN_FINISHED) {
97
+ const result = (event as unknown as { result?: Record<string, unknown> }).result
98
+ onSessionUpdate?.({
99
+ activeAgentId: (result?.activeAgentId as string) ?? null,
100
+ state: (result?.state as Record<string, unknown>) ?? {},
101
+ })
102
+ setMessages((prev) => prev.map((m) => (m.messageId === assistantId ? { ...m, isStreaming: false } : m)))
103
+ } else if (event.type === EventType.RUN_ERROR) {
104
+ setMessages((prev) =>
105
+ prev.map((m) =>
106
+ m.messageId === assistantId ? { ...m, text: m.text || CHAT_ERROR_SHORT, isStreaming: false } : m,
107
+ ),
108
+ )
109
+ }
110
+ },
111
+ },
112
+ )
113
+ } catch (error) {
114
+ console.error('AG-UI chat error:', error)
115
+ setMessages((prev) =>
116
+ prev.map((m) => (m.messageId === assistantId ? { ...m, text: m.text || CHAT_ERROR, isStreaming: false } : m)),
117
+ )
118
+ } finally {
119
+ setIsLoading(false)
120
+ setMessages((prev) => prev.map((m) => (m.messageId === assistantId ? { ...m, isStreaming: false } : m)))
121
+ }
122
+ },
123
+ [agent, onSessionUpdate],
124
+ )
125
+
126
+ return (
127
+ <ChatContainer elevation={0}>
128
+ <ChatHeader onRestart={() => void handleRestart?.()} isRestarting={isRestarting} />
129
+ <ChatMessageList messages={messages} />
130
+ <ChatInput onSubmit={(text) => void handleSubmit(text)} disabled={isLoading || isRestarting} />
131
+ </ChatContainer>
132
+ )
133
+ }
@@ -0,0 +1,88 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import { CircularProgress } from '@mui/material'
5
+ import { ChatMessageList } from '@organisms/ChatMessageList'
6
+ import { ChatContainer, ChatHeader } from '@atoms'
7
+ import { ChatInput } from '@molecules'
8
+ import { MessageSender } from '@genui/a3'
9
+ import type { Message } from '@genui/a3'
10
+ import { CHAT_ERROR } from '@constants/ui'
11
+ import { useRestart, type RestartResult } from '@lib/hooks/useRestart'
12
+
13
+ type ChatApiResponse = {
14
+ response: string
15
+ activeAgentId: string | null
16
+ nextAgentId: string | null
17
+ state: Record<string, unknown>
18
+ goalAchieved: boolean
19
+ }
20
+
21
+ interface ChatProps {
22
+ sessionId: string
23
+ initialMessages?: Message[]
24
+ onSessionUpdate?: (update: { activeAgentId: string | null; state: Record<string, unknown> }) => void
25
+ onRestart?: () => Promise<RestartResult>
26
+ }
27
+
28
+ export function Chat({ sessionId, initialMessages, onSessionUpdate, onRestart }: ChatProps) {
29
+ const [messages, setMessages] = useState<Message[]>(initialMessages ?? [])
30
+ const [isLoading, setIsLoading] = useState(false)
31
+ const { isRestarting, handleRestart } = useRestart({ onRestart, setMessages, onSessionUpdate })
32
+
33
+ const handleSubmit = useCallback(
34
+ async (text: string) => {
35
+ const userMsg: Message = {
36
+ messageId: crypto.randomUUID(),
37
+ text,
38
+ metadata: { source: MessageSender.USER },
39
+ }
40
+ setMessages((prev) => [...prev, userMsg])
41
+ setIsLoading(true)
42
+
43
+ try {
44
+ const response = await fetch('/api/chat', {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: JSON.stringify({ message: text, sessionId }),
48
+ })
49
+
50
+ if (!response.ok) {
51
+ throw new Error(`HTTP error: ${response.status}`)
52
+ }
53
+
54
+ const data = (await response.json()) as ChatApiResponse
55
+
56
+ onSessionUpdate?.({ activeAgentId: data.activeAgentId, state: data.state })
57
+
58
+ const assistantMsg: Message = {
59
+ messageId: crypto.randomUUID(),
60
+ text: data.response,
61
+ metadata: { source: MessageSender.ASSISTANT },
62
+ }
63
+ setMessages((prev) => [...prev, assistantMsg])
64
+ } catch (error) {
65
+ console.error('Chat API error:', error)
66
+ const errorMsg: Message = {
67
+ messageId: crypto.randomUUID(),
68
+ text: CHAT_ERROR,
69
+ metadata: { source: MessageSender.ASSISTANT },
70
+ }
71
+ setMessages((prev) => [...prev, errorMsg])
72
+ } finally {
73
+ setIsLoading(false)
74
+ }
75
+ },
76
+ [onSessionUpdate, sessionId],
77
+ )
78
+
79
+ return (
80
+ <ChatContainer elevation={0}>
81
+ <ChatHeader onRestart={() => void handleRestart?.()} isRestarting={isRestarting}>
82
+ {isLoading && <CircularProgress size={16} />}
83
+ </ChatHeader>
84
+ <ChatMessageList messages={messages} />
85
+ <ChatInput onSubmit={handleSubmit} disabled={isLoading || isRestarting} />
86
+ </ChatContainer>
87
+ )
88
+ }
@@ -0,0 +1,35 @@
1
+ 'use client'
2
+
3
+ import { useRef, useEffect } from 'react'
4
+ import styled from 'styled-components'
5
+ import { Box } from '@mui/material'
6
+ import type { Theme } from '@mui/material/styles'
7
+ import { ChatMessage } from '@molecules'
8
+ import type { Message } from '@genui/a3'
9
+
10
+ const MessageListContainer = styled(Box)`
11
+ flex: 1;
12
+ overflow-y: auto;
13
+ padding: ${({ theme }) => (theme as Theme).spacing(2)};
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: ${({ theme }) => (theme as Theme).spacing(1.5)};
17
+ `
18
+
19
+ export function ChatMessageList({ messages }: { messages: Message[] }) {
20
+ const bottomRef = useRef<HTMLDivElement>(null)
21
+ const lastMessage = messages[messages.length - 1]
22
+
23
+ useEffect(() => {
24
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
25
+ }, [messages.length, lastMessage?.text])
26
+
27
+ return (
28
+ <MessageListContainer>
29
+ {messages.map((m) => (
30
+ <ChatMessage key={m.messageId} message={m} />
31
+ ))}
32
+ <div ref={bottomRef} />
33
+ </MessageListContainer>
34
+ )
35
+ }
@@ -0,0 +1,118 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import { Box, Typography } from '@mui/material'
5
+ import { AgentGraph } from './AgentGraph'
6
+ import { Chat } from './Chat'
7
+ import { AguiChat } from './AguiChat'
8
+ import { StreamChat } from './StreamChat'
9
+ import { StateViewer } from './StateViewer'
10
+ import { restartSession } from '@lib/actions/restartSession'
11
+ import type { AgentInfo } from '@lib/getAgentGraphData'
12
+ import type { Message } from '@genui/a3'
13
+
14
+ type ExampleVariant = 'blocking' | 'stream' | 'agui'
15
+
16
+ interface ExamplePageLayoutProps {
17
+ title: string
18
+ description: string
19
+ sessionId: string
20
+ initialMessages: Message[]
21
+ variant: ExampleVariant
22
+ agents: AgentInfo[]
23
+ initialActiveAgentId?: string | null
24
+ initialState?: Record<string, unknown>
25
+ }
26
+
27
+ export function ExamplePageLayout({
28
+ title,
29
+ description,
30
+ sessionId,
31
+ initialMessages,
32
+ variant,
33
+ agents,
34
+ initialActiveAgentId,
35
+ initialState,
36
+ }: ExamplePageLayoutProps) {
37
+ const [activeAgentId, setActiveAgentId] = useState<string | null>(initialActiveAgentId ?? null)
38
+ const [state, setState] = useState<Record<string, unknown>>(initialState ?? {})
39
+
40
+ const handleSessionUpdate = useCallback(
41
+ (update: { activeAgentId: string | null; state: Record<string, unknown> }) => {
42
+ setActiveAgentId(update.activeAgentId)
43
+ setState(update.state)
44
+ },
45
+ [],
46
+ )
47
+
48
+ const handleRestart = useCallback(async () => {
49
+ const fresh = await restartSession(sessionId)
50
+ return {
51
+ messages: fresh.messages,
52
+ activeAgentId: fresh.activeAgentId,
53
+ state: fresh.state as Record<string, unknown>,
54
+ }
55
+ }, [sessionId])
56
+
57
+ return (
58
+ <Box
59
+ sx={{
60
+ display: 'flex',
61
+ flexDirection: 'column',
62
+ height: '100%',
63
+ px: { xs: 2, sm: 3, md: 5, lg: 8 },
64
+ maxWidth: 1600,
65
+ width: '100%',
66
+ mx: 'auto',
67
+ }}
68
+ >
69
+ <Typography variant="h5" fontWeight="bold" sx={{ pt: 3 }}>
70
+ {title}
71
+ </Typography>
72
+ <Typography variant="body1" color="text.secondary" sx={{ pt: 1, pb: 0, lineHeight: 1.6 }}>
73
+ {description}
74
+ </Typography>
75
+ <Box
76
+ sx={{
77
+ flex: 1,
78
+ py: 3,
79
+ minHeight: 0,
80
+ display: 'grid',
81
+ gridTemplateColumns: { xs: '1fr', md: '60% 40%' },
82
+ gap: 3,
83
+ }}
84
+ >
85
+ <Box sx={{ minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
86
+ {variant === 'blocking' && (
87
+ <Chat
88
+ sessionId={sessionId}
89
+ initialMessages={initialMessages}
90
+ onSessionUpdate={handleSessionUpdate}
91
+ onRestart={handleRestart}
92
+ />
93
+ )}
94
+ {variant === 'stream' && (
95
+ <StreamChat
96
+ sessionId={sessionId}
97
+ initialMessages={initialMessages}
98
+ onSessionUpdate={handleSessionUpdate}
99
+ onRestart={handleRestart}
100
+ />
101
+ )}
102
+ {variant === 'agui' && (
103
+ <AguiChat
104
+ sessionId={sessionId}
105
+ initialMessages={initialMessages}
106
+ onSessionUpdate={handleSessionUpdate}
107
+ onRestart={handleRestart}
108
+ />
109
+ )}
110
+ </Box>
111
+ <Box sx={{ minHeight: 0, display: 'flex', flexDirection: 'column', gap: 3, overflow: 'auto' }}>
112
+ <AgentGraph agents={agents} activeAgentId={activeAgentId} />
113
+ <StateViewer state={state} />
114
+ </Box>
115
+ </Box>
116
+ </Box>
117
+ )
118
+ }
@@ -0,0 +1,24 @@
1
+ 'use client'
2
+
3
+ import { useCallback } from 'react'
4
+ import { StreamChat } from './StreamChat'
5
+ import { restartSession } from '@lib/actions/restartSession'
6
+ import type { Message } from '@genui/a3'
7
+
8
+ interface OnboardingChatProps {
9
+ sessionId: string
10
+ initialMessages: Message[]
11
+ }
12
+
13
+ export function OnboardingChat({ sessionId, initialMessages }: OnboardingChatProps) {
14
+ const handleRestart = useCallback(async () => {
15
+ const fresh = await restartSession(sessionId)
16
+ return {
17
+ messages: fresh.messages,
18
+ activeAgentId: fresh.activeAgentId,
19
+ state: fresh.state as Record<string, unknown>,
20
+ }
21
+ }, [sessionId])
22
+
23
+ return <StreamChat sessionId={sessionId} initialMessages={initialMessages} onRestart={handleRestart} />
24
+ }
@@ -0,0 +1,147 @@
1
+ 'use client'
2
+
3
+ import { usePathname } from 'next/navigation'
4
+ import Link from 'next/link'
5
+ import { Box, Drawer, List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
6
+ import HomeOutlined from '@mui/icons-material/HomeOutlined'
7
+ import SmartToyOutlined from '@mui/icons-material/SmartToyOutlined'
8
+ import CodeOutlined from '@mui/icons-material/CodeOutlined'
9
+ import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline'
10
+ import StreamIcon from '@mui/icons-material/Stream'
11
+ import ExtensionIcon from '@mui/icons-material/Extension'
12
+ import { AppLogo } from '@atoms'
13
+ import { SIDEBAR_WIDTH } from './SidebarLayout'
14
+ import { NAV_HOME, NAV_ONBOARDING, NAV_EXAMPLES, NAV_CHAT, NAV_STREAMING, NAV_AGUI } from '@constants/ui'
15
+
16
+ interface SidebarProps {
17
+ open: boolean
18
+ onClose: () => void
19
+ }
20
+
21
+ export function Sidebar({ open, onClose }: SidebarProps) {
22
+ const pathname = usePathname()
23
+
24
+ const drawerContent = (
25
+ <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
26
+ <Box sx={{ p: 2, display: 'flex', alignItems: 'center', gap: 1.5 }}>
27
+ <AppLogo width={28} height={28} />
28
+ </Box>
29
+
30
+ <List component="nav" sx={{ px: 1 }}>
31
+ <ListItemButton
32
+ component={Link}
33
+ href="/"
34
+ selected={pathname === '/'}
35
+ onClick={onClose}
36
+ sx={{ borderRadius: 1, mb: 0.5 }}
37
+ >
38
+ <ListItemIcon sx={{ minWidth: 40 }}>
39
+ <HomeOutlined />
40
+ </ListItemIcon>
41
+ <ListItemText primary={NAV_HOME} />
42
+ </ListItemButton>
43
+
44
+ <ListItemButton
45
+ component={Link}
46
+ href="/onboarding"
47
+ selected={pathname === '/onboarding'}
48
+ onClick={onClose}
49
+ sx={{ borderRadius: 1, mb: 0.5 }}
50
+ >
51
+ <ListItemIcon sx={{ minWidth: 40 }}>
52
+ <SmartToyOutlined />
53
+ </ListItemIcon>
54
+ <ListItemText primary={NAV_ONBOARDING} />
55
+ </ListItemButton>
56
+
57
+ <ListItemButton
58
+ component={Link}
59
+ href="/examples"
60
+ selected={pathname === '/examples'}
61
+ onClick={onClose}
62
+ sx={{ borderRadius: 1, mb: 0.5 }}
63
+ >
64
+ <ListItemIcon sx={{ minWidth: 40 }}>
65
+ <CodeOutlined />
66
+ </ListItemIcon>
67
+ <ListItemText primary={NAV_EXAMPLES} />
68
+ </ListItemButton>
69
+
70
+ <List component="div" disablePadding>
71
+ <ListItemButton
72
+ component={Link}
73
+ href="/examples/chat"
74
+ selected={pathname === '/examples/chat'}
75
+ onClick={onClose}
76
+ sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
77
+ >
78
+ <ListItemIcon sx={{ minWidth: 40 }}>
79
+ <ChatBubbleOutlineIcon />
80
+ </ListItemIcon>
81
+ <ListItemText primary={NAV_CHAT} />
82
+ </ListItemButton>
83
+
84
+ <ListItemButton
85
+ component={Link}
86
+ href="/examples/stream"
87
+ selected={pathname === '/examples/stream'}
88
+ onClick={onClose}
89
+ sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
90
+ >
91
+ <ListItemIcon sx={{ minWidth: 40 }}>
92
+ <StreamIcon />
93
+ </ListItemIcon>
94
+ <ListItemText primary={NAV_STREAMING} />
95
+ </ListItemButton>
96
+
97
+ <ListItemButton
98
+ component={Link}
99
+ href="/examples/agui"
100
+ selected={pathname === '/examples/agui'}
101
+ onClick={onClose}
102
+ sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
103
+ >
104
+ <ListItemIcon sx={{ minWidth: 40 }}>
105
+ <ExtensionIcon />
106
+ </ListItemIcon>
107
+ <ListItemText primary={NAV_AGUI} />
108
+ </ListItemButton>
109
+ </List>
110
+ </List>
111
+ </Box>
112
+ )
113
+
114
+ return (
115
+ <>
116
+ {/* Mobile drawer */}
117
+ <Drawer
118
+ variant="temporary"
119
+ open={open}
120
+ onClose={onClose}
121
+ ModalProps={{ keepMounted: true }}
122
+ sx={{
123
+ display: { xs: 'block', md: 'none' },
124
+ '& .MuiDrawer-paper': { width: SIDEBAR_WIDTH },
125
+ }}
126
+ >
127
+ {drawerContent}
128
+ </Drawer>
129
+
130
+ {/* Desktop drawer */}
131
+ <Drawer
132
+ variant="permanent"
133
+ sx={{
134
+ display: { xs: 'none', md: 'block' },
135
+ '& .MuiDrawer-paper': {
136
+ width: SIDEBAR_WIDTH,
137
+ borderRight: 1,
138
+ borderColor: 'divider',
139
+ },
140
+ }}
141
+ open
142
+ >
143
+ {drawerContent}
144
+ </Drawer>
145
+ </>
146
+ )
147
+ }
@@ -0,0 +1,58 @@
1
+ 'use client'
2
+
3
+ import { ReactNode, useState } from 'react'
4
+ import { Box, IconButton, useMediaQuery, useTheme } from '@mui/material'
5
+ import MenuIcon from '@mui/icons-material/Menu'
6
+ import { AppLogo } from '@atoms'
7
+ import { Sidebar } from './Sidebar'
8
+
9
+ export const SIDEBAR_WIDTH = 260
10
+
11
+ interface SidebarLayoutProps {
12
+ children: ReactNode
13
+ }
14
+
15
+ export function SidebarLayout({ children }: SidebarLayoutProps) {
16
+ const theme = useTheme()
17
+ const isDesktop = useMediaQuery(theme.breakpoints.up('md'))
18
+ const [mobileOpen, setMobileOpen] = useState(false)
19
+
20
+ return (
21
+ <Box sx={{ display: 'flex', height: '100vh' }}>
22
+ <Sidebar open={mobileOpen} onClose={() => setMobileOpen(false)} />
23
+
24
+ <Box
25
+ component="main"
26
+ sx={{
27
+ flexGrow: 1,
28
+ display: 'flex',
29
+ flexDirection: 'column',
30
+ overflow: 'auto',
31
+ height: '100vh',
32
+ ml: { md: `${SIDEBAR_WIDTH}px` },
33
+ }}
34
+ >
35
+ {!isDesktop && (
36
+ <Box
37
+ sx={{
38
+ p: 1,
39
+ px: 2,
40
+ display: 'flex',
41
+ alignItems: 'center',
42
+ gap: 1,
43
+ borderBottom: 1,
44
+ borderColor: 'divider',
45
+ bgcolor: 'background.paper',
46
+ }}
47
+ >
48
+ <IconButton onClick={() => setMobileOpen(true)} edge="start">
49
+ <MenuIcon />
50
+ </IconButton>
51
+ <AppLogo width={24} height={24} />
52
+ </Box>
53
+ )}
54
+ {children}
55
+ </Box>
56
+ </Box>
57
+ )
58
+ }