@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.
- package/LICENSE +13 -0
- package/README.md +44 -0
- package/app/(dashboard)/error.tsx +47 -0
- package/app/(dashboard)/layout.tsx +41 -0
- package/app/(dashboard)/loading.tsx +30 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
- package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
- package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
- package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
- package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
- package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
- package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
- package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
- package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
- package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
- package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
- package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
- package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
- package/app/(dashboard)/projects/[id]/page.tsx +68 -0
- package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
- package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
- package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
- package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
- package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
- package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
- package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
- package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
- package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
- package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
- package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
- package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
- package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
- package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
- package/app/(dashboard)/projects/[id]/view.tsx +421 -0
- package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
- package/app/(dashboard)/projects/projects-list.tsx +126 -0
- package/app/(playground)/error.tsx +42 -0
- package/app/(playground)/layout.tsx +27 -0
- package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
- package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
- package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
- package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
- package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +385 -0
- package/app/icon.svg +6 -0
- package/app/layout.tsx +39 -0
- package/app/page.tsx +11 -0
- package/components/ai-elements/code-block.tsx +562 -0
- package/components/dashboard/agent-capabilities.tsx +326 -0
- package/components/dashboard/agent-identity-header.tsx +50 -0
- package/components/dashboard/agent-model-picker.tsx +195 -0
- package/components/dashboard/agent-model-selector.tsx +121 -0
- package/components/dashboard/agent-picker-card.tsx +46 -0
- package/components/dashboard/agent-studio.tsx +242 -0
- package/components/dashboard/agents-table.tsx +48 -0
- package/components/dashboard/breadcrumb.tsx +70 -0
- package/components/dashboard/builder-chat.tsx +418 -0
- package/components/dashboard/chat-shell.tsx +40 -0
- package/components/dashboard/client-picker.tsx +63 -0
- package/components/dashboard/command-snippet.tsx +77 -0
- package/components/dashboard/connect-dialog.tsx +680 -0
- package/components/dashboard/copy-button.tsx +45 -0
- package/components/dashboard/copy-card.tsx +48 -0
- package/components/dashboard/file-browser.tsx +161 -0
- package/components/dashboard/hint.tsx +22 -0
- package/components/dashboard/inference-mode.tsx +15 -0
- package/components/dashboard/manual-refresh-button.tsx +52 -0
- package/components/dashboard/markdown.tsx +31 -0
- package/components/dashboard/mcp-install-panel.tsx +196 -0
- package/components/dashboard/mission-graph.tsx +262 -0
- package/components/dashboard/nav-tabs.tsx +86 -0
- package/components/dashboard/polpo-chat.tsx +468 -0
- package/components/dashboard/project-copilot.tsx +253 -0
- package/components/dashboard/sdk-snippet-panel.tsx +194 -0
- package/components/dashboard/section-header.tsx +37 -0
- package/components/dashboard/settings-form.tsx +2477 -0
- package/components/dashboard/sidebar.tsx +385 -0
- package/components/dashboard/skeletons.tsx +1054 -0
- package/components/dashboard/skills-install-wizard.tsx +273 -0
- package/components/dashboard/swarm-runs.tsx +316 -0
- package/components/dashboard/tab-toggle.tsx +35 -0
- package/components/dashboard/task-studio.tsx +224 -0
- package/components/dashboard/top-header.tsx +203 -0
- package/components/dashboard/webhook-deliveries.tsx +234 -0
- package/components/icons/coding-agents.tsx +151 -0
- package/components/json-ld.tsx +8 -0
- package/components/providers.tsx +25 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/chart.tsx +356 -0
- package/components/ui/command.tsx +196 -0
- package/components/ui/dialog.tsx +157 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/multi-select.tsx +161 -0
- package/components/ui/popover.tsx +90 -0
- package/components/ui/select.tsx +201 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +135 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/tabs.tsx +56 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/hooks/use-desktop-sidebar.tsx +50 -0
- package/hooks/use-mobile-sidebar.tsx +37 -0
- package/index.ts +14 -0
- package/lib/api.ts +194 -0
- package/lib/builder-context.ts +60 -0
- package/lib/data-client.ts +68 -0
- package/lib/get-query-client.ts +25 -0
- package/lib/polpo-client.tsx +49 -0
- package/lib/tool-catalog.ts +234 -0
- package/lib/use-event-catalog.ts +28 -0
- package/lib/utils.ts +6 -0
- 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
|
+
}
|