@townco/gui-template 0.1.36 → 0.1.37

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.
package/src/ChatView.tsx DELETED
@@ -1,434 +0,0 @@
1
- import { createLogger } from "@townco/core";
2
- import type { AcpClient } from "@townco/ui";
3
- import {
4
- useChatMessages,
5
- useChatSession,
6
- useChatStore,
7
- useToolCalls,
8
- } from "@townco/ui/core";
9
- import {
10
- ChatEmptyState,
11
- ChatHeader,
12
- ChatInputActions,
13
- ChatInputAttachment,
14
- ChatInputCommandMenu,
15
- ChatInputField,
16
- ChatInputRoot,
17
- ChatInputSubmit,
18
- ChatInputToolbar,
19
- ChatInputVoiceInput,
20
- ChatLayout,
21
- type CommandMenuItem,
22
- cn,
23
- FilesTabContent,
24
- Message,
25
- MessageContent,
26
- PanelTabsHeader,
27
- SourcesTabContent,
28
- Tabs,
29
- TabsContent,
30
- TodoTabContent,
31
- } from "@townco/ui/gui";
32
- import {
33
- ArrowUp,
34
- ChevronUp,
35
- Code,
36
- PanelRight,
37
- Settings,
38
- Sparkles,
39
- } from "lucide-react";
40
- import { useEffect, useState } from "react";
41
-
42
- const logger = createLogger("gui");
43
-
44
- // Helper component to provide openFiles callback
45
- function OpenFilesButton({
46
- children,
47
- }: {
48
- children: (props: { openFiles: () => void }) => React.ReactNode;
49
- }) {
50
- const { setPanelSize, setActiveTab } = ChatLayout.useChatLayoutContext();
51
-
52
- const openFiles = () => {
53
- setPanelSize("small");
54
- setActiveTab("files");
55
- };
56
-
57
- return <>{children({ openFiles })}</>;
58
- }
59
-
60
- export interface ChatViewProps {
61
- client: AcpClient | null;
62
- initialSessionId?: string | null;
63
- }
64
-
65
- // Mobile header component that uses ChatHeader context
66
- function MobileHeader({
67
- agentName,
68
- showHeader,
69
- }: {
70
- agentName: string;
71
- showHeader: boolean;
72
- }) {
73
- const { isExpanded, setIsExpanded } = ChatHeader.useChatHeaderContext();
74
-
75
- return (
76
- <div className="flex lg:hidden items-center gap-2 flex-1">
77
- {showHeader && (
78
- <div className="flex items-center gap-2 flex-1">
79
- <h1 className="text-heading-4 text-foreground">{agentName}</h1>
80
- </div>
81
- )}
82
- {!showHeader && <div className="flex-1" />}
83
- <button
84
- type="button"
85
- className="flex items-center justify-center shrink-0 cursor-pointer"
86
- aria-label="Toggle menu"
87
- onClick={() => setIsExpanded(!isExpanded)}
88
- >
89
- <ChevronUp
90
- className={cn(
91
- "size-4 text-muted-foreground transition-transform duration-200",
92
- isExpanded ? "" : "rotate-180",
93
- )}
94
- />
95
- </button>
96
- </div>
97
- );
98
- }
99
-
100
- // Header component that uses ChatLayout context (must be inside ChatLayout.Root)
101
- function AppChatHeader({
102
- agentName,
103
- showHeader,
104
- }: {
105
- agentName: string;
106
- showHeader: boolean;
107
- }) {
108
- const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
109
-
110
- return (
111
- <ChatHeader.Root
112
- className={cn(
113
- "border-b border-border bg-card relative lg:p-0",
114
- "[border-bottom-width:0.5px]",
115
- )}
116
- >
117
- {/* Desktop view: 64px height, padding 20px vertical, 24px left, 16px right */}
118
- <div className="hidden lg:flex items-center gap-2 w-full h-16 py-5 pl-6 pr-4">
119
- {showHeader && (
120
- <div className="flex items-center gap-2 flex-1">
121
- <h1 className="text-heading-4 text-foreground">{agentName}</h1>
122
- </div>
123
- )}
124
- {!showHeader && <div className="flex-1" />}
125
- <button
126
- type="button"
127
- className="flex items-center justify-center shrink-0 cursor-pointer"
128
- aria-label="Toggle sidebar"
129
- onClick={() => {
130
- setPanelSize(panelSize === "hidden" ? "small" : "hidden");
131
- }}
132
- >
133
- <PanelRight className="size-4 text-muted-foreground" />
134
- </button>
135
- </div>
136
-
137
- {/* Mobile view: conditionally show agent name based on showHeader */}
138
- <MobileHeader agentName={agentName} showHeader={showHeader} />
139
-
140
- {/* Expandable Panel for Mobile - always available */}
141
- <ChatHeader.ExpandablePanel
142
- className={cn(
143
- "pt-6 pb-8 px-6",
144
- "border-b border-border bg-card",
145
- "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]",
146
- "[border-bottom-width:0.5px]",
147
- )}
148
- >
149
- <Tabs defaultValue="todo" className="w-full">
150
- <PanelTabsHeader
151
- showIcons={true}
152
- visibleTabs={["todo", "files", "sources"]}
153
- variant="default"
154
- />
155
- <TabsContent value="todo" className="mt-4">
156
- <TodoTabContent />
157
- </TabsContent>
158
- <TabsContent value="files" className="mt-4">
159
- <FilesTabContent />
160
- </TabsContent>
161
- <TabsContent value="sources" className="mt-4">
162
- <SourcesTabContent />
163
- </TabsContent>
164
- </Tabs>
165
- </ChatHeader.ExpandablePanel>
166
- </ChatHeader.Root>
167
- );
168
- }
169
-
170
- export function ChatView({ client, initialSessionId }: ChatViewProps) {
171
- // Use shared hooks from @townco/ui/core
172
- const { connectionStatus, connect, sessionId, startSession } = useChatSession(
173
- client,
174
- initialSessionId,
175
- );
176
- const { messages, sendMessage } = useChatMessages(client, startSession);
177
- useToolCalls(client); // Still need to subscribe to tool call events
178
- const error = useChatStore((state) => state.error);
179
- const [agentName, setAgentName] = useState<string>("Agent");
180
- const [isLargeScreen, setIsLargeScreen] = useState(
181
- typeof window !== "undefined" ? window.innerWidth >= 1024 : true,
182
- );
183
- const [placeholder, setPlaceholder] = useState<string>(
184
- "Type a message or / for commands...",
185
- );
186
-
187
- // Log connection status changes
188
- useEffect(() => {
189
- logger.debug("Connection status changed", { status: connectionStatus });
190
-
191
- if (connectionStatus === "error" && error) {
192
- logger.error("Connection error occurred", { error });
193
- }
194
- }, [connectionStatus, error]);
195
-
196
- // Get agent name from session metadata
197
- useEffect(() => {
198
- if (client && sessionId) {
199
- const session = client.getCurrentSession();
200
- if (session?.metadata?.agentName) {
201
- setAgentName(session.metadata.agentName);
202
- }
203
- }
204
- }, [client, sessionId]);
205
-
206
- // Monitor screen size changes and update isLargeScreen state
207
- useEffect(() => {
208
- const mediaQuery = window.matchMedia("(min-width: 1024px)");
209
-
210
- const handleChange = (e: MediaQueryListEvent) => {
211
- setIsLargeScreen(e.matches);
212
- };
213
-
214
- // Set initial value
215
- setIsLargeScreen(mediaQuery.matches);
216
-
217
- // Listen for changes
218
- mediaQuery.addEventListener("change", handleChange);
219
-
220
- return () => {
221
- mediaQuery.removeEventListener("change", handleChange);
222
- };
223
- }, []);
224
-
225
- // Handle prompt hover - temporarily show the full prompt as placeholder
226
- const handlePromptHover = (prompt: string) => {
227
- setPlaceholder(prompt);
228
- };
229
-
230
- // Handle prompt leave - restore the default placeholder
231
- const handlePromptLeave = () => {
232
- setPlaceholder("Type a message or / for commands...");
233
- };
234
-
235
- // Command menu items for chat input
236
- const commandMenuItems: CommandMenuItem[] = [
237
- {
238
- id: "model-sonnet",
239
- label: "Use Sonnet 4.5",
240
- description: "Switch to Claude Sonnet 4.5 model",
241
- icon: <Sparkles className="h-4 w-4" />,
242
- category: "model",
243
- onSelect: () => {
244
- logger.info("User selected Sonnet 4.5 model");
245
- },
246
- },
247
- {
248
- id: "model-opus",
249
- label: "Use Opus",
250
- description: "Switch to Claude Opus model",
251
- icon: <Sparkles className="h-4 w-4" />,
252
- category: "model",
253
- onSelect: () => {
254
- logger.info("User selected Opus model");
255
- },
256
- },
257
- {
258
- id: "settings",
259
- label: "Open Settings",
260
- description: "Configure chat preferences",
261
- icon: <Settings className="h-4 w-4" />,
262
- category: "action",
263
- onSelect: () => {
264
- logger.info("User opened settings");
265
- },
266
- },
267
- {
268
- id: "code-mode",
269
- label: "Code Mode",
270
- description: "Enable code-focused responses",
271
- icon: <Code className="h-4 w-4" />,
272
- category: "mode",
273
- onSelect: () => {
274
- logger.info("User enabled code mode");
275
- },
276
- },
277
- ];
278
-
279
- return (
280
- <ChatLayout.Root defaultPanelSize="hidden" defaultActiveTab="todo">
281
- {/* Main: Vertical container for Messages + Footer */}
282
- <ChatLayout.Main>
283
- {/* Top Row */}
284
- <AppChatHeader agentName={agentName} showHeader={messages.length > 0} />
285
-
286
- {/* Connection Error Banner */}
287
- {connectionStatus === "error" && error && (
288
- <div className="border-b border-destructive/20 bg-destructive/10 px-6 py-4">
289
- <div className="flex items-start justify-between gap-4">
290
- <div className="flex-1">
291
- <h3 className="mb-1 text-paragraph-sm font-semibold text-destructive">
292
- Connection Error
293
- </h3>
294
- <p className="whitespace-pre-line text-paragraph-sm text-foreground">
295
- {error}
296
- </p>
297
- </div>
298
- <button
299
- type="button"
300
- onClick={connect}
301
- className="rounded-lg bg-destructive px-4 py-2 text-paragraph-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive-hover"
302
- >
303
- Retry
304
- </button>
305
- </div>
306
- </div>
307
- )}
308
-
309
- {/* Body: Container for Messages + Footer + Toaster */}
310
- <ChatLayout.Body>
311
- {/* Messages: Scrollable message area */}
312
- <ChatLayout.Messages>
313
- {messages.length === 0 ? (
314
- <OpenFilesButton>
315
- {({ openFiles }) => (
316
- <div className="flex flex-1 items-center px-4">
317
- <ChatEmptyState
318
- title={agentName}
319
- description="This agent can help you with your tasks. Start a conversation by typing a message below."
320
- suggestedPrompts={[
321
- "Search the web for the latest news on top tech company earnings, produce a summary for each company, and then a macro trend analysis of the tech industry. Use your todo list",
322
- "Explain how this works",
323
- "Create a new feature",
324
- "Review my changes",
325
- ]}
326
- onPromptClick={(prompt) => {
327
- sendMessage(prompt);
328
- setPlaceholder("Type a message or / for commands...");
329
- logger.info("Prompt clicked", { prompt });
330
- }}
331
- onPromptHover={handlePromptHover}
332
- onPromptLeave={handlePromptLeave}
333
- onOpenFiles={openFiles}
334
- />
335
- </div>
336
- )}
337
- </OpenFilesButton>
338
- ) : (
339
- <div className="flex flex-col px-4">
340
- {messages.map((message, index) => {
341
- // Calculate dynamic spacing based on message sequence
342
- const isFirst = index === 0;
343
- const previousMessage = isFirst ? null : messages[index - 1];
344
-
345
- let spacingClass = "mt-2";
346
-
347
- if (isFirst) {
348
- spacingClass = "mt-2";
349
- } else if (message.role === "user") {
350
- // User message usually starts a new turn
351
- spacingClass =
352
- previousMessage?.role === "user" ? "mt-4" : "mt-4";
353
- } else if (message.role === "assistant") {
354
- // Assistant message is usually a response
355
- spacingClass =
356
- previousMessage?.role === "assistant" ? "mt-2" : "mt-6";
357
- }
358
-
359
- return (
360
- <Message
361
- key={message.id}
362
- message={message}
363
- className={spacingClass}
364
- isLastMessage={index === messages.length - 1}
365
- >
366
- <MessageContent
367
- message={message}
368
- thinkingDisplayStyle="collapsible"
369
- />
370
- </Message>
371
- );
372
- })}
373
- </div>
374
- )}
375
- </ChatLayout.Messages>
376
-
377
- {/* Footer: Input area */}
378
- <ChatLayout.Footer>
379
- <ChatInputRoot client={client}>
380
- <ChatInputCommandMenu commands={commandMenuItems} />
381
- <ChatInputField placeholder={placeholder} autoFocus />
382
- <ChatInputToolbar>
383
- <div className="flex items-center gap-1">
384
- <ChatInputActions />
385
- <ChatInputAttachment />
386
- </div>
387
- <div className="flex items-center gap-1">
388
- <ChatInputVoiceInput />
389
- <ChatInputSubmit>
390
- <ArrowUp className="size-4" />
391
- </ChatInputSubmit>
392
- </div>
393
- </ChatInputToolbar>
394
- </ChatInputRoot>
395
- </ChatLayout.Footer>
396
- </ChatLayout.Body>
397
- </ChatLayout.Main>
398
-
399
- {/* Aside: Right side panel - only render on large screens */}
400
- {isLargeScreen && (
401
- <ChatLayout.Aside breakpoint="lg">
402
- <Tabs defaultValue="todo" className="flex flex-col h-full">
403
- {/* Desktop sidebar header (64px) */}
404
- <div
405
- className={cn(
406
- "border-b border-border bg-card",
407
- "px-6 py-2 h-16",
408
- "flex items-center",
409
- "[border-bottom-width:0.5px]",
410
- )}
411
- >
412
- <PanelTabsHeader
413
- showIcons={true}
414
- visibleTabs={["todo", "files", "sources"]}
415
- variant="compact"
416
- />
417
- </div>
418
-
419
- {/* Desktop sidebar content */}
420
- <TabsContent value="todo" className="flex-1 p-4 mt-0">
421
- <TodoTabContent />
422
- </TabsContent>
423
- <TabsContent value="files" className="flex-1 p-4 mt-0">
424
- <FilesTabContent />
425
- </TabsContent>
426
- <TabsContent value="sources" className="flex-1 p-4 mt-0">
427
- <SourcesTabContent />
428
- </TabsContent>
429
- </Tabs>
430
- </ChatLayout.Aside>
431
- )}
432
- </ChatLayout.Root>
433
- );
434
- }