@polpo-ai/chat 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/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # @polpo-ai/chat
2
+
3
+ Composable chat UI components for [Polpo](https://polpo.sh) AI agents. Built on top of `@polpo-ai/sdk` and `@polpo-ai/react`.
4
+
5
+ Three levels of composition — from zero-config to full control.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @polpo-ai/chat
11
+ ```
12
+
13
+ Peer dependencies:
14
+
15
+ ```bash
16
+ npm install @polpo-ai/sdk @polpo-ai/react react react-virtuoso lucide-react
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### Level 1 — Zero Config
22
+
23
+ ```tsx
24
+ import { Chat } from "@polpo-ai/chat";
25
+
26
+ function ChatPage() {
27
+ return <Chat sessionId="session_abc" agent="coder" />;
28
+ }
29
+ ```
30
+
31
+ That's it. Messages, streaming, scroll-to-bottom, tool calls, typing dots, skeleton loading — all included.
32
+
33
+ ### Level 2 — Compose
34
+
35
+ ```tsx
36
+ import { Chat, useChatContext } from "@polpo-ai/chat";
37
+
38
+ function ChatInput() {
39
+ const { sendMessage, isStreaming, abort } = useChatContext();
40
+
41
+ return (
42
+ <form onSubmit={(e) => { e.preventDefault(); sendMessage(input); }}>
43
+ <textarea />
44
+ {isStreaming ? <button onClick={abort}>Stop</button> : <button type="submit">Send</button>}
45
+ </form>
46
+ );
47
+ }
48
+
49
+ function ChatPage() {
50
+ return (
51
+ <Chat sessionId="session_abc" agent="coder" avatar={<Avatar />} agentName="Coder">
52
+ <ChatInput />
53
+ </Chat>
54
+ );
55
+ }
56
+ ```
57
+
58
+ Children render below the message list. Use `useChatContext()` to access chat state from any child.
59
+
60
+ ### Level 3 — Primitives
61
+
62
+ ```tsx
63
+ import { useChat } from "@polpo-ai/react";
64
+ import { ChatMessage, ToolCallChip, ChatSkeleton } from "@polpo-ai/chat";
65
+
66
+ function MyChat() {
67
+ const { messages, sendMessage, isStreaming } = useChat({ agent: "coder" });
68
+
69
+ return (
70
+ <div>
71
+ {messages.map((msg, i) => (
72
+ <ChatMessage key={msg.id} msg={msg} isLast={i === messages.length - 1} isStreaming={isStreaming} />
73
+ ))}
74
+ </div>
75
+ );
76
+ }
77
+ ```
78
+
79
+ Use individual components and hooks to build completely custom layouts.
80
+
81
+ ## Components
82
+
83
+ ### `<Chat>`
84
+
85
+ Root compound component. Wraps `ChatProvider` + `ChatMessages` + `ChatScrollButton`.
86
+
87
+ ```tsx
88
+ <Chat
89
+ sessionId="session_abc" // Existing session ID (omit for new chats)
90
+ agent="coder" // Agent name
91
+ onSessionCreated={(id) => {}} // Called when server creates a new session
92
+ avatar={<Avatar />} // ReactNode for assistant messages
93
+ agentName="Coder" // Display name for assistant
94
+ streamdownComponents={...} // Custom code block renderer
95
+ skeletonCount={3} // Loading skeleton count
96
+ className="flex-1" // Outer container class
97
+ >
98
+ {children} {/* Rendered below messages (e.g. input bar) */}
99
+ </Chat>
100
+ ```
101
+
102
+ ### `<ChatMessage>`
103
+
104
+ Renders a single message. Dispatches to `ChatUserMessage` or `ChatAssistantMessage` based on role.
105
+
106
+ ```tsx
107
+ <ChatMessage
108
+ msg={message} // ChatMessageItemData
109
+ isLast={true} // Is this the last message?
110
+ isStreaming={false} // Is the chat currently streaming?
111
+ avatar={<Avatar />} // Assistant avatar (ReactNode)
112
+ agentName="Coder" // Assistant display name
113
+ streamdownComponents={...} // Markdown renderer override
114
+ />
115
+ ```
116
+
117
+ ### `<ChatMessages>`
118
+
119
+ Virtuoso-powered scrollable message list with auto-scroll, scroll-to-bottom button, and skeleton loading.
120
+
121
+ ```tsx
122
+ <ChatMessages
123
+ renderItem={(msg, index, isLast, isStreaming) => <ChatMessage msg={msg} ... />}
124
+ skeletonCount={3}
125
+ className="flex-1"
126
+ />
127
+ ```
128
+
129
+ Uses `useChatContext()` internally. Must be inside a `<ChatProvider>`.
130
+
131
+ ### `<ChatScrollButton>`
132
+
133
+ Scroll-to-bottom button with new message indicator.
134
+
135
+ ```tsx
136
+ <ChatScrollButton isAtBottom={false} showNewMessage={true} onClick={scrollToBottom} />
137
+ ```
138
+
139
+ ### `<ChatSkeleton>`
140
+
141
+ Loading skeleton matching the message layout.
142
+
143
+ ```tsx
144
+ <ChatSkeleton count={3} />
145
+ ```
146
+
147
+ ### `<ChatTyping>`
148
+
149
+ Animated typing dots.
150
+
151
+ ```tsx
152
+ <ChatTyping className="text-gray-400" />
153
+ ```
154
+
155
+ ## Tool Calls
156
+
157
+ Built-in renderers for common Polpo tools:
158
+
159
+ | Tool | Renderer | What it shows |
160
+ |------|----------|--------------|
161
+ | `read` | `ToolRead` | File path + content with line numbers |
162
+ | `write` / `edit` | `ToolWrite` | File path + content preview in green |
163
+ | `bash` | `ToolBash` | Command with `$` prompt + dark terminal output |
164
+ | `grep` / `glob` | `ToolSearch` | Pattern + matched results list |
165
+ | `http_fetch` / `search_web` | `ToolHttp` | URL + response preview |
166
+ | `email_send` | `ToolEmail` | To, subject, body preview |
167
+ | `ask_user_question` | `ToolAskUser` | Questions with answered state |
168
+ | (any other) | `ToolCallShell` | Generic with expand/collapse |
169
+
170
+ ### Custom Tool Renderers
171
+
172
+ ```tsx
173
+ import { ToolCallShell } from "@polpo-ai/chat/tools";
174
+ import { Database } from "lucide-react";
175
+
176
+ function ToolDatabaseQuery({ tool }) {
177
+ const query = tool.arguments?.query;
178
+ return (
179
+ <ToolCallShell tool={tool} icon={Database} label="Query" summary={query}>
180
+ <pre>{tool.result}</pre>
181
+ </ToolCallShell>
182
+ );
183
+ }
184
+ ```
185
+
186
+ ## Hooks
187
+
188
+ ### `useSubmitHandler(sendMessage, uploadFile)`
189
+
190
+ Handles file uploads and sends messages with `ContentPart[]`.
191
+
192
+ ```tsx
193
+ import { useSubmitHandler } from "@polpo-ai/chat/hooks";
194
+
195
+ const handleSubmit = useSubmitHandler(sendMessage, uploadFile);
196
+ // handleSubmit({ text: "Analyze this", files: [...] })
197
+ ```
198
+
199
+ ### `useDocumentDrag()`
200
+
201
+ Tracks document-level drag state for drop overlay feedback.
202
+
203
+ ```tsx
204
+ import { useDocumentDrag } from "@polpo-ai/chat/hooks";
205
+
206
+ const dragging = useDocumentDrag();
207
+ // dragging: boolean — true when files are being dragged over the page
208
+ ```
209
+
210
+ ## Utilities
211
+
212
+ ### `getTextContent(content)`
213
+
214
+ Extracts text from `string | ContentPart[]`.
215
+
216
+ ### `relativeTime(isoString)`
217
+
218
+ Formats timestamps as "Just now", "2m ago", "An hour ago", or full date.
219
+
220
+ ## Styling
221
+
222
+ The package uses Tailwind utility classes. Colors reference CSS custom properties:
223
+
224
+ ```css
225
+ :root {
226
+ --bg: #FAFAF7;
227
+ --surface: #FFFFFF;
228
+ --ink: #0F0F0F;
229
+ --ink-2: #555;
230
+ --ink-3: #999;
231
+ --p-accent: #E2733D;
232
+ --accent-light: #FDF0E8;
233
+ --green: #1A7F52;
234
+ --green-light: #E8F5EE;
235
+ --warm: #F3F0EB;
236
+ --line: #E5E1DA;
237
+ }
238
+ ```
239
+
240
+ Override these in your `globals.css` to match your brand. No CSS is shipped — everything is Tailwind inline.
241
+
242
+ ## Keyframe Animations
243
+
244
+ Add these to your Tailwind config or `globals.css`:
245
+
246
+ ```css
247
+ @keyframes typing-dot {
248
+ 0%, 60%, 100% { opacity: .3; transform: translateY(0); }
249
+ 30% { opacity: 1; transform: translateY(-3px); }
250
+ }
251
+ ```
252
+
253
+ ## License
254
+
255
+ MIT
@@ -0,0 +1,230 @@
1
+ // src/tools/index.tsx
2
+ import { Wrench } from "lucide-react";
3
+
4
+ // src/tools/tool-call-shell.tsx
5
+ import { useState } from "react";
6
+ import { ChevronRight, Loader2, Check, AlertCircle } from "lucide-react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ function ToolCallShell({ tool, icon: Icon, label, summary, children }) {
9
+ const [expanded, setExpanded] = useState(false);
10
+ const isPending = tool.state === "calling" || tool.state === "preparing";
11
+ const isError = tool.state === "error";
12
+ const isDone = tool.state === "completed";
13
+ const hasContent = isDone && (children || tool.result);
14
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col rounded-lg bg-p-warm border border-p-line text-[13px] text-p-ink-2 overflow-hidden ${isError ? "border-destructive/20 bg-destructive/5" : ""}`, children: [
15
+ /* @__PURE__ */ jsxs(
16
+ "button",
17
+ {
18
+ className: `flex items-center gap-2 px-3 py-2 border-none bg-transparent font-inherit text-inherit text-left w-full ${hasContent ? "cursor-pointer" : "cursor-default"}`,
19
+ onClick: () => hasContent && setExpanded(!expanded),
20
+ children: [
21
+ /* @__PURE__ */ jsx(Icon, { size: 14, className: "shrink-0" }),
22
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-p-ink whitespace-nowrap", children: label }),
23
+ summary ? /* @__PURE__ */ jsx("span", { className: "text-p-ink-3 text-xs overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]", children: summary }) : null,
24
+ isPending ? /* @__PURE__ */ jsx(Loader2, { size: 14, className: "animate-spin text-p-accent shrink-0" }) : null,
25
+ isDone ? /* @__PURE__ */ jsx(Check, { size: 14, className: "text-p-green shrink-0" }) : null,
26
+ isError ? /* @__PURE__ */ jsx(AlertCircle, { size: 14, className: "text-destructive shrink-0" }) : null,
27
+ hasContent ? /* @__PURE__ */ jsx(ChevronRight, { size: 12, className: `ml-auto text-p-ink-3 shrink-0 transition-transform duration-150 ${expanded ? "rotate-90" : ""}` }) : null
28
+ ]
29
+ }
30
+ ),
31
+ expanded ? /* @__PURE__ */ jsx("div", { className: "border-t border-p-line", children: children || /* @__PURE__ */ jsx("pre", { className: "m-0 px-2.5 py-2 text-[11px] leading-normal font-mono text-p-ink-2 bg-p-bg whitespace-pre-wrap break-all max-h-[180px] overflow-y-auto", children: tool.result }) }) : null
32
+ ] });
33
+ }
34
+
35
+ // src/tools/tool-read.tsx
36
+ import { FileText } from "lucide-react";
37
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
38
+ function ToolRead({ tool }) {
39
+ const path = tool.arguments?.path || tool.arguments?.file_path;
40
+ const lines = tool.result?.split("\n") || [];
41
+ const maxLines = 12;
42
+ const truncated = lines.length > maxLines;
43
+ return /* @__PURE__ */ jsx2(ToolCallShell, { tool, icon: FileText, label: "Read", summary: path, children: tool.result && /* @__PURE__ */ jsxs2("div", { className: "bg-p-bg max-h-[220px] overflow-y-auto", children: [
44
+ /* @__PURE__ */ jsx2("table", { className: "w-full text-[11px] leading-relaxed font-mono border-collapse", children: /* @__PURE__ */ jsx2("tbody", { children: lines.slice(0, maxLines).map((line, i) => /* @__PURE__ */ jsxs2("tr", { className: "hover:bg-p-warm/50", children: [
45
+ /* @__PURE__ */ jsx2("td", { className: "text-right text-p-ink-3 select-none px-2.5 py-0 w-[1%] whitespace-nowrap", children: i + 1 }),
46
+ /* @__PURE__ */ jsx2("td", { className: "text-p-ink-2 px-2.5 py-0 whitespace-pre-wrap break-all", children: line })
47
+ ] }, i)) }) }),
48
+ truncated && /* @__PURE__ */ jsxs2("div", { className: "px-2.5 py-1 text-[10px] text-p-ink-3 border-t border-p-line", children: [
49
+ "+",
50
+ lines.length - maxLines,
51
+ " more lines"
52
+ ] })
53
+ ] }) });
54
+ }
55
+
56
+ // src/tools/tool-write.tsx
57
+ import { Pen } from "lucide-react";
58
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
59
+ function ToolWrite({ tool }) {
60
+ const path = tool.arguments?.path || tool.arguments?.file_path;
61
+ const content = tool.arguments?.content || tool.arguments?.new_string;
62
+ const preview = content?.slice(0, 200);
63
+ return /* @__PURE__ */ jsx3(ToolCallShell, { tool, icon: Pen, label: tool.name === "edit" ? "Edit" : "Write", summary: path, children: preview && /* @__PURE__ */ jsx3("div", { className: "bg-p-bg max-h-[180px] overflow-y-auto", children: /* @__PURE__ */ jsxs3("pre", { className: "m-0 px-2.5 py-2 text-[11px] leading-normal font-mono text-p-green whitespace-pre-wrap break-all", children: [
64
+ preview,
65
+ content && content.length > 200 ? "\n\u2026" : ""
66
+ ] }) }) });
67
+ }
68
+
69
+ // src/tools/tool-bash.tsx
70
+ import { Terminal } from "lucide-react";
71
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
72
+ function ToolBash({ tool }) {
73
+ const command = tool.arguments?.command;
74
+ const lines = tool.result?.split("\n") || [];
75
+ const maxLines = 10;
76
+ const truncated = lines.length > maxLines;
77
+ return /* @__PURE__ */ jsx4(ToolCallShell, { tool, icon: Terminal, label: "Bash", summary: command?.slice(0, 60), children: /* @__PURE__ */ jsxs4("div", { className: "bg-[#1a1a1a] max-h-[220px] overflow-y-auto", children: [
78
+ command && /* @__PURE__ */ jsxs4("div", { className: "px-3 py-1.5 text-[11px] font-mono text-emerald-400 border-b border-white/10", children: [
79
+ /* @__PURE__ */ jsx4("span", { className: "text-p-ink-3 select-none", children: "$ " }),
80
+ command
81
+ ] }),
82
+ tool.result && /* @__PURE__ */ jsxs4("pre", { className: "m-0 px-3 py-1.5 text-[11px] leading-normal font-mono text-neutral-300 whitespace-pre-wrap break-all", children: [
83
+ lines.slice(0, maxLines).join("\n"),
84
+ truncated ? `
85
+ \u2026 +${lines.length - maxLines} lines` : ""
86
+ ] })
87
+ ] }) });
88
+ }
89
+
90
+ // src/tools/tool-search.tsx
91
+ import { Search, FolderSearch } from "lucide-react";
92
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
93
+ function ToolSearch({ tool }) {
94
+ const isGlob = tool.name === "glob";
95
+ const pattern = tool.arguments?.pattern || tool.arguments?.query || tool.arguments?.q;
96
+ const path = tool.arguments?.path || tool.arguments?.root;
97
+ const summary = pattern ? `${pattern}${path ? ` in ${path}` : ""}` : path;
98
+ const lines = tool.result?.split("\n").filter(Boolean) || [];
99
+ const maxItems = 8;
100
+ const truncated = lines.length > maxItems;
101
+ return /* @__PURE__ */ jsx5(ToolCallShell, { tool, icon: isGlob ? FolderSearch : Search, label: isGlob ? "Glob" : tool.name === "grep" ? "Grep" : "Search", summary, children: lines.length > 0 && /* @__PURE__ */ jsxs5("div", { className: "bg-p-bg max-h-[200px] overflow-y-auto", children: [
102
+ /* @__PURE__ */ jsx5("ul", { className: "list-none m-0 p-0", children: lines.slice(0, maxItems).map((line, i) => /* @__PURE__ */ jsx5("li", { className: "px-2.5 py-0.5 text-xs font-mono text-p-ink-2 border-b border-p-line/50 last:border-b-0 hover:bg-p-warm/50 truncate", children: line }, i)) }),
103
+ truncated && /* @__PURE__ */ jsxs5("div", { className: "px-2.5 py-1 text-[10px] text-p-ink-3 border-t border-p-line", children: [
104
+ "+",
105
+ lines.length - maxItems,
106
+ " more results"
107
+ ] })
108
+ ] }) });
109
+ }
110
+
111
+ // src/tools/tool-http.tsx
112
+ import { Globe } from "lucide-react";
113
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
114
+ function ToolHttp({ tool }) {
115
+ const url = tool.arguments?.url;
116
+ const method = tool.arguments?.method?.toUpperCase() || "GET";
117
+ const summary = url ? `${method} ${url}` : null;
118
+ return /* @__PURE__ */ jsx6(ToolCallShell, { tool, icon: Globe, label: tool.name === "search_web" ? "Search Web" : "HTTP", summary, children: tool.result && /* @__PURE__ */ jsxs6("pre", { className: "m-0 px-2.5 py-2 text-[11px] leading-normal font-mono text-p-ink-2 bg-p-bg whitespace-pre-wrap break-all max-h-[180px] overflow-y-auto", children: [
119
+ tool.result.slice(0, 500),
120
+ tool.result.length > 500 ? "\n\u2026" : ""
121
+ ] }) });
122
+ }
123
+
124
+ // src/tools/tool-email.tsx
125
+ import { Mail } from "lucide-react";
126
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
127
+ function ToolEmail({ tool }) {
128
+ const to = tool.arguments?.to || tool.arguments?.recipient;
129
+ const subject = tool.arguments?.subject;
130
+ const body = tool.arguments?.body || tool.arguments?.content;
131
+ const summary = to ? `\u2192 ${to}` : null;
132
+ return /* @__PURE__ */ jsx7(ToolCallShell, { tool, icon: Mail, label: "Email", summary, children: /* @__PURE__ */ jsxs7("div", { className: "bg-p-bg px-2.5 py-2 text-xs max-h-[180px] overflow-y-auto", children: [
133
+ subject && /* @__PURE__ */ jsxs7("div", { className: "mb-1", children: [
134
+ /* @__PURE__ */ jsx7("span", { className: "text-p-ink-3", children: "Subject: " }),
135
+ /* @__PURE__ */ jsx7("span", { className: "text-p-ink font-medium", children: subject })
136
+ ] }),
137
+ to && /* @__PURE__ */ jsxs7("div", { className: "mb-1.5", children: [
138
+ /* @__PURE__ */ jsx7("span", { className: "text-p-ink-3", children: "To: " }),
139
+ /* @__PURE__ */ jsx7("span", { className: "text-p-ink-2", children: to })
140
+ ] }),
141
+ body && /* @__PURE__ */ jsxs7("p", { className: "m-0 text-p-ink-2 leading-relaxed whitespace-pre-wrap", children: [
142
+ body.slice(0, 300),
143
+ body.length > 300 ? "\u2026" : ""
144
+ ] })
145
+ ] }) });
146
+ }
147
+
148
+ // src/tools/tool-ask-user.tsx
149
+ import { MessageSquareMore, Check as Check2 } from "lucide-react";
150
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
151
+ function ToolAskUser({ tool }) {
152
+ const questions = tool.arguments?.questions || [];
153
+ const isAnswered = tool.state === "completed" || tool.state === "interrupted";
154
+ let answers = [];
155
+ if (tool.result) {
156
+ try {
157
+ const parsed = JSON.parse(tool.result);
158
+ answers = parsed.answers || [];
159
+ } catch {
160
+ }
161
+ }
162
+ return /* @__PURE__ */ jsx8(
163
+ ToolCallShell,
164
+ {
165
+ tool,
166
+ icon: MessageSquareMore,
167
+ label: "Question",
168
+ summary: isAnswered ? `${questions.length} answered` : `${questions.length} question${questions.length > 1 ? "s" : ""}`,
169
+ children: /* @__PURE__ */ jsx8("div", { className: "bg-p-bg px-3 py-2 text-xs space-y-2", children: questions.map((q) => {
170
+ const answer = answers.find((a) => a.questionId === q.id);
171
+ return /* @__PURE__ */ jsxs8("div", { className: "flex items-start gap-2", children: [
172
+ isAnswered ? /* @__PURE__ */ jsx8(Check2, { size: 12, className: "text-p-green shrink-0 mt-0.5" }) : /* @__PURE__ */ jsx8(MessageSquareMore, { size: 12, className: "text-p-accent shrink-0 mt-0.5" }),
173
+ /* @__PURE__ */ jsxs8("div", { className: "min-w-0", children: [
174
+ /* @__PURE__ */ jsx8("p", { className: "text-p-ink font-medium", children: q.question }),
175
+ answer && answer.selected.length > 0 && /* @__PURE__ */ jsx8("p", { className: "text-p-ink-3 mt-0.5", children: answer.selected.join(", ") }),
176
+ !answer && isAnswered && /* @__PURE__ */ jsx8("p", { className: "text-p-ink-3/50 italic mt-0.5", children: "Skipped" })
177
+ ] })
178
+ ] }, q.id);
179
+ }) })
180
+ }
181
+ );
182
+ }
183
+
184
+ // src/tools/index.tsx
185
+ import { jsx as jsx9 } from "react/jsx-runtime";
186
+ var TOOL_COMPONENTS = {
187
+ ask_user_question: ToolAskUser,
188
+ read: ToolRead,
189
+ read_attachment: ToolRead,
190
+ write: ToolWrite,
191
+ edit: ToolWrite,
192
+ bash: ToolBash,
193
+ grep: ToolSearch,
194
+ glob: ToolSearch,
195
+ search_web: ToolHttp,
196
+ http_fetch: ToolHttp,
197
+ http_download: ToolHttp,
198
+ email_send: ToolEmail
199
+ };
200
+ var TOOL_PREFIX_COMPONENTS = [
201
+ { prefix: "browser_", component: ToolHttp },
202
+ { prefix: "email_", component: ToolEmail },
203
+ { prefix: "search_", component: ToolSearch }
204
+ ];
205
+ function getToolLabel(name) {
206
+ return name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
207
+ }
208
+ function ToolCallChip({ tool }) {
209
+ const Exact = TOOL_COMPONENTS[tool.name];
210
+ if (Exact) return /* @__PURE__ */ jsx9(Exact, { tool });
211
+ for (const { prefix, component: Prefixed } of TOOL_PREFIX_COMPONENTS) {
212
+ if (tool.name.startsWith(prefix)) return /* @__PURE__ */ jsx9(Prefixed, { tool });
213
+ }
214
+ const summary = tool.arguments ? Object.values(tool.arguments).find((v) => typeof v === "string" && v.length > 0) : void 0;
215
+ return /* @__PURE__ */ jsx9(
216
+ ToolCallShell,
217
+ {
218
+ tool,
219
+ icon: Wrench,
220
+ label: getToolLabel(tool.name),
221
+ summary: summary?.slice(0, 80)
222
+ }
223
+ );
224
+ }
225
+
226
+ export {
227
+ ToolCallShell,
228
+ ToolCallChip
229
+ };
230
+ //# sourceMappingURL=chunk-CCSIMOXD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tools/index.tsx","../src/tools/tool-call-shell.tsx","../src/tools/tool-read.tsx","../src/tools/tool-write.tsx","../src/tools/tool-bash.tsx","../src/tools/tool-search.tsx","../src/tools/tool-http.tsx","../src/tools/tool-email.tsx","../src/tools/tool-ask-user.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Wrench } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\nimport { ToolRead } from \"./tool-read\";\nimport { ToolWrite } from \"./tool-write\";\nimport { ToolBash } from \"./tool-bash\";\nimport { ToolSearch } from \"./tool-search\";\nimport { ToolHttp } from \"./tool-http\";\nimport { ToolEmail } from \"./tool-email\";\nimport { ToolAskUser } from \"./tool-ask-user\";\n\n// ── Tool name → component map ──\n\nconst TOOL_COMPONENTS: Record<string, React.ComponentType<{ tool: ToolCallEvent }>> = {\n ask_user_question: ToolAskUser,\n read: ToolRead,\n read_attachment: ToolRead,\n write: ToolWrite,\n edit: ToolWrite,\n bash: ToolBash,\n grep: ToolSearch,\n glob: ToolSearch,\n search_web: ToolHttp,\n http_fetch: ToolHttp,\n http_download: ToolHttp,\n email_send: ToolEmail,\n};\n\n// Prefix-based matching for tools like browser_*, memory_*, etc.\nconst TOOL_PREFIX_COMPONENTS: { prefix: string; component: React.ComponentType<{ tool: ToolCallEvent }> }[] = [\n { prefix: \"browser_\", component: ToolHttp },\n { prefix: \"email_\", component: ToolEmail },\n { prefix: \"search_\", component: ToolSearch },\n];\n\nfunction getToolLabel(name: string) {\n return name.replace(/_/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n// ── Dispatcher ──\n\nexport function ToolCallChip({ tool }: { tool: ToolCallEvent }) {\n // Exact match\n const Exact = TOOL_COMPONENTS[tool.name];\n if (Exact) return <Exact tool={tool} />;\n\n // Prefix match\n for (const { prefix, component: Prefixed } of TOOL_PREFIX_COMPONENTS) {\n if (tool.name.startsWith(prefix)) return <Prefixed tool={tool} />;\n }\n\n // Generic fallback\n const summary = tool.arguments\n ? Object.values(tool.arguments).find((v) => typeof v === \"string\" && v.length > 0) as string | undefined\n : undefined;\n\n return (\n <ToolCallShell\n tool={tool}\n icon={Wrench}\n label={getToolLabel(tool.name)}\n summary={summary?.slice(0, 80)}\n />\n );\n}\n\n// Re-export shell for custom usage\nexport { ToolCallShell } from \"./tool-call-shell\";\n","\"use client\";\n\nimport { useState, type ReactNode } from \"react\";\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { ChevronRight, Loader2, Check, AlertCircle, type LucideIcon } from \"lucide-react\";\n\n// ── Shell props ──\n\nexport interface ToolCallShellProps {\n tool: ToolCallEvent;\n icon: LucideIcon;\n label: string;\n /** One-line summary shown next to the label */\n summary?: string | null;\n /** Custom expanded content — replaces default raw result */\n children?: ReactNode;\n}\n\n// ── Shell ──\n\nexport function ToolCallShell({ tool, icon: Icon, label, summary, children }: ToolCallShellProps) {\n const [expanded, setExpanded] = useState(false);\n const isPending = tool.state === \"calling\" || tool.state === \"preparing\";\n const isError = tool.state === \"error\";\n const isDone = tool.state === \"completed\";\n const hasContent = isDone && (children || tool.result);\n\n return (\n <div className={`flex flex-col rounded-lg bg-p-warm border border-p-line text-[13px] text-p-ink-2 overflow-hidden ${isError ? \"border-destructive/20 bg-destructive/5\" : \"\"}`}>\n <button\n className={`flex items-center gap-2 px-3 py-2 border-none bg-transparent font-inherit text-inherit text-left w-full ${hasContent ? \"cursor-pointer\" : \"cursor-default\"}`}\n onClick={() => hasContent && setExpanded(!expanded)}\n >\n <Icon size={14} className=\"shrink-0\" />\n <span className=\"font-medium text-p-ink whitespace-nowrap\">{label}</span>\n {summary ? <span className=\"text-p-ink-3 text-xs overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]\">{summary}</span> : null}\n {isPending ? <Loader2 size={14} className=\"animate-spin text-p-accent shrink-0\" /> : null}\n {isDone ? <Check size={14} className=\"text-p-green shrink-0\" /> : null}\n {isError ? <AlertCircle size={14} className=\"text-destructive shrink-0\" /> : null}\n {hasContent ? (\n <ChevronRight size={12} className={`ml-auto text-p-ink-3 shrink-0 transition-transform duration-150 ${expanded ? \"rotate-90\" : \"\"}`} />\n ) : null}\n </button>\n {expanded ? (\n <div className=\"border-t border-p-line\">\n {children || (\n <pre className=\"m-0 px-2.5 py-2 text-[11px] leading-normal font-mono text-p-ink-2 bg-p-bg whitespace-pre-wrap break-all max-h-[180px] overflow-y-auto\">\n {tool.result}\n </pre>\n )}\n </div>\n ) : null}\n </div>\n );\n}\n","\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { FileText } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** Read tool — shows file path and truncated content with line numbers */\nexport function ToolRead({ tool }: { tool: ToolCallEvent }) {\n const path = (tool.arguments?.path || tool.arguments?.file_path) as string | undefined;\n const lines = tool.result?.split(\"\\n\") || [];\n const maxLines = 12;\n const truncated = lines.length > maxLines;\n\n return (\n <ToolCallShell tool={tool} icon={FileText} label=\"Read\" summary={path}>\n {tool.result && (\n <div className=\"bg-p-bg max-h-[220px] overflow-y-auto\">\n <table className=\"w-full text-[11px] leading-relaxed font-mono border-collapse\">\n <tbody>\n {lines.slice(0, maxLines).map((line, i) => (\n <tr key={i} className=\"hover:bg-p-warm/50\">\n <td className=\"text-right text-p-ink-3 select-none px-2.5 py-0 w-[1%] whitespace-nowrap\">{i + 1}</td>\n <td className=\"text-p-ink-2 px-2.5 py-0 whitespace-pre-wrap break-all\">{line}</td>\n </tr>\n ))}\n </tbody>\n </table>\n {truncated && (\n <div className=\"px-2.5 py-1 text-[10px] text-p-ink-3 border-t border-p-line\">\n +{lines.length - maxLines} more lines\n </div>\n )}\n </div>\n )}\n </ToolCallShell>\n );\n}\n","\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Pen } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** Write/Edit tool — shows file path and a diff-like preview */\nexport function ToolWrite({ tool }: { tool: ToolCallEvent }) {\n const path = (tool.arguments?.path || tool.arguments?.file_path) as string | undefined;\n const content = (tool.arguments?.content || tool.arguments?.new_string) as string | undefined;\n const preview = content?.slice(0, 200);\n\n return (\n <ToolCallShell tool={tool} icon={Pen} label={tool.name === \"edit\" ? \"Edit\" : \"Write\"} summary={path}>\n {preview && (\n <div className=\"bg-p-bg max-h-[180px] overflow-y-auto\">\n <pre className=\"m-0 px-2.5 py-2 text-[11px] leading-normal font-mono text-p-green whitespace-pre-wrap break-all\">\n {preview}{content && content.length > 200 ? \"\\n…\" : \"\"}\n </pre>\n </div>\n )}\n </ToolCallShell>\n );\n}\n","\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Terminal } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** Bash tool — shows command and output */\nexport function ToolBash({ tool }: { tool: ToolCallEvent }) {\n const command = (tool.arguments?.command) as string | undefined;\n const lines = tool.result?.split(\"\\n\") || [];\n const maxLines = 10;\n const truncated = lines.length > maxLines;\n\n return (\n <ToolCallShell tool={tool} icon={Terminal} label=\"Bash\" summary={command?.slice(0, 60)}>\n <div className=\"bg-[#1a1a1a] max-h-[220px] overflow-y-auto\">\n {command && (\n <div className=\"px-3 py-1.5 text-[11px] font-mono text-emerald-400 border-b border-white/10\">\n <span className=\"text-p-ink-3 select-none\">$ </span>{command}\n </div>\n )}\n {tool.result && (\n <pre className=\"m-0 px-3 py-1.5 text-[11px] leading-normal font-mono text-neutral-300 whitespace-pre-wrap break-all\">\n {lines.slice(0, maxLines).join(\"\\n\")}\n {truncated ? `\\n… +${lines.length - maxLines} lines` : \"\"}\n </pre>\n )}\n </div>\n </ToolCallShell>\n );\n}\n","\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Search, FolderSearch } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** Grep/Glob/Search tool — shows query/pattern and matched results */\nexport function ToolSearch({ tool }: { tool: ToolCallEvent }) {\n const isGlob = tool.name === \"glob\";\n const pattern = (tool.arguments?.pattern || tool.arguments?.query || tool.arguments?.q) as string | undefined;\n const path = (tool.arguments?.path || tool.arguments?.root) as string | undefined;\n const summary = pattern ? `${pattern}${path ? ` in ${path}` : \"\"}` : path;\n\n const lines = tool.result?.split(\"\\n\").filter(Boolean) || [];\n const maxItems = 8;\n const truncated = lines.length > maxItems;\n\n return (\n <ToolCallShell tool={tool} icon={isGlob ? FolderSearch : Search} label={isGlob ? \"Glob\" : tool.name === \"grep\" ? \"Grep\" : \"Search\"} summary={summary}>\n {lines.length > 0 && (\n <div className=\"bg-p-bg max-h-[200px] overflow-y-auto\">\n <ul className=\"list-none m-0 p-0\">\n {lines.slice(0, maxItems).map((line, i) => (\n <li key={i} className=\"px-2.5 py-0.5 text-xs font-mono text-p-ink-2 border-b border-p-line/50 last:border-b-0 hover:bg-p-warm/50 truncate\">\n {line}\n </li>\n ))}\n </ul>\n {truncated && (\n <div className=\"px-2.5 py-1 text-[10px] text-p-ink-3 border-t border-p-line\">\n +{lines.length - maxItems} more results\n </div>\n )}\n </div>\n )}\n </ToolCallShell>\n );\n}\n","\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Globe } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** HTTP fetch/download/search_web tool — shows URL and response preview */\nexport function ToolHttp({ tool }: { tool: ToolCallEvent }) {\n const url = (tool.arguments?.url) as string | undefined;\n const method = (tool.arguments?.method as string)?.toUpperCase() || \"GET\";\n const summary = url ? `${method} ${url}` : null;\n\n return (\n <ToolCallShell tool={tool} icon={Globe} label={tool.name === \"search_web\" ? \"Search Web\" : \"HTTP\"} summary={summary}>\n {tool.result && (\n <pre className=\"m-0 px-2.5 py-2 text-[11px] leading-normal font-mono text-p-ink-2 bg-p-bg whitespace-pre-wrap break-all max-h-[180px] overflow-y-auto\">\n {tool.result.slice(0, 500)}{tool.result.length > 500 ? \"\\n…\" : \"\"}\n </pre>\n )}\n </ToolCallShell>\n );\n}\n","\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { Mail } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\n/** Email send tool — shows recipient, subject, and body preview */\nexport function ToolEmail({ tool }: { tool: ToolCallEvent }) {\n const to = (tool.arguments?.to || tool.arguments?.recipient) as string | undefined;\n const subject = (tool.arguments?.subject) as string | undefined;\n const body = (tool.arguments?.body || tool.arguments?.content) as string | undefined;\n const summary = to ? `→ ${to}` : null;\n\n return (\n <ToolCallShell tool={tool} icon={Mail} label=\"Email\" summary={summary}>\n <div className=\"bg-p-bg px-2.5 py-2 text-xs max-h-[180px] overflow-y-auto\">\n {subject && (\n <div className=\"mb-1\">\n <span className=\"text-p-ink-3\">Subject: </span>\n <span className=\"text-p-ink font-medium\">{subject}</span>\n </div>\n )}\n {to && (\n <div className=\"mb-1.5\">\n <span className=\"text-p-ink-3\">To: </span>\n <span className=\"text-p-ink-2\">{to}</span>\n </div>\n )}\n {body && (\n <p className=\"m-0 text-p-ink-2 leading-relaxed whitespace-pre-wrap\">\n {body.slice(0, 300)}{body.length > 300 ? \"…\" : \"\"}\n </p>\n )}\n </div>\n </ToolCallShell>\n );\n}\n","\"use client\";\n\nimport type { ToolCallEvent } from \"@polpo-ai/sdk\";\nimport { MessageSquareMore, Check } from \"lucide-react\";\nimport { ToolCallShell } from \"./tool-call-shell\";\n\ninterface AskUserQuestion {\n id: string;\n question: string;\n header?: string;\n options?: { label: string; description?: string }[];\n}\n\n/** Ask user question tool — shows the questions and answers after completion */\nexport function ToolAskUser({ tool }: { tool: ToolCallEvent }) {\n const questions = (tool.arguments?.questions || []) as AskUserQuestion[];\n const isAnswered = tool.state === \"completed\" || tool.state === \"interrupted\";\n\n // Try to parse the result as answers\n let answers: { questionId: string; selected: string[] }[] = [];\n if (tool.result) {\n try {\n const parsed = JSON.parse(tool.result);\n answers = parsed.answers || [];\n } catch {\n // result might be plain text — not structured\n }\n }\n\n return (\n <ToolCallShell\n tool={tool}\n icon={MessageSquareMore}\n label=\"Question\"\n summary={isAnswered ? `${questions.length} answered` : `${questions.length} question${questions.length > 1 ? \"s\" : \"\"}`}\n >\n <div className=\"bg-p-bg px-3 py-2 text-xs space-y-2\">\n {questions.map((q) => {\n const answer = answers.find((a) => a.questionId === q.id);\n return (\n <div key={q.id} className=\"flex items-start gap-2\">\n {isAnswered ? (\n <Check size={12} className=\"text-p-green shrink-0 mt-0.5\" />\n ) : (\n <MessageSquareMore size={12} className=\"text-p-accent shrink-0 mt-0.5\" />\n )}\n <div className=\"min-w-0\">\n <p className=\"text-p-ink font-medium\">{q.question}</p>\n {answer && answer.selected.length > 0 && (\n <p className=\"text-p-ink-3 mt-0.5\">{answer.selected.join(\", \")}</p>\n )}\n {!answer && isAnswered && (\n <p className=\"text-p-ink-3/50 italic mt-0.5\">Skipped</p>\n )}\n </div>\n </div>\n );\n })}\n </div>\n </ToolCallShell>\n );\n}\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs","jsx","jsxs","jsx","jsxs","jsx","jsxs","jsx","jsxs","Check","jsx","jsxs","Check","jsx"]}
@@ -0,0 +1,64 @@
1
+ // src/hooks/use-submit-handler.ts
2
+ import { useCallback } from "react";
3
+ function useSubmitHandler(sendMessage, uploadFile) {
4
+ return useCallback(async (message) => {
5
+ const text = message.text.trim();
6
+ const files = message.files || [];
7
+ if (!text && files.length === 0) return;
8
+ if (files.length > 0) {
9
+ const parts = [];
10
+ if (text) parts.push({ type: "text", text });
11
+ for (const f of files) {
12
+ const name = f.filename || "upload";
13
+ try {
14
+ const res = await fetch(f.url);
15
+ const blob = await res.blob();
16
+ await uploadFile("workspace", blob, name);
17
+ parts.push({ type: "file", file_id: `workspace/${name}` });
18
+ } catch {
19
+ }
20
+ }
21
+ if (parts.length > 0) sendMessage(parts);
22
+ } else {
23
+ sendMessage(text);
24
+ }
25
+ }, [sendMessage, uploadFile]);
26
+ }
27
+
28
+ // src/hooks/use-document-drag.ts
29
+ import { useState, useRef, useEffect } from "react";
30
+ function useDocumentDrag() {
31
+ const [dragging, setDragging] = useState(false);
32
+ const counterRef = useRef(0);
33
+ useEffect(() => {
34
+ const onEnter = (e) => {
35
+ if (e.dataTransfer?.types?.includes("Files")) {
36
+ counterRef.current++;
37
+ setDragging(true);
38
+ }
39
+ };
40
+ const onLeave = () => {
41
+ counterRef.current--;
42
+ if (counterRef.current === 0) setDragging(false);
43
+ };
44
+ const onDrop = () => {
45
+ counterRef.current = 0;
46
+ setDragging(false);
47
+ };
48
+ document.addEventListener("dragenter", onEnter);
49
+ document.addEventListener("dragleave", onLeave);
50
+ document.addEventListener("drop", onDrop);
51
+ return () => {
52
+ document.removeEventListener("dragenter", onEnter);
53
+ document.removeEventListener("dragleave", onLeave);
54
+ document.removeEventListener("drop", onDrop);
55
+ };
56
+ }, []);
57
+ return dragging;
58
+ }
59
+
60
+ export {
61
+ useSubmitHandler,
62
+ useDocumentDrag
63
+ };
64
+ //# sourceMappingURL=chunk-LTLIBITC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/use-submit-handler.ts","../src/hooks/use-document-drag.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback } from \"react\";\nimport type { ContentPart } from \"@polpo-ai/sdk\";\n\n/** Shape of the message emitted by the PromptInput component. */\nexport interface PromptInputMessage {\n text: string;\n files: { url: string; filename?: string }[];\n}\n\n/** Shared submit handler — uploads files via SDK then sends ContentPart[] */\nexport function useSubmitHandler(\n sendMessage: (content: string | ContentPart[]) => Promise<void>,\n uploadFile: (destPath: string, file: Blob, filename: string) => Promise<unknown>,\n) {\n return useCallback(async (message: PromptInputMessage) => {\n const text = message.text.trim();\n const files = message.files || [];\n if (!text && files.length === 0) return;\n\n if (files.length > 0) {\n const parts: ContentPart[] = [];\n if (text) parts.push({ type: \"text\", text });\n for (const f of files) {\n const name = f.filename || \"upload\";\n try {\n const res = await fetch(f.url);\n const blob = await res.blob();\n await uploadFile(\"workspace\", blob, name);\n parts.push({ type: \"file\", file_id: `workspace/${name}` });\n } catch { /* skip failed uploads */ }\n }\n if (parts.length > 0) sendMessage(parts);\n } else {\n sendMessage(text);\n }\n }, [sendMessage, uploadFile]);\n}\n","\"use client\";\n\nimport { useState, useRef, useEffect } from \"react\";\n\n/** Track document-level drag state for visual feedback */\nexport function useDocumentDrag() {\n const [dragging, setDragging] = useState(false);\n const counterRef = useRef(0);\n useEffect(() => {\n const onEnter = (e: DragEvent) => { if (e.dataTransfer?.types?.includes(\"Files\")) { counterRef.current++; setDragging(true); } };\n const onLeave = () => { counterRef.current--; if (counterRef.current === 0) setDragging(false); };\n const onDrop = () => { counterRef.current = 0; setDragging(false); };\n document.addEventListener(\"dragenter\", onEnter);\n document.addEventListener(\"dragleave\", onLeave);\n document.addEventListener(\"drop\", onDrop);\n return () => { document.removeEventListener(\"dragenter\", onEnter); document.removeEventListener(\"dragleave\", onLeave); document.removeEventListener(\"drop\", onDrop); };\n }, []);\n return dragging;\n}\n"],"mappings":";AAEA,SAAS,mBAAmB;AAUrB,SAAS,iBACd,aACA,YACA;AACA,SAAO,YAAY,OAAO,YAAgC;AACxD,UAAM,OAAO,QAAQ,KAAK,KAAK;AAC/B,UAAM,QAAQ,QAAQ,SAAS,CAAC;AAChC,QAAI,CAAC,QAAQ,MAAM,WAAW,EAAG;AAEjC,QAAI,MAAM,SAAS,GAAG;AACpB,YAAM,QAAuB,CAAC;AAC9B,UAAI,KAAM,OAAM,KAAK,EAAE,MAAM,QAAQ,KAAK,CAAC;AAC3C,iBAAW,KAAK,OAAO;AACrB,cAAM,OAAO,EAAE,YAAY;AAC3B,YAAI;AACF,gBAAM,MAAM,MAAM,MAAM,EAAE,GAAG;AAC7B,gBAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,gBAAM,WAAW,aAAa,MAAM,IAAI;AACxC,gBAAM,KAAK,EAAE,MAAM,QAAQ,SAAS,aAAa,IAAI,GAAG,CAAC;AAAA,QAC3D,QAAQ;AAAA,QAA4B;AAAA,MACtC;AACA,UAAI,MAAM,SAAS,EAAG,aAAY,KAAK;AAAA,IACzC,OAAO;AACL,kBAAY,IAAI;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,aAAa,UAAU,CAAC;AAC9B;;;ACpCA,SAAS,UAAU,QAAQ,iBAAiB;AAGrC,SAAS,kBAAkB;AAChC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,aAAa,OAAO,CAAC;AAC3B,YAAU,MAAM;AACd,UAAM,UAAU,CAAC,MAAiB;AAAE,UAAI,EAAE,cAAc,OAAO,SAAS,OAAO,GAAG;AAAE,mBAAW;AAAW,oBAAY,IAAI;AAAA,MAAG;AAAA,IAAE;AAC/H,UAAM,UAAU,MAAM;AAAE,iBAAW;AAAW,UAAI,WAAW,YAAY,EAAG,aAAY,KAAK;AAAA,IAAG;AAChG,UAAM,SAAS,MAAM;AAAE,iBAAW,UAAU;AAAG,kBAAY,KAAK;AAAA,IAAG;AACnE,aAAS,iBAAiB,aAAa,OAAO;AAC9C,aAAS,iBAAiB,aAAa,OAAO;AAC9C,aAAS,iBAAiB,QAAQ,MAAM;AACxC,WAAO,MAAM;AAAE,eAAS,oBAAoB,aAAa,OAAO;AAAG,eAAS,oBAAoB,aAAa,OAAO;AAAG,eAAS,oBAAoB,QAAQ,MAAM;AAAA,IAAG;AAAA,EACvK,GAAG,CAAC,CAAC;AACL,SAAO;AACT;","names":[]}
@@ -0,0 +1,17 @@
1
+ import { ContentPart } from '@polpo-ai/sdk';
2
+
3
+ /** Shape of the message emitted by the PromptInput component. */
4
+ interface PromptInputMessage {
5
+ text: string;
6
+ files: {
7
+ url: string;
8
+ filename?: string;
9
+ }[];
10
+ }
11
+ /** Shared submit handler — uploads files via SDK then sends ContentPart[] */
12
+ declare function useSubmitHandler(sendMessage: (content: string | ContentPart[]) => Promise<void>, uploadFile: (destPath: string, file: Blob, filename: string) => Promise<unknown>): (message: PromptInputMessage) => Promise<void>;
13
+
14
+ /** Track document-level drag state for visual feedback */
15
+ declare function useDocumentDrag(): boolean;
16
+
17
+ export { useDocumentDrag, useSubmitHandler };
@@ -0,0 +1,9 @@
1
+ import {
2
+ useDocumentDrag,
3
+ useSubmitHandler
4
+ } from "../chunk-LTLIBITC.js";
5
+ export {
6
+ useDocumentDrag,
7
+ useSubmitHandler
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}