@polpo-ai/dashboard 0.1.0

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 (135) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +44 -0
  3. package/app/(dashboard)/error.tsx +47 -0
  4. package/app/(dashboard)/layout.tsx +41 -0
  5. package/app/(dashboard)/loading.tsx +30 -0
  6. package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
  7. package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
  8. package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
  9. package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
  10. package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
  11. package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
  12. package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
  13. package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
  14. package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
  15. package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
  16. package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
  17. package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
  18. package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
  19. package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
  20. package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
  21. package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
  22. package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
  23. package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
  24. package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
  25. package/app/(dashboard)/projects/[id]/page.tsx +68 -0
  26. package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
  27. package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
  28. package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
  29. package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
  30. package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
  31. package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
  32. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
  33. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
  34. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
  35. package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
  36. package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
  37. package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
  38. package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
  39. package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
  40. package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
  41. package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
  42. package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
  43. package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
  44. package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
  45. package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
  46. package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
  47. package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
  48. package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
  49. package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
  50. package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
  51. package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
  52. package/app/(dashboard)/projects/[id]/view.tsx +421 -0
  53. package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
  54. package/app/(dashboard)/projects/projects-list.tsx +126 -0
  55. package/app/(playground)/error.tsx +42 -0
  56. package/app/(playground)/layout.tsx +27 -0
  57. package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
  58. package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
  59. package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
  60. package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
  61. package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
  62. package/app/favicon.ico +0 -0
  63. package/app/globals.css +385 -0
  64. package/app/icon.svg +6 -0
  65. package/app/layout.tsx +39 -0
  66. package/app/page.tsx +11 -0
  67. package/components/ai-elements/code-block.tsx +562 -0
  68. package/components/dashboard/agent-capabilities.tsx +326 -0
  69. package/components/dashboard/agent-identity-header.tsx +50 -0
  70. package/components/dashboard/agent-model-picker.tsx +195 -0
  71. package/components/dashboard/agent-model-selector.tsx +121 -0
  72. package/components/dashboard/agent-picker-card.tsx +46 -0
  73. package/components/dashboard/agent-studio.tsx +242 -0
  74. package/components/dashboard/agents-table.tsx +48 -0
  75. package/components/dashboard/breadcrumb.tsx +70 -0
  76. package/components/dashboard/builder-chat.tsx +418 -0
  77. package/components/dashboard/chat-shell.tsx +40 -0
  78. package/components/dashboard/client-picker.tsx +63 -0
  79. package/components/dashboard/command-snippet.tsx +77 -0
  80. package/components/dashboard/connect-dialog.tsx +680 -0
  81. package/components/dashboard/copy-button.tsx +45 -0
  82. package/components/dashboard/copy-card.tsx +48 -0
  83. package/components/dashboard/file-browser.tsx +161 -0
  84. package/components/dashboard/hint.tsx +22 -0
  85. package/components/dashboard/inference-mode.tsx +15 -0
  86. package/components/dashboard/manual-refresh-button.tsx +52 -0
  87. package/components/dashboard/markdown.tsx +31 -0
  88. package/components/dashboard/mcp-install-panel.tsx +196 -0
  89. package/components/dashboard/mission-graph.tsx +262 -0
  90. package/components/dashboard/nav-tabs.tsx +86 -0
  91. package/components/dashboard/polpo-chat.tsx +468 -0
  92. package/components/dashboard/project-copilot.tsx +253 -0
  93. package/components/dashboard/sdk-snippet-panel.tsx +194 -0
  94. package/components/dashboard/section-header.tsx +37 -0
  95. package/components/dashboard/settings-form.tsx +2477 -0
  96. package/components/dashboard/sidebar.tsx +385 -0
  97. package/components/dashboard/skeletons.tsx +1054 -0
  98. package/components/dashboard/skills-install-wizard.tsx +273 -0
  99. package/components/dashboard/swarm-runs.tsx +316 -0
  100. package/components/dashboard/tab-toggle.tsx +35 -0
  101. package/components/dashboard/task-studio.tsx +224 -0
  102. package/components/dashboard/top-header.tsx +203 -0
  103. package/components/dashboard/webhook-deliveries.tsx +234 -0
  104. package/components/icons/coding-agents.tsx +151 -0
  105. package/components/json-ld.tsx +8 -0
  106. package/components/providers.tsx +25 -0
  107. package/components/ui/badge.tsx +52 -0
  108. package/components/ui/button.tsx +60 -0
  109. package/components/ui/card.tsx +103 -0
  110. package/components/ui/chart.tsx +356 -0
  111. package/components/ui/command.tsx +196 -0
  112. package/components/ui/dialog.tsx +157 -0
  113. package/components/ui/input-group.tsx +158 -0
  114. package/components/ui/input.tsx +20 -0
  115. package/components/ui/multi-select.tsx +161 -0
  116. package/components/ui/popover.tsx +90 -0
  117. package/components/ui/select.tsx +201 -0
  118. package/components/ui/separator.tsx +25 -0
  119. package/components/ui/sheet.tsx +135 -0
  120. package/components/ui/skeleton.tsx +13 -0
  121. package/components/ui/tabs.tsx +56 -0
  122. package/components/ui/textarea.tsx +18 -0
  123. package/components/ui/tooltip.tsx +66 -0
  124. package/hooks/use-desktop-sidebar.tsx +50 -0
  125. package/hooks/use-mobile-sidebar.tsx +37 -0
  126. package/index.ts +14 -0
  127. package/lib/api.ts +194 -0
  128. package/lib/builder-context.ts +60 -0
  129. package/lib/data-client.ts +68 -0
  130. package/lib/get-query-client.ts +25 -0
  131. package/lib/polpo-client.tsx +49 -0
  132. package/lib/tool-catalog.ts +234 -0
  133. package/lib/use-event-catalog.ts +28 -0
  134. package/lib/utils.ts +6 -0
  135. package/package.json +99 -0
@@ -0,0 +1,468 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Shared Polpo chat surface — the single source of truth for the chat
5
+ * composition used by BOTH the Playground and the Agent Builder so they
6
+ * are guaranteed identical.
7
+ *
8
+ * Composition (mirrors lumea-agents components/screens/chat.tsx):
9
+ * PolpoProvider (session-cookie fetch, no API key in the browser)
10
+ * → FilesPanel (workspace files, slides in from the right)
11
+ * → ChatSurface = usePolpoChatAdapter + ChatProvider
12
+ * + ToolCallVariantProvider variant="task"
13
+ * + <Chat> with a custom <InputBar> (Skills menu, slash trigger,
14
+ * attachments, ask-user handoff)
15
+ *
16
+ * `<PolpoChat>` is the high-level wrapper: pass `baseUrl` + the selected
17
+ * `agent`, it owns the session lifecycle (?session= persistence, reset on
18
+ * agent switch) and the cookie fetch. Files panel open state is optional
19
+ * (controlled) so a parent toolbar can toggle it.
20
+ */
21
+
22
+ import {
23
+ useCallback,
24
+ useEffect,
25
+ useMemo,
26
+ useRef,
27
+ useState,
28
+ type FormEvent,
29
+ type KeyboardEvent,
30
+ } from "react";
31
+ import { useRouter, useSearchParams } from "next/navigation";
32
+ import { PolpoProvider } from "@polpo-ai/react";
33
+ import {
34
+ usePolpoChatAdapter,
35
+ PolpoChatCapabilitiesMenu,
36
+ usePolpoChatSlashItems,
37
+ } from "@lumea-labs/chat-polpo";
38
+ import {
39
+ ChatAskUser,
40
+ ChatTriggerMenu,
41
+ useChatContext,
42
+ useChatTriggerMenu,
43
+ useSubmitHandler,
44
+ type AskUserQuestion,
45
+ type PromptInputMessage,
46
+ } from "@lumea-labs/chat";
47
+ import { ChatShell } from "#/components/dashboard/chat-shell";
48
+ import {
49
+ FileManagerProvider,
50
+ FileList,
51
+ FileToolbar,
52
+ FilePreviewDialog,
53
+ FilePreviewRouter,
54
+ defaultFmLabels,
55
+ } from "@lumea-labs/file-manager";
56
+ import {
57
+ PolpoFilePreview,
58
+ usePolpoFileAdapter,
59
+ } from "@lumea-labs/file-manager-polpo";
60
+ import { pdfHandler } from "@lumea-labs/file-manager-polpo/handlers/pdf-handler";
61
+ import { imageHandler } from "@lumea-labs/file-manager-polpo/handlers/image-handler";
62
+ import { audioHandler } from "@lumea-labs/file-manager-polpo/handlers/audio-handler";
63
+ import { videoHandler } from "@lumea-labs/file-manager-polpo/handlers/video-handler";
64
+ import { NextIntlClientProvider } from "next-intl";
65
+ import { ArrowUp, Paperclip, Square, X } from "lucide-react";
66
+ import { Sheet, SheetContent, SheetTitle } from "#/components/ui/sheet";
67
+
68
+ /* ------------------------------------------------------------------ */
69
+ /* PolpoChat — high-level wrapper (providers + session lifecycle) */
70
+ /* ------------------------------------------------------------------ */
71
+
72
+ export function PolpoChat({
73
+ baseUrl,
74
+ agent,
75
+ filesOpen,
76
+ onFilesOpenChange,
77
+ }: {
78
+ baseUrl: string;
79
+ agent: string | undefined;
80
+ /** Controlled files-panel open state. Optional — defaults to internal. */
81
+ filesOpen?: boolean;
82
+ onFilesOpenChange?: (open: boolean) => void;
83
+ }) {
84
+ const router = useRouter();
85
+ const searchParams = useSearchParams();
86
+
87
+ // Session lifecycle — capture ?session= at mount, never re-pass as
88
+ // prop, commit new ids to the URL on stream finish so we don't trigger
89
+ // a mid-stream reload. Reset on agent switch.
90
+ const initialSessionIdRef = useRef<string | undefined>(
91
+ searchParams.get("session") ?? undefined,
92
+ );
93
+ const pendingSessionIdRef = useRef<string | null>(null);
94
+
95
+ const commitSession = useRef<(id: string) => void>(() => {});
96
+ commitSession.current = (id: string) => {
97
+ const current = searchParams.get("session");
98
+ if (current === id) return;
99
+ const params = new URLSearchParams(Array.from(searchParams.entries()));
100
+ params.set("session", id);
101
+ router.replace(`?${params.toString()}`, { scroll: false });
102
+ };
103
+
104
+ const handleSessionCreated = useCallback((id: string) => {
105
+ pendingSessionIdRef.current = id;
106
+ }, []);
107
+
108
+ const handleFinish = useCallback(() => {
109
+ const id = pendingSessionIdRef.current;
110
+ if (id) {
111
+ pendingSessionIdRef.current = null;
112
+ commitSession.current(id);
113
+ }
114
+ }, []);
115
+
116
+ // On agent switch (not initial mount) clear the session so a fresh one
117
+ // starts. The `key={agent}` on ChatSurface remounts the tier-2 wiring.
118
+ const firstRender = useRef(true);
119
+ useEffect(() => {
120
+ if (firstRender.current) {
121
+ firstRender.current = false;
122
+ return;
123
+ }
124
+ initialSessionIdRef.current = undefined;
125
+ pendingSessionIdRef.current = null;
126
+ if (searchParams.get("session")) {
127
+ const params = new URLSearchParams(Array.from(searchParams.entries()));
128
+ params.delete("session");
129
+ const qs = params.toString();
130
+ router.replace(qs ? `?${qs}` : "?", { scroll: false });
131
+ }
132
+ // eslint-disable-next-line react-hooks/exhaustive-deps
133
+ }, [agent]);
134
+
135
+ const cookieFetch = useMemo<typeof globalThis.fetch>(
136
+ () =>
137
+ ((input, init) =>
138
+ globalThis.fetch(input, {
139
+ ...init,
140
+ credentials: "include",
141
+ })) as typeof globalThis.fetch,
142
+ [],
143
+ );
144
+
145
+ const [internalFiles, setInternalFiles] = useState(false);
146
+ const isOpen = filesOpen ?? internalFiles;
147
+ const setOpen = onFilesOpenChange ?? setInternalFiles;
148
+
149
+ return (
150
+ <NextIntlClientProvider
151
+ locale="en"
152
+ messages={{}}
153
+ onError={() => {}}
154
+ getMessageFallback={({ key }) => key}
155
+ >
156
+ <PolpoProvider
157
+ baseUrl={baseUrl}
158
+ apiPrefix="/v1"
159
+ fetch={cookieFetch}
160
+ autoConnect={false}
161
+ >
162
+ <FilesPanel open={isOpen} onOpenChange={setOpen} />
163
+ <ChatSurface
164
+ key={agent}
165
+ sessionId={initialSessionIdRef.current}
166
+ agent={agent}
167
+ onSessionCreated={handleSessionCreated}
168
+ onFinish={handleFinish}
169
+ />
170
+ </PolpoProvider>
171
+ </NextIntlClientProvider>
172
+ );
173
+ }
174
+
175
+ /* ------------------------------------------------------------------ */
176
+ /* FilesPanel — workspace files (slides in from the right) */
177
+ /* ------------------------------------------------------------------ */
178
+
179
+ export function FilesPanel({
180
+ open,
181
+ onOpenChange,
182
+ }: {
183
+ open: boolean;
184
+ onOpenChange: (v: boolean) => void;
185
+ }) {
186
+ const adapter = usePolpoFileAdapter();
187
+
188
+ return (
189
+ <FileManagerProvider adapter={adapter} layout="compact" labels={defaultFmLabels}>
190
+ <Sheet open={open} onOpenChange={onOpenChange}>
191
+ <SheetContent
192
+ side="right"
193
+ className="w-full sm:!w-[560px] sm:!max-w-[560px] lg:!w-[640px] lg:!max-w-[640px] p-0 gap-0 flex flex-col bg-card"
194
+ >
195
+ <div className="shrink-0 border-b border-border px-4 py-3">
196
+ <SheetTitle className="text-sm font-semibold tracking-tight text-foreground">
197
+ Files
198
+ </SheetTitle>
199
+ <p className="mt-0.5 text-[11px] text-muted-foreground">
200
+ Project workspace — files written by agents and uploads
201
+ </p>
202
+ </div>
203
+ <div className="shrink-0 border-b border-border px-2 py-1.5">
204
+ <FileToolbar />
205
+ </div>
206
+ <div className="flex-1 min-h-0 overflow-auto">
207
+ <FileList />
208
+ </div>
209
+ </SheetContent>
210
+ </Sheet>
211
+
212
+ <FilePreviewDialog>
213
+ <FilePreviewRouter
214
+ handlers={[pdfHandler, imageHandler, audioHandler, videoHandler]}
215
+ >
216
+ <PolpoFilePreview />
217
+ </FilePreviewRouter>
218
+ </FilePreviewDialog>
219
+ </FileManagerProvider>
220
+ );
221
+ }
222
+
223
+ /* ------------------------------------------------------------------ */
224
+ /* ChatSurface — usePolpoChatAdapter + ChatProvider + Chat */
225
+ /* ------------------------------------------------------------------ */
226
+
227
+ function ChatSurface({
228
+ sessionId,
229
+ agent,
230
+ onSessionCreated,
231
+ onFinish,
232
+ }: {
233
+ sessionId: string | undefined;
234
+ agent: string | undefined;
235
+ onSessionCreated: (id: string) => void;
236
+ onFinish: () => void;
237
+ }) {
238
+ const adapter = usePolpoChatAdapter({
239
+ sessionId,
240
+ agent,
241
+ onSessionCreated,
242
+ onFinish,
243
+ });
244
+ return (
245
+ <ChatShell adapter={adapter}>
246
+ <InputBar agent={agent} />
247
+ </ChatShell>
248
+ );
249
+ }
250
+
251
+ /* ------------------------------------------------------------------ */
252
+ /* InputBar — custom composer (skills, slash, attachments, ask-user) */
253
+ /* ------------------------------------------------------------------ */
254
+
255
+ function InputBar({ agent }: { agent: string | undefined }) {
256
+ const {
257
+ sendMessage,
258
+ isStreaming,
259
+ abort,
260
+ uploadFile,
261
+ isUploading,
262
+ pendingToolCall,
263
+ } = useChatContext();
264
+
265
+ const [text, setText] = useState("");
266
+ const [pendingFiles, setPendingFiles] = useState<File[]>([]);
267
+ const [sendError, setSendError] = useState<string | null>(null);
268
+ const fileInputRef = useRef<HTMLInputElement>(null);
269
+
270
+ const askUserQuestions =
271
+ pendingToolCall?.toolName === "ask_user_question"
272
+ ? ((pendingToolCall.arguments?.questions ?? []) as AskUserQuestion[])
273
+ : null;
274
+
275
+ const submit = useSubmitHandler(sendMessage, uploadFile);
276
+
277
+ // Scope the "/" skills menu to the selected agent's own skills.
278
+ const slash = usePolpoChatSlashItems({ agent, skillsScope: "assigned" });
279
+ const slashMenu = useChatTriggerMenu({
280
+ triggers: slash.triggers,
281
+ onSelect: (item) => {
282
+ void slash.onSelect(item);
283
+ },
284
+ });
285
+
286
+ async function handleSend() {
287
+ const trimmed = text.trim();
288
+ if (!trimmed && pendingFiles.length === 0) return;
289
+
290
+ const message: PromptInputMessage = {
291
+ text: trimmed,
292
+ files: pendingFiles.map((f) => ({
293
+ url: URL.createObjectURL(f),
294
+ filename: f.name,
295
+ })),
296
+ };
297
+
298
+ setText("");
299
+ setPendingFiles([]);
300
+ setSendError(null);
301
+
302
+ try {
303
+ await submit(message);
304
+ } catch (err) {
305
+ const msg = err instanceof Error ? err.message : String(err);
306
+ setSendError(msg || "Send failed");
307
+ // eslint-disable-next-line no-console
308
+ console.error("[polpo-chat] send failed:", err);
309
+ } finally {
310
+ message.files.forEach((f) => URL.revokeObjectURL(f.url));
311
+ }
312
+ }
313
+
314
+ function handleSubmit(e: FormEvent) {
315
+ e.preventDefault();
316
+ if (isStreaming) {
317
+ abort();
318
+ } else {
319
+ void handleSend();
320
+ }
321
+ }
322
+
323
+ function handleKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
324
+ if (e.key === "Enter" && !e.shiftKey) {
325
+ e.preventDefault();
326
+ handleSubmit(e as unknown as FormEvent);
327
+ }
328
+ }
329
+
330
+ function handleFiles(files: FileList | null) {
331
+ if (!files) return;
332
+ setPendingFiles((prev) => [...prev, ...Array.from(files)]);
333
+ }
334
+
335
+ if (askUserQuestions && askUserQuestions.length > 0) {
336
+ return (
337
+ <div className="shrink-0">
338
+ <div className="mx-auto max-w-3xl px-4 py-3">
339
+ <ChatAskUser
340
+ variant="command"
341
+ questions={askUserQuestions}
342
+ onSubmit={(answers) => {
343
+ void sendMessage(JSON.stringify({ answers }));
344
+ }}
345
+ />
346
+ </div>
347
+ </div>
348
+ );
349
+ }
350
+
351
+ return (
352
+ <form onSubmit={handleSubmit} className="shrink-0">
353
+ <div className="mx-auto max-w-3xl px-4 py-3">
354
+ {sendError && (
355
+ <div className="mb-2 flex items-start justify-between gap-2 border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
356
+ <span className="flex-1">
357
+ <span className="font-semibold">Send failed.</span>{" "}
358
+ <span className="opacity-80">{sendError}</span>
359
+ </span>
360
+ <button
361
+ type="button"
362
+ onClick={() => setSendError(null)}
363
+ aria-label="Dismiss"
364
+ className="shrink-0 text-destructive/70 hover:text-destructive"
365
+ >
366
+ <X className="h-3.5 w-3.5" />
367
+ </button>
368
+ </div>
369
+ )}
370
+
371
+ {pendingFiles.length > 0 && (
372
+ <div className="mb-2 flex flex-wrap gap-2">
373
+ {pendingFiles.map((f, i) => (
374
+ <span
375
+ key={`${f.name}-${i}`}
376
+ className="inline-flex items-center gap-1.5 rounded border border-border bg-card px-2 py-1 text-xs font-mono"
377
+ >
378
+ {f.name}
379
+ <button
380
+ type="button"
381
+ onClick={() =>
382
+ setPendingFiles((prev) => prev.filter((_, j) => j !== i))
383
+ }
384
+ aria-label={`Remove ${f.name}`}
385
+ className="text-muted-foreground hover:text-foreground"
386
+ >
387
+ <X className="h-3 w-3" />
388
+ </button>
389
+ </span>
390
+ ))}
391
+ </div>
392
+ )}
393
+
394
+ <div className="relative rounded border border-border bg-card focus-within:border-foreground/40 transition-colors">
395
+ <textarea
396
+ ref={slashMenu.inputRef as React.Ref<HTMLTextAreaElement>}
397
+ value={text}
398
+ onChange={(e) => {
399
+ setText(e.target.value);
400
+ slashMenu.onChange(e);
401
+ }}
402
+ onKeyDown={(e) => {
403
+ slashMenu.onKeyDown(e);
404
+ if (!e.defaultPrevented) handleKeyDown(e);
405
+ }}
406
+ placeholder="Send a message…"
407
+ rows={2}
408
+ className="block w-full resize-none bg-transparent px-3 py-2.5 text-sm placeholder:text-muted-foreground/50 focus:outline-none"
409
+ />
410
+
411
+ <ChatTriggerMenu {...slashMenu.menuProps} />
412
+
413
+ <div className="flex items-center gap-1 border-t border-border px-2 py-1.5">
414
+ <input
415
+ ref={fileInputRef}
416
+ type="file"
417
+ multiple
418
+ hidden
419
+ onChange={(e) => {
420
+ handleFiles(e.target.files);
421
+ e.target.value = "";
422
+ }}
423
+ />
424
+ <button
425
+ type="button"
426
+ onClick={() => fileInputRef.current?.click()}
427
+ disabled={isUploading}
428
+ className="inline-flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-foreground/5 hover:text-foreground disabled:opacity-50"
429
+ aria-label="Attach file"
430
+ >
431
+ <Paperclip className="h-4 w-4" />
432
+ </button>
433
+
434
+ <PolpoChatCapabilitiesMenu
435
+ agent={agent}
436
+ label="Skills"
437
+ variant="icon"
438
+ searchable
439
+ />
440
+
441
+ <span
442
+ aria-hidden
443
+ className="ml-1 hidden items-center gap-1 font-mono text-[10.5px] uppercase tracking-[0.14em] text-muted-foreground sm:inline-flex"
444
+ >
445
+ <kbd className="rounded border border-border bg-card px-1.5 py-px font-mono text-[10px] text-muted-foreground">
446
+ /
447
+ </kbd>
448
+ skills
449
+ </span>
450
+
451
+ <button
452
+ type="submit"
453
+ disabled={!isStreaming && !text.trim() && pendingFiles.length === 0}
454
+ className="ml-auto inline-flex h-7 w-7 items-center justify-center rounded bg-foreground text-background transition-opacity hover:opacity-90 disabled:opacity-40"
455
+ aria-label={isStreaming ? "Stop" : "Send"}
456
+ >
457
+ {isStreaming ? (
458
+ <Square className="h-3.5 w-3.5" />
459
+ ) : (
460
+ <ArrowUp className="h-3.5 w-3.5" />
461
+ )}
462
+ </button>
463
+ </div>
464
+ </div>
465
+ </div>
466
+ </form>
467
+ );
468
+ }