@open-mercato/ui 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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 (148) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -1
  3. package/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +204 -0
  4. package/dist/ai/AiAssistantLauncher.js +596 -0
  5. package/dist/ai/AiAssistantLauncher.js.map +7 -0
  6. package/dist/ai/AiChat.js +1092 -0
  7. package/dist/ai/AiChat.js.map +7 -0
  8. package/dist/ai/AiChatSessions.js +297 -0
  9. package/dist/ai/AiChatSessions.js.map +7 -0
  10. package/dist/ai/AiDock.js +347 -0
  11. package/dist/ai/AiDock.js.map +7 -0
  12. package/dist/ai/AiMessageContent.js +369 -0
  13. package/dist/ai/AiMessageContent.js.map +7 -0
  14. package/dist/ai/ChatPaneTabs.js +251 -0
  15. package/dist/ai/ChatPaneTabs.js.map +7 -0
  16. package/dist/ai/index.js +115 -0
  17. package/dist/ai/index.js.map +7 -0
  18. package/dist/ai/parts/ConfirmationCard.js +211 -0
  19. package/dist/ai/parts/ConfirmationCard.js.map +7 -0
  20. package/dist/ai/parts/FieldDiffCard.js +119 -0
  21. package/dist/ai/parts/FieldDiffCard.js.map +7 -0
  22. package/dist/ai/parts/MutationPreviewCard.js +224 -0
  23. package/dist/ai/parts/MutationPreviewCard.js.map +7 -0
  24. package/dist/ai/parts/MutationResultCard.js +240 -0
  25. package/dist/ai/parts/MutationResultCard.js.map +7 -0
  26. package/dist/ai/parts/approval-cards-map.js +15 -0
  27. package/dist/ai/parts/approval-cards-map.js.map +7 -0
  28. package/dist/ai/parts/index.js +24 -0
  29. package/dist/ai/parts/index.js.map +7 -0
  30. package/dist/ai/parts/pending-action-api.js +60 -0
  31. package/dist/ai/parts/pending-action-api.js.map +7 -0
  32. package/dist/ai/parts/types.js +1 -0
  33. package/dist/ai/parts/types.js.map +7 -0
  34. package/dist/ai/parts/useAiPendingActionPolling.js +126 -0
  35. package/dist/ai/parts/useAiPendingActionPolling.js.map +7 -0
  36. package/dist/ai/records/ActivityCard.js +83 -0
  37. package/dist/ai/records/ActivityCard.js.map +7 -0
  38. package/dist/ai/records/CompanyCard.js +81 -0
  39. package/dist/ai/records/CompanyCard.js.map +7 -0
  40. package/dist/ai/records/DealCard.js +76 -0
  41. package/dist/ai/records/DealCard.js.map +7 -0
  42. package/dist/ai/records/PersonCard.js +68 -0
  43. package/dist/ai/records/PersonCard.js.map +7 -0
  44. package/dist/ai/records/ProductCard.js +68 -0
  45. package/dist/ai/records/ProductCard.js.map +7 -0
  46. package/dist/ai/records/RecordCard.js +29 -0
  47. package/dist/ai/records/RecordCard.js.map +7 -0
  48. package/dist/ai/records/RecordCardShell.js +103 -0
  49. package/dist/ai/records/RecordCardShell.js.map +7 -0
  50. package/dist/ai/records/index.js +31 -0
  51. package/dist/ai/records/index.js.map +7 -0
  52. package/dist/ai/records/registry.js +51 -0
  53. package/dist/ai/records/registry.js.map +7 -0
  54. package/dist/ai/records/types.js +1 -0
  55. package/dist/ai/records/types.js.map +7 -0
  56. package/dist/ai/ui-part-registry.js +112 -0
  57. package/dist/ai/ui-part-registry.js.map +7 -0
  58. package/dist/ai/ui-part-slots.js +14 -0
  59. package/dist/ai/ui-part-slots.js.map +7 -0
  60. package/dist/ai/ui-parts/pending-phase3-placeholder.js +35 -0
  61. package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +7 -0
  62. package/dist/ai/upload-adapter.js +256 -0
  63. package/dist/ai/upload-adapter.js.map +7 -0
  64. package/dist/ai/useAiChat.js +549 -0
  65. package/dist/ai/useAiChat.js.map +7 -0
  66. package/dist/ai/useAiChatUpload.js +127 -0
  67. package/dist/ai/useAiChatUpload.js.map +7 -0
  68. package/dist/ai/useAiShortcuts.js +43 -0
  69. package/dist/ai/useAiShortcuts.js.map +7 -0
  70. package/dist/backend/AppShell.js +8 -4
  71. package/dist/backend/AppShell.js.map +2 -2
  72. package/dist/backend/BackendChromeProvider.js +2 -0
  73. package/dist/backend/BackendChromeProvider.js.map +2 -2
  74. package/dist/backend/DataTable.js +19 -2
  75. package/dist/backend/DataTable.js.map +2 -2
  76. package/dist/backend/FilterBar.js +19 -15
  77. package/dist/backend/FilterBar.js.map +2 -2
  78. package/dist/backend/dashboard/DashboardScreen.js +31 -3
  79. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  80. package/dist/backend/injection/spotIds.js +6 -0
  81. package/dist/backend/injection/spotIds.js.map +2 -2
  82. package/dist/backend/notifications/useNotificationEffect.js +38 -2
  83. package/dist/backend/notifications/useNotificationEffect.js.map +2 -2
  84. package/dist/index.js +1 -0
  85. package/dist/index.js.map +2 -2
  86. package/jest.config.cjs +7 -1
  87. package/jest.markdown-mock.tsx +7 -0
  88. package/package.json +10 -4
  89. package/src/ai/AiAssistantLauncher.tsx +805 -0
  90. package/src/ai/AiChat.tsx +1483 -0
  91. package/src/ai/AiChatSessions.tsx +429 -0
  92. package/src/ai/AiDock.tsx +505 -0
  93. package/src/ai/AiMessageContent.tsx +515 -0
  94. package/src/ai/ChatPaneTabs.tsx +310 -0
  95. package/src/ai/__tests__/AiChat.conversation.test.tsx +160 -0
  96. package/src/ai/__tests__/AiChat.debug.test.tsx +152 -0
  97. package/src/ai/__tests__/AiChat.registry.test.tsx +213 -0
  98. package/src/ai/__tests__/AiChat.test.tsx +257 -0
  99. package/src/ai/__tests__/AiDock.test.tsx +124 -0
  100. package/src/ai/__tests__/AiMessageContent.test.ts +111 -0
  101. package/src/ai/__tests__/ui-part-registry.test.ts +199 -0
  102. package/src/ai/__tests__/ui-part-slots.test.ts +43 -0
  103. package/src/ai/__tests__/upload-adapter.test.ts +213 -0
  104. package/src/ai/__tests__/useAiChatUpload.test.tsx +163 -0
  105. package/src/ai/__tests__/useAiShortcuts.test.tsx +100 -0
  106. package/src/ai/index.ts +125 -0
  107. package/src/ai/parts/ConfirmationCard.tsx +310 -0
  108. package/src/ai/parts/FieldDiffCard.tsx +173 -0
  109. package/src/ai/parts/MutationPreviewCard.tsx +302 -0
  110. package/src/ai/parts/MutationResultCard.tsx +360 -0
  111. package/src/ai/parts/__tests__/ConfirmationCard.test.tsx +169 -0
  112. package/src/ai/parts/__tests__/FieldDiffCard.test.tsx +74 -0
  113. package/src/ai/parts/__tests__/MutationPreviewCard.test.tsx +177 -0
  114. package/src/ai/parts/__tests__/MutationResultCard.test.tsx +127 -0
  115. package/src/ai/parts/__tests__/useAiPendingActionPolling.test.tsx +151 -0
  116. package/src/ai/parts/approval-cards-map.ts +24 -0
  117. package/src/ai/parts/index.ts +27 -0
  118. package/src/ai/parts/pending-action-api.ts +123 -0
  119. package/src/ai/parts/types.ts +84 -0
  120. package/src/ai/parts/useAiPendingActionPolling.ts +210 -0
  121. package/src/ai/records/ActivityCard.tsx +102 -0
  122. package/src/ai/records/CompanyCard.tsx +89 -0
  123. package/src/ai/records/DealCard.tsx +85 -0
  124. package/src/ai/records/PersonCard.tsx +77 -0
  125. package/src/ai/records/ProductCard.tsx +83 -0
  126. package/src/ai/records/RecordCard.tsx +37 -0
  127. package/src/ai/records/RecordCardShell.tsx +169 -0
  128. package/src/ai/records/index.ts +30 -0
  129. package/src/ai/records/registry.tsx +80 -0
  130. package/src/ai/records/types.ts +90 -0
  131. package/src/ai/ui-part-registry.ts +233 -0
  132. package/src/ai/ui-part-slots.ts +32 -0
  133. package/src/ai/ui-parts/pending-phase3-placeholder.tsx +50 -0
  134. package/src/ai/upload-adapter.ts +421 -0
  135. package/src/ai/useAiChat.ts +865 -0
  136. package/src/ai/useAiChatUpload.ts +180 -0
  137. package/src/ai/useAiShortcuts.ts +79 -0
  138. package/src/backend/AppShell.tsx +12 -5
  139. package/src/backend/BackendChromeProvider.tsx +2 -0
  140. package/src/backend/DataTable.tsx +20 -1
  141. package/src/backend/FilterBar.tsx +26 -13
  142. package/src/backend/__tests__/BackendChromeProvider.test.tsx +45 -0
  143. package/src/backend/dashboard/DashboardScreen.tsx +38 -3
  144. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +24 -1
  145. package/src/backend/injection/spotIds.ts +6 -0
  146. package/src/backend/notifications/__tests__/useNotificationEffect.test.tsx +77 -0
  147. package/src/backend/notifications/useNotificationEffect.ts +47 -2
  148. package/src/index.ts +1 -0
@@ -0,0 +1,1092 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import {
5
+ Bot,
6
+ Check,
7
+ ChevronDown,
8
+ ChevronRight,
9
+ Copy,
10
+ Lightbulb,
11
+ Loader2,
12
+ Paperclip,
13
+ Plus,
14
+ Send,
15
+ Square,
16
+ User,
17
+ Wrench,
18
+ X
19
+ } from "lucide-react";
20
+ import ReactMarkdown from "react-markdown";
21
+ import remarkGfm from "remark-gfm";
22
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
23
+ import { cn } from "@open-mercato/shared/lib/utils";
24
+ import { Alert, AlertDescription, AlertTitle } from "../primitives/alert.js";
25
+ import { Button } from "../primitives/button.js";
26
+ import { IconButton } from "../primitives/icon-button.js";
27
+ import { Label } from "../primitives/label.js";
28
+ import { Textarea } from "../primitives/textarea.js";
29
+ import { parseAiContentSegments } from "./AiMessageContent.js";
30
+ import { RecordCard } from "./records/RecordCard.js";
31
+ import {
32
+ defaultAiUiPartRegistry,
33
+ isReservedAiUiPartId
34
+ } from "./ui-part-registry.js";
35
+ import {
36
+ useAiChat
37
+ } from "./useAiChat.js";
38
+ import { useAiChatUpload } from "./useAiChatUpload.js";
39
+ import { useAiShortcuts } from "./useAiShortcuts.js";
40
+ const PREVIEW_DATA_URL_MAX_BYTES = 2 * 1024 * 1024;
41
+ async function readFileAsDataUrl(file) {
42
+ if (!file.type.startsWith("image/")) return void 0;
43
+ if (file.size > PREVIEW_DATA_URL_MAX_BYTES) return void 0;
44
+ return new Promise((resolve) => {
45
+ const reader = new FileReader();
46
+ reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : void 0);
47
+ reader.onerror = () => resolve(void 0);
48
+ try {
49
+ reader.readAsDataURL(file);
50
+ } catch {
51
+ resolve(void 0);
52
+ }
53
+ });
54
+ }
55
+ function mapErrorCodeToVariant(code) {
56
+ if (!code) return "destructive";
57
+ const warningCodes = /* @__PURE__ */ new Set([
58
+ "tool_not_whitelisted",
59
+ "tool_features_denied",
60
+ "attachment_type_not_accepted"
61
+ ]);
62
+ return warningCodes.has(code) ? "warning" : "destructive";
63
+ }
64
+ const MARKDOWN_TYPOGRAPHY_CLASS = cn(
65
+ "[&_p]:my-1 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0",
66
+ "[&_ul]:my-2 [&_ol]:my-2 [&_ul]:ml-4 [&_ol]:ml-4 [&_ul]:list-disc [&_ol]:list-decimal",
67
+ "[&_li]:my-0.5",
68
+ "[&_h1]:mt-3 [&_h1]:mb-2 [&_h1]:text-base [&_h1]:font-semibold",
69
+ "[&_h2]:mt-3 [&_h2]:mb-2 [&_h2]:text-sm [&_h2]:font-semibold",
70
+ "[&_h3]:mt-2 [&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-semibold",
71
+ "[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2",
72
+ "[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs",
73
+ "[&_pre]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:border [&_pre]:border-border [&_pre]:bg-muted [&_pre]:p-3",
74
+ "[&_pre_code]:bg-transparent [&_pre_code]:p-0",
75
+ "[&_blockquote]:my-2 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground",
76
+ "[&_table]:my-2 [&_table]:w-full [&_table]:border-collapse [&_table]:text-xs",
77
+ "[&_th]:border [&_th]:border-border [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_th]:font-medium",
78
+ "[&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1"
79
+ );
80
+ const MARKDOWN_COMPONENTS = {
81
+ a: ({ node, ...props }) => /* @__PURE__ */ jsx("a", { ...props, target: "_blank", rel: "noreferrer" })
82
+ };
83
+ function MarkdownChunk({ text }) {
84
+ if (!text.trim()) return null;
85
+ return /* @__PURE__ */ jsx("div", { className: cn("text-sm", MARKDOWN_TYPOGRAPHY_CLASS), children: /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: MARKDOWN_COMPONENTS, children: text }) });
86
+ }
87
+ function MessageContent({ content, isAssistant }) {
88
+ if (!isAssistant) {
89
+ return /* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap text-sm", children: content });
90
+ }
91
+ if (!content) {
92
+ return null;
93
+ }
94
+ const segments = parseAiContentSegments(content);
95
+ if (segments.length === 0) {
96
+ return null;
97
+ }
98
+ return /* @__PURE__ */ jsx("div", { className: "space-y-1", "data-ai-message-content": "", children: segments.map((segment, index) => {
99
+ if (segment.kind === "record-card") {
100
+ return /* @__PURE__ */ jsx(RecordCard, { data: segment.payload }, `card-${index}`);
101
+ }
102
+ if (segment.kind === "invalid-card") {
103
+ return /* @__PURE__ */ jsx(
104
+ "pre",
105
+ {
106
+ className: "my-2 max-h-60 overflow-auto rounded-md border border-dashed border-border bg-muted p-2 text-xs",
107
+ "data-ai-record-card-invalid": segment.info,
108
+ children: segment.raw
109
+ },
110
+ `raw-${index}`
111
+ );
112
+ }
113
+ return /* @__PURE__ */ jsx(MarkdownChunk, { text: segment.text }, `md-${index}`);
114
+ }) });
115
+ }
116
+ function safeStringify(value) {
117
+ if (value === null || value === void 0) return "";
118
+ if (typeof value === "string") return value;
119
+ try {
120
+ return JSON.stringify(value, null, 2);
121
+ } catch {
122
+ return String(value);
123
+ }
124
+ }
125
+ function ToolCallList({ toolCalls }) {
126
+ const t = useT();
127
+ const [openId, setOpenId] = React.useState(null);
128
+ if (!toolCalls || toolCalls.length === 0) return null;
129
+ return /* @__PURE__ */ jsx("div", { className: "space-y-1", "data-ai-chat-tool-calls": "", children: toolCalls.map((call) => {
130
+ const isOpen = openId === call.id;
131
+ const isError = call.state === "error";
132
+ const isPending = call.state === "pending";
133
+ const isComplete = call.state === "complete";
134
+ const statusLabel = isError ? t("ai_assistant.chat.toolError", "failed") : isPending ? t("ai_assistant.chat.toolRunning", "running\u2026") : t("ai_assistant.chat.toolDone", "done");
135
+ return /* @__PURE__ */ jsxs(
136
+ "div",
137
+ {
138
+ className: cn(
139
+ "rounded-md border border-border bg-muted/30",
140
+ isError ? "border-destructive/40 bg-destructive/5" : ""
141
+ ),
142
+ "data-ai-chat-tool-call": call.toolName,
143
+ "data-ai-chat-tool-state": call.state,
144
+ children: [
145
+ /* @__PURE__ */ jsxs(
146
+ "button",
147
+ {
148
+ type: "button",
149
+ onClick: () => setOpenId(isOpen ? null : call.id),
150
+ className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs font-medium hover:bg-muted/60",
151
+ children: [
152
+ isOpen ? /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5 text-muted-foreground", "aria-hidden": true }) : /* @__PURE__ */ jsx(ChevronRight, { className: "size-3.5 text-muted-foreground", "aria-hidden": true }),
153
+ isPending ? /* @__PURE__ */ jsx(Loader2, { className: "size-3.5 animate-spin text-muted-foreground", "aria-hidden": true }) : /* @__PURE__ */ jsx(
154
+ Wrench,
155
+ {
156
+ className: cn(
157
+ "size-3.5",
158
+ isError ? "text-destructive" : "text-muted-foreground"
159
+ ),
160
+ "aria-hidden": true
161
+ }
162
+ ),
163
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: call.toolName }),
164
+ /* @__PURE__ */ jsx(
165
+ "span",
166
+ {
167
+ className: cn(
168
+ "ml-auto text-[10px] uppercase tracking-wide",
169
+ isError ? "text-destructive" : isComplete ? "text-status-success-text" : "text-muted-foreground"
170
+ ),
171
+ children: statusLabel
172
+ }
173
+ )
174
+ ]
175
+ }
176
+ ),
177
+ isOpen ? /* @__PURE__ */ jsxs("div", { className: "space-y-1 border-t border-border/60 px-2 py-1.5 text-xs", children: [
178
+ call.input !== void 0 ? /* @__PURE__ */ jsxs("div", { children: [
179
+ /* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: t("ai_assistant.chat.toolInput", "Input") }),
180
+ /* @__PURE__ */ jsx("pre", { className: "mt-0.5 max-h-32 overflow-auto rounded bg-background p-1.5 font-mono text-[11px]", children: safeStringify(call.input) })
181
+ ] }) : null,
182
+ call.output !== void 0 && !isError ? /* @__PURE__ */ jsxs("div", { children: [
183
+ /* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: t("ai_assistant.chat.toolOutput", "Output") }),
184
+ /* @__PURE__ */ jsx("pre", { className: "mt-0.5 max-h-32 overflow-auto rounded bg-background p-1.5 font-mono text-[11px]", children: safeStringify(call.output) })
185
+ ] }) : null,
186
+ call.errorMessage ? /* @__PURE__ */ jsx("div", { className: "text-destructive", children: call.errorMessage }) : null
187
+ ] }) : null
188
+ ]
189
+ },
190
+ call.id
191
+ );
192
+ }) });
193
+ }
194
+ function ReasoningPanel({ text, streaming }) {
195
+ const t = useT();
196
+ const [open, setOpen] = React.useState(false);
197
+ if (!text) return null;
198
+ return /* @__PURE__ */ jsxs(
199
+ "div",
200
+ {
201
+ className: "rounded-md border border-border bg-muted/30",
202
+ "data-ai-chat-reasoning": streaming ? "streaming" : "complete",
203
+ children: [
204
+ /* @__PURE__ */ jsxs(
205
+ "button",
206
+ {
207
+ type: "button",
208
+ onClick: () => setOpen((o) => !o),
209
+ className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs font-medium hover:bg-muted/60",
210
+ children: [
211
+ open ? /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5 text-muted-foreground", "aria-hidden": true }) : /* @__PURE__ */ jsx(ChevronRight, { className: "size-3.5 text-muted-foreground", "aria-hidden": true }),
212
+ /* @__PURE__ */ jsx(Lightbulb, { className: "size-3.5 text-muted-foreground", "aria-hidden": true }),
213
+ /* @__PURE__ */ jsx("span", { children: t("ai_assistant.chat.reasoning", "Reasoning") }),
214
+ streaming ? /* @__PURE__ */ jsx(
215
+ Loader2,
216
+ {
217
+ className: "ml-1 size-3 animate-spin text-muted-foreground",
218
+ "aria-hidden": true
219
+ }
220
+ ) : null
221
+ ]
222
+ }
223
+ ),
224
+ open ? /* @__PURE__ */ jsx("pre", { className: "max-h-48 overflow-auto whitespace-pre-wrap border-t border-border/60 px-2 py-1.5 text-xs text-muted-foreground", children: text }) : null
225
+ ]
226
+ }
227
+ );
228
+ }
229
+ function MessageRow({
230
+ message,
231
+ registry,
232
+ onMutationRequested
233
+ }) {
234
+ const t = useT();
235
+ const isAssistant = message.role === "assistant";
236
+ const label = isAssistant ? t("ai_assistant.chat.assistantRoleLabel", "Assistant") : t("ai_assistant.chat.userRoleLabel", "You");
237
+ const Icon = isAssistant ? Bot : User;
238
+ const [copied, setCopied] = React.useState(false);
239
+ const copyTimerRef = React.useRef(null);
240
+ React.useEffect(() => {
241
+ return () => {
242
+ if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
243
+ };
244
+ }, []);
245
+ const handleCopy = React.useCallback(async () => {
246
+ const text = message.content;
247
+ if (!text) return;
248
+ try {
249
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
250
+ await navigator.clipboard.writeText(text);
251
+ } else {
252
+ const textarea = document.createElement("textarea");
253
+ textarea.value = text;
254
+ textarea.setAttribute("readonly", "");
255
+ textarea.style.position = "absolute";
256
+ textarea.style.left = "-9999px";
257
+ document.body.appendChild(textarea);
258
+ textarea.select();
259
+ document.execCommand("copy");
260
+ document.body.removeChild(textarea);
261
+ }
262
+ setCopied(true);
263
+ if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
264
+ copyTimerRef.current = setTimeout(() => setCopied(false), 1500);
265
+ } catch {
266
+ }
267
+ }, [message.content]);
268
+ return /* @__PURE__ */ jsxs(
269
+ "div",
270
+ {
271
+ className: cn(
272
+ "group/message flex gap-3 px-3 py-2",
273
+ isAssistant ? "bg-muted/40 rounded-md" : ""
274
+ ),
275
+ "data-role": message.role,
276
+ children: [
277
+ /* @__PURE__ */ jsx(
278
+ "div",
279
+ {
280
+ className: cn(
281
+ "flex size-6 shrink-0 items-center justify-center rounded-full",
282
+ isAssistant ? "bg-primary/10 text-primary" : "bg-secondary text-secondary-foreground"
283
+ ),
284
+ "aria-hidden": true,
285
+ children: /* @__PURE__ */ jsx(Icon, { className: "size-4" })
286
+ }
287
+ ),
288
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-1", children: [
289
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
290
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }),
291
+ message.content ? /* @__PURE__ */ jsx(
292
+ IconButton,
293
+ {
294
+ type: "button",
295
+ variant: "ghost",
296
+ size: "sm",
297
+ onClick: handleCopy,
298
+ "aria-label": copied ? t("ai_assistant.chat.copied", "Copied") : t("ai_assistant.chat.copyMessage", "Copy message"),
299
+ "data-ai-chat-copy-button": "",
300
+ className: "opacity-0 transition-opacity group-hover/message:opacity-100 focus-visible:opacity-100",
301
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-3.5 text-status-success-icon", "aria-hidden": true }) : /* @__PURE__ */ jsx(Copy, { className: "size-3.5", "aria-hidden": true })
302
+ }
303
+ ) : null
304
+ ] }),
305
+ message.files && message.files.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 py-1", children: message.files.map(
306
+ (file, i) => file.previewUrl ? /* @__PURE__ */ jsx(
307
+ "img",
308
+ {
309
+ src: file.previewUrl,
310
+ alt: file.name,
311
+ className: "max-h-32 max-w-[200px] rounded-md border border-border object-cover"
312
+ },
313
+ i
314
+ ) : /* @__PURE__ */ jsxs(
315
+ "span",
316
+ {
317
+ className: "inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2 py-0.5 text-xs",
318
+ children: [
319
+ /* @__PURE__ */ jsx(Paperclip, { className: "size-3", "aria-hidden": true }),
320
+ file.name
321
+ ]
322
+ },
323
+ i
324
+ )
325
+ ) }) : null,
326
+ isAssistant && message.reasoning ? /* @__PURE__ */ jsx(
327
+ ReasoningPanel,
328
+ {
329
+ text: message.reasoning,
330
+ streaming: message.reasoningStreaming === true
331
+ }
332
+ ) : null,
333
+ isAssistant && message.toolCalls && message.toolCalls.length > 0 ? /* @__PURE__ */ jsx(ToolCallList, { toolCalls: message.toolCalls }) : null,
334
+ /* @__PURE__ */ jsx(MessageContent, { content: message.content, isAssistant }),
335
+ isAssistant && registry && message.uiParts && message.uiParts.length > 0 ? /* @__PURE__ */ jsx(
336
+ MessageUiParts,
337
+ {
338
+ parts: message.uiParts,
339
+ registry,
340
+ onMutationRequested
341
+ }
342
+ ) : null
343
+ ] })
344
+ ]
345
+ }
346
+ );
347
+ }
348
+ function MessageUiParts({
349
+ parts,
350
+ registry,
351
+ onMutationRequested
352
+ }) {
353
+ return /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-col gap-2", "data-ai-message-ui-parts": "", children: parts.map((part) => /* @__PURE__ */ jsx(
354
+ AiUiPartRenderer,
355
+ {
356
+ part: {
357
+ componentId: part.componentId,
358
+ payload: part.payload,
359
+ pendingActionId: part.pendingActionId
360
+ },
361
+ registry,
362
+ onMutationRequested
363
+ },
364
+ part.key
365
+ )) });
366
+ }
367
+ function UnknownUiPartPlaceholder({ componentId }) {
368
+ const t = useT();
369
+ return /* @__PURE__ */ jsx(
370
+ "div",
371
+ {
372
+ className: "inline-flex items-center gap-2 rounded-full border border-dashed border-border bg-muted px-3 py-1 text-xs text-muted-foreground",
373
+ "data-ai-ui-part-placeholder": componentId,
374
+ children: /* @__PURE__ */ jsxs("span", { children: [
375
+ t("ai_assistant.chat.uiPartPending", "Pending UI part:"),
376
+ " ",
377
+ componentId
378
+ ] })
379
+ }
380
+ );
381
+ }
382
+ function AiUiPartRenderer({
383
+ part,
384
+ registry,
385
+ onMutationRequested
386
+ }) {
387
+ const Component = registry.resolve(part.componentId);
388
+ const isReserved = isReservedAiUiPartId(part.componentId);
389
+ React.useEffect(() => {
390
+ if (Component) return;
391
+ if (isReserved) return;
392
+ try {
393
+ console.warn(
394
+ `[AiChat] No component registered for UI part "${part.componentId}".`
395
+ );
396
+ } catch {
397
+ }
398
+ }, [Component, part.componentId, isReserved]);
399
+ React.useEffect(() => {
400
+ if (part.pendingActionId) {
401
+ onMutationRequested?.(part.pendingActionId);
402
+ }
403
+ }, [part.pendingActionId, onMutationRequested]);
404
+ if (!Component) {
405
+ return /* @__PURE__ */ jsx(UnknownUiPartPlaceholder, { componentId: part.componentId });
406
+ }
407
+ return /* @__PURE__ */ jsx(
408
+ Component,
409
+ {
410
+ componentId: part.componentId,
411
+ payload: part.payload,
412
+ pendingActionId: part.pendingActionId
413
+ }
414
+ );
415
+ }
416
+ function WelcomeState({
417
+ title,
418
+ description,
419
+ suggestions,
420
+ onSuggestionClick
421
+ }) {
422
+ const t = useT();
423
+ const heading = title ?? t("ai_assistant.chat.welcomeTitle", "How can I help?");
424
+ const desc = description ?? t(
425
+ "ai_assistant.chat.welcomeDescription",
426
+ "Ask me anything about your data. Here are some things I can do:"
427
+ );
428
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-4 px-4 py-8", children: [
429
+ /* @__PURE__ */ jsx(
430
+ "div",
431
+ {
432
+ className: "flex size-12 items-center justify-center rounded-full bg-primary/10 text-primary",
433
+ "aria-hidden": true,
434
+ children: /* @__PURE__ */ jsx(Bot, { className: "size-6" })
435
+ }
436
+ ),
437
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1 text-center", children: [
438
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: heading }),
439
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: desc })
440
+ ] }),
441
+ suggestions && suggestions.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex w-full max-w-md flex-col gap-2", "data-ai-chat-suggestions": "", children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsxs(
442
+ "button",
443
+ {
444
+ type: "button",
445
+ className: "flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground",
446
+ onClick: () => onSuggestionClick(suggestion.prompt),
447
+ "data-ai-chat-suggestion": index,
448
+ children: [
449
+ suggestion.icon ? /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", "aria-hidden": true, children: suggestion.icon }) : null,
450
+ /* @__PURE__ */ jsx("span", { children: suggestion.label })
451
+ ]
452
+ },
453
+ index
454
+ )) }) : null
455
+ ] });
456
+ }
457
+ function ContextItemsPill({ items }) {
458
+ if (items.length === 0) return null;
459
+ return /* @__PURE__ */ jsx(
460
+ "div",
461
+ {
462
+ className: "flex flex-wrap gap-1.5 border-b border-border px-3 py-2",
463
+ "data-ai-chat-context-items": "",
464
+ children: items.map((item, index) => /* @__PURE__ */ jsx(
465
+ "span",
466
+ {
467
+ className: "inline-flex items-center gap-1 rounded-full border border-border bg-secondary px-2 py-0.5 text-xs text-secondary-foreground",
468
+ "data-ai-chat-context-item": index,
469
+ title: item.detail,
470
+ children: item.label
471
+ },
472
+ index
473
+ ))
474
+ }
475
+ );
476
+ }
477
+ function AiChat({
478
+ agent,
479
+ apiPath,
480
+ pageContext,
481
+ attachmentIds,
482
+ initialMessages,
483
+ debug,
484
+ className,
485
+ placeholder,
486
+ onMutationRequested,
487
+ onError,
488
+ registry,
489
+ uiParts: uiPartsProp,
490
+ debugTools,
491
+ debugPromptSections,
492
+ conversationId,
493
+ suggestions,
494
+ contextItems,
495
+ welcomeTitle,
496
+ welcomeDescription
497
+ }) {
498
+ const t = useT();
499
+ const textareaRef = React.useRef(null);
500
+ const transcriptRef = React.useRef(null);
501
+ const fileInputRef = React.useRef(null);
502
+ const [input, setInput] = React.useState("");
503
+ const [pendingFiles, setPendingFiles] = React.useState([]);
504
+ const upload = useAiChatUpload();
505
+ const isUploading = upload.busy;
506
+ const uploadedAttachmentIds = React.useMemo(
507
+ () => pendingFiles.map((entry) => entry.attachmentId).filter((id) => typeof id === "string" && id.length > 0),
508
+ [pendingFiles]
509
+ );
510
+ const allAttachmentIds = React.useMemo(
511
+ () => [...attachmentIds ?? [], ...uploadedAttachmentIds],
512
+ [attachmentIds, uploadedAttachmentIds]
513
+ );
514
+ const chat = useAiChat({
515
+ agent,
516
+ apiPath,
517
+ pageContext,
518
+ attachmentIds: allAttachmentIds.length > 0 ? allAttachmentIds : void 0,
519
+ debug,
520
+ initialMessages,
521
+ onError,
522
+ conversationId
523
+ });
524
+ const isStreaming = chat.status === "streaming";
525
+ const isSubmitting = chat.status === "submitting";
526
+ const isBusy = isStreaming || isSubmitting;
527
+ const lastAssistant = React.useMemo(() => {
528
+ for (let index = chat.messages.length - 1; index >= 0; index -= 1) {
529
+ const candidate = chat.messages[index];
530
+ if (candidate?.role === "assistant") return candidate;
531
+ }
532
+ return null;
533
+ }, [chat.messages]);
534
+ const trimmedContent = lastAssistant?.content?.trim() ?? "";
535
+ const hasReasoning = !!(lastAssistant?.reasoning && lastAssistant.reasoning.length > 0);
536
+ const toolCalls = lastAssistant?.toolCalls ?? [];
537
+ const hasPendingToolCall = toolCalls.some((call) => call.state === "pending");
538
+ const hasCompletedToolCall = toolCalls.some(
539
+ (call) => call.state === "complete" || call.state === "error"
540
+ );
541
+ const hasAnyVisibleSignal = !!(trimmedContent || hasReasoning || toolCalls.length > 0);
542
+ const assistantStreamSnapshot = React.useMemo(() => {
543
+ if (!lastAssistant) return "";
544
+ const toolSig = toolCalls.map((call) => `${call.id}:${call.state}:${call.output != null ? 1 : 0}`).join("|");
545
+ return [
546
+ lastAssistant.id,
547
+ lastAssistant.content?.length ?? 0,
548
+ lastAssistant.reasoning?.length ?? 0,
549
+ lastAssistant.reasoningStreaming ? 1 : 0,
550
+ toolSig
551
+ ].join("#");
552
+ }, [lastAssistant]);
553
+ const lastStreamUpdateRef = React.useRef(Date.now());
554
+ const lastSnapshotRef = React.useRef("");
555
+ const [, setStreamTick] = React.useState(0);
556
+ React.useEffect(() => {
557
+ if (assistantStreamSnapshot !== lastSnapshotRef.current) {
558
+ lastSnapshotRef.current = assistantStreamSnapshot;
559
+ lastStreamUpdateRef.current = Date.now();
560
+ setStreamTick((value) => value + 1);
561
+ }
562
+ }, [assistantStreamSnapshot]);
563
+ React.useEffect(() => {
564
+ if (!isStreaming && !isSubmitting) return;
565
+ const interval = window.setInterval(() => {
566
+ setStreamTick((value) => value + 1);
567
+ }, 200);
568
+ return () => window.clearInterval(interval);
569
+ }, [isStreaming, isSubmitting]);
570
+ const idleDuringStream = isStreaming && Date.now() - lastStreamUpdateRef.current >= 300;
571
+ const showThinkingIndicator = isSubmitting || isStreaming && (!hasAnyVisibleSignal || hasPendingToolCall || // Tool just returned and the model hasn't started speaking yet.
572
+ hasCompletedToolCall && !trimmedContent || idleDuringStream);
573
+ const activeRegistry = registry ?? defaultAiUiPartRegistry;
574
+ const uiParts = React.useMemo(
575
+ () => (uiPartsProp ?? []).map((part) => ({
576
+ componentId: part.componentId,
577
+ payload: part.payload,
578
+ pendingActionId: part.pendingActionId
579
+ })),
580
+ [uiPartsProp]
581
+ );
582
+ const hasUploadingFiles = React.useMemo(
583
+ () => pendingFiles.some((entry) => !entry.attachmentId && !entry.error),
584
+ [pendingFiles]
585
+ );
586
+ const handleSendMessage = React.useCallback(
587
+ (text) => {
588
+ if (!text.trim() || isBusy) return;
589
+ if (hasUploadingFiles || isUploading) return;
590
+ const filesToAttach = pendingFiles.map((entry) => {
591
+ const isImage = entry.file.type.startsWith("image/");
592
+ const fallback = isImage ? URL.createObjectURL(entry.file) : void 0;
593
+ return {
594
+ name: entry.file.name,
595
+ type: entry.file.type,
596
+ previewUrl: isImage ? entry.previewDataUrl ?? fallback : void 0
597
+ };
598
+ });
599
+ setInput("");
600
+ setPendingFiles([]);
601
+ void chat.sendMessage(text, filesToAttach.length > 0 ? filesToAttach : void 0);
602
+ },
603
+ [chat, hasUploadingFiles, isBusy, isUploading, pendingFiles]
604
+ );
605
+ const handleSubmit = React.useCallback(() => {
606
+ handleSendMessage(input);
607
+ }, [handleSendMessage, input]);
608
+ React.useEffect(() => {
609
+ if (typeof window === "undefined") return;
610
+ const handler = (event) => {
611
+ const detail = event.detail;
612
+ const message = detail?.message;
613
+ if (typeof message !== "string" || message.trim().length === 0) return;
614
+ handleSendMessage(message);
615
+ };
616
+ window.addEventListener("om-ai-chat-fix-request", handler);
617
+ return () => {
618
+ window.removeEventListener("om-ai-chat-fix-request", handler);
619
+ };
620
+ }, [handleSendMessage]);
621
+ const cancelOrBlur = React.useCallback(() => {
622
+ if (isBusy) {
623
+ chat.cancel();
624
+ return;
625
+ }
626
+ textareaRef.current?.blur();
627
+ }, [chat, isBusy]);
628
+ const handleFileSelect = React.useCallback(
629
+ async (event) => {
630
+ const files = Array.from(event.target.files ?? []);
631
+ if (files.length === 0) return;
632
+ const queued = files.map((file) => ({ file }));
633
+ setPendingFiles((prev) => [...prev, ...queued]);
634
+ const [previewResults, result] = await Promise.all([
635
+ Promise.all(files.map((file) => readFileAsDataUrl(file))),
636
+ upload.upload(files)
637
+ ]);
638
+ const idByIndex = /* @__PURE__ */ new Map();
639
+ for (const item of result.items) {
640
+ if (typeof item.inputIndex === "number") idByIndex.set(item.inputIndex, item.attachmentId);
641
+ }
642
+ const errorByIndex = /* @__PURE__ */ new Map();
643
+ for (const failure of result.failed) {
644
+ if (typeof failure.inputIndex === "number") errorByIndex.set(failure.inputIndex, failure.message);
645
+ }
646
+ setPendingFiles((prev) => {
647
+ const next = prev.slice();
648
+ const baseIndex = next.length - files.length;
649
+ for (let offset = 0; offset < files.length; offset += 1) {
650
+ const index = baseIndex + offset;
651
+ if (index < 0) continue;
652
+ const entry = next[index];
653
+ if (!entry) continue;
654
+ const dataUrl = previewResults[offset];
655
+ const patch = {
656
+ ...entry,
657
+ previewDataUrl: dataUrl ?? entry.previewDataUrl
658
+ };
659
+ if (!patch.attachmentId) {
660
+ const id = idByIndex.get(offset);
661
+ if (id) {
662
+ patch.attachmentId = id;
663
+ patch.error = void 0;
664
+ } else {
665
+ const explicitError = errorByIndex.get(offset);
666
+ patch.error = explicitError ?? "Upload finished without a server response. Remove the file and try again.";
667
+ }
668
+ }
669
+ next[index] = patch;
670
+ }
671
+ return next;
672
+ });
673
+ if (fileInputRef.current) fileInputRef.current.value = "";
674
+ },
675
+ [upload]
676
+ );
677
+ const removePendingFile = React.useCallback((index) => {
678
+ setPendingFiles((prev) => prev.filter((_, i) => i !== index));
679
+ }, []);
680
+ const { handleKeyDown } = useAiShortcuts({
681
+ onSubmit: handleSubmit,
682
+ onCancel: cancelOrBlur
683
+ });
684
+ React.useEffect(() => {
685
+ textareaRef.current?.focus();
686
+ }, []);
687
+ const stickToBottomRef = React.useRef(true);
688
+ const SCROLL_STICK_TOLERANCE_PX = 64;
689
+ React.useEffect(() => {
690
+ const node = transcriptRef.current;
691
+ if (!node) return;
692
+ const handleScroll = () => {
693
+ const distanceFromBottom = node.scrollHeight - node.scrollTop - node.clientHeight;
694
+ stickToBottomRef.current = distanceFromBottom <= SCROLL_STICK_TOLERANCE_PX;
695
+ };
696
+ node.addEventListener("scroll", handleScroll, { passive: true });
697
+ return () => node.removeEventListener("scroll", handleScroll);
698
+ }, []);
699
+ React.useEffect(() => {
700
+ const node = transcriptRef.current;
701
+ if (!node) return;
702
+ if (!stickToBottomRef.current) return;
703
+ node.scrollTop = node.scrollHeight;
704
+ }, [chat.messages]);
705
+ React.useEffect(() => {
706
+ if (typeof document === "undefined") return;
707
+ const previous = document.body.getAttribute("data-ai-chat-open");
708
+ document.body.setAttribute("data-ai-chat-open", "true");
709
+ return () => {
710
+ if (previous === null) {
711
+ document.body.removeAttribute("data-ai-chat-open");
712
+ } else {
713
+ document.body.setAttribute("data-ai-chat-open", previous);
714
+ }
715
+ };
716
+ }, []);
717
+ const handleNewConversation = React.useCallback(() => {
718
+ chat.reset();
719
+ setInput("");
720
+ setPendingFiles([]);
721
+ upload.reset();
722
+ setTimeout(() => textareaRef.current?.focus(), 0);
723
+ }, [chat, upload]);
724
+ const resolvedPlaceholder = placeholder ?? t("ai_assistant.chat.composerPlaceholder", "Message the AI agent...");
725
+ const errorVariant = mapErrorCodeToVariant(chat.error?.code);
726
+ return /* @__PURE__ */ jsxs(
727
+ "section",
728
+ {
729
+ className: cn(
730
+ "flex h-full min-h-[320px] flex-col gap-3 rounded-lg border border-border bg-background p-3",
731
+ className
732
+ ),
733
+ "aria-label": t("ai_assistant.chat.regionLabel", "AI chat"),
734
+ "data-ai-chat-agent": agent,
735
+ "data-ai-chat-conversation-id": chat.conversationId,
736
+ children: [
737
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border pb-2", children: [
738
+ /* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center gap-2 text-xs text-muted-foreground", children: contextItems && contextItems.length > 0 ? /* @__PURE__ */ jsx(ContextItemsPill, { items: contextItems }) : /* @__PURE__ */ jsx("span", { className: "font-mono opacity-70", "aria-hidden": true, children: chat.conversationId.slice(0, 8) }) }),
739
+ /* @__PURE__ */ jsx(
740
+ IconButton,
741
+ {
742
+ type: "button",
743
+ variant: "ghost",
744
+ size: "sm",
745
+ onClick: handleNewConversation,
746
+ disabled: isBusy,
747
+ "aria-label": t("ai_assistant.chat.newConversation", "Start new conversation"),
748
+ title: t("ai_assistant.chat.newConversation", "Start new conversation"),
749
+ "data-ai-chat-new-conversation": "",
750
+ children: /* @__PURE__ */ jsx(Plus, { className: "size-4", "aria-hidden": true })
751
+ }
752
+ )
753
+ ] }),
754
+ /* @__PURE__ */ jsxs(
755
+ "div",
756
+ {
757
+ ref: transcriptRef,
758
+ role: "log",
759
+ "aria-live": "polite",
760
+ "aria-label": t("ai_assistant.chat.transcriptLabel", "Chat transcript"),
761
+ className: "flex-1 space-y-2 overflow-y-auto pr-1",
762
+ children: [
763
+ chat.messages.length === 0 ? /* @__PURE__ */ jsx(
764
+ WelcomeState,
765
+ {
766
+ title: welcomeTitle,
767
+ description: welcomeDescription,
768
+ suggestions,
769
+ onSuggestionClick: handleSendMessage
770
+ }
771
+ ) : chat.messages.map((message) => /* @__PURE__ */ jsx(
772
+ MessageRow,
773
+ {
774
+ message,
775
+ registry: activeRegistry,
776
+ onMutationRequested
777
+ },
778
+ message.id
779
+ )),
780
+ uiParts.map((part, index) => /* @__PURE__ */ jsx(
781
+ AiUiPartRenderer,
782
+ {
783
+ part,
784
+ registry: activeRegistry,
785
+ onMutationRequested
786
+ },
787
+ `${part.componentId}-${index}`
788
+ )),
789
+ showThinkingIndicator ? /* @__PURE__ */ jsxs(
790
+ "div",
791
+ {
792
+ className: "flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground",
793
+ "data-ai-chat-state": "thinking",
794
+ children: [
795
+ /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }),
796
+ /* @__PURE__ */ jsx("span", { children: t("ai_assistant.chat.thinking", "Thinking...") })
797
+ ]
798
+ }
799
+ ) : null
800
+ ]
801
+ }
802
+ ),
803
+ chat.error ? /* @__PURE__ */ jsxs(Alert, { variant: errorVariant, "data-ai-chat-error": chat.error.code ?? "unknown", children: [
804
+ /* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.chat.errorTitle", "Agent dispatch failed") }),
805
+ /* @__PURE__ */ jsxs(AlertDescription, { children: [
806
+ chat.error.code ? /* @__PURE__ */ jsx("span", { className: "mr-2 font-mono text-xs", children: chat.error.code }) : null,
807
+ chat.error.message
808
+ ] })
809
+ ] }) : null,
810
+ /* @__PURE__ */ jsxs(
811
+ "form",
812
+ {
813
+ className: "flex flex-col gap-2",
814
+ onSubmit: (event) => {
815
+ event.preventDefault();
816
+ handleSubmit();
817
+ },
818
+ children: [
819
+ /* @__PURE__ */ jsx(
820
+ Label,
821
+ {
822
+ htmlFor: "ai-chat-composer",
823
+ className: "sr-only",
824
+ children: t("ai_assistant.chat.composerLabel", "Message composer")
825
+ }
826
+ ),
827
+ pendingFiles.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1.5 rounded-md border border-border bg-muted/30 px-2 py-1.5", "data-ai-chat-attachments": "", children: [
828
+ pendingFiles.map((entry, index) => /* @__PURE__ */ jsxs(
829
+ "span",
830
+ {
831
+ className: "inline-flex items-center gap-1 rounded-full border border-border bg-background px-2 py-0.5 text-xs",
832
+ title: entry.error ? entry.error : void 0,
833
+ "data-ai-chat-attachment-state": entry.error ? "error" : entry.attachmentId ? "ready" : "uploading",
834
+ children: [
835
+ /* @__PURE__ */ jsx(Paperclip, { className: "size-3 text-muted-foreground", "aria-hidden": true }),
836
+ /* @__PURE__ */ jsx("span", { className: "max-w-[120px] truncate", children: entry.file.name }),
837
+ !entry.attachmentId && !entry.error ? /* @__PURE__ */ jsx(Loader2, { className: "size-3 animate-spin text-muted-foreground", "aria-hidden": true }) : null,
838
+ /* @__PURE__ */ jsx(
839
+ "button",
840
+ {
841
+ type: "button",
842
+ className: "ml-0.5 rounded-full p-0.5 hover:bg-muted",
843
+ onClick: () => removePendingFile(index),
844
+ "aria-label": t("ai_assistant.chat.removeFile", "Remove file"),
845
+ children: /* @__PURE__ */ jsx(X, { className: "size-3" })
846
+ }
847
+ )
848
+ ]
849
+ },
850
+ index
851
+ )),
852
+ isUploading ? /* @__PURE__ */ jsx(Loader2, { className: "size-3 animate-spin text-muted-foreground", "aria-hidden": true }) : null
853
+ ] }) : null,
854
+ /* @__PURE__ */ jsx(
855
+ Textarea,
856
+ {
857
+ id: "ai-chat-composer",
858
+ ref: textareaRef,
859
+ value: input,
860
+ placeholder: resolvedPlaceholder,
861
+ onChange: (event) => setInput(event.target.value),
862
+ onKeyDown: handleKeyDown,
863
+ rows: 3,
864
+ "aria-label": t("ai_assistant.chat.composerLabel", "Message composer"),
865
+ className: "resize-none"
866
+ }
867
+ ),
868
+ /* @__PURE__ */ jsx(
869
+ "input",
870
+ {
871
+ ref: fileInputRef,
872
+ type: "file",
873
+ multiple: true,
874
+ accept: "image/*,.pdf,.doc,.docx,.txt,.csv",
875
+ className: "hidden",
876
+ onChange: handleFileSelect,
877
+ "data-ai-chat-file-input": ""
878
+ }
879
+ ),
880
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
881
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
882
+ /* @__PURE__ */ jsx(
883
+ IconButton,
884
+ {
885
+ type: "button",
886
+ variant: "ghost",
887
+ size: "sm",
888
+ onClick: () => fileInputRef.current?.click(),
889
+ disabled: isBusy || isUploading,
890
+ "aria-label": t("ai_assistant.chat.attachFile", "Attach file"),
891
+ children: /* @__PURE__ */ jsx(Paperclip, { className: "size-4", "aria-hidden": true })
892
+ }
893
+ ),
894
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: hasUploadingFiles || isUploading ? t(
895
+ "ai_assistant.chat.uploadingHint",
896
+ "Uploading attachments\u2026 Send is disabled until they finish."
897
+ ) : t(
898
+ "ai_assistant.chat.shortcutHint",
899
+ "Press Enter to send, Shift+Enter for new line."
900
+ ) })
901
+ ] }),
902
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
903
+ isStreaming ? /* @__PURE__ */ jsx(
904
+ IconButton,
905
+ {
906
+ type: "button",
907
+ variant: "outline",
908
+ size: "sm",
909
+ onClick: () => chat.cancel(),
910
+ "aria-label": t("ai_assistant.chat.cancel", "Cancel streaming response"),
911
+ children: /* @__PURE__ */ jsx(Square, { className: "size-4", "aria-hidden": true })
912
+ }
913
+ ) : null,
914
+ /* @__PURE__ */ jsxs(
915
+ Button,
916
+ {
917
+ type: "submit",
918
+ size: "sm",
919
+ disabled: isBusy || isUploading || hasUploadingFiles || input.trim().length === 0,
920
+ "aria-label": hasUploadingFiles || isUploading ? t("ai_assistant.chat.sendWaitingForUpload", "Waiting for upload to finish\u2026") : t("ai_assistant.chat.send", "Send message"),
921
+ title: hasUploadingFiles || isUploading ? t("ai_assistant.chat.sendWaitingForUpload", "Waiting for upload to finish\u2026") : void 0,
922
+ children: [
923
+ /* @__PURE__ */ jsx(Send, { className: "size-4", "aria-hidden": true }),
924
+ /* @__PURE__ */ jsx("span", { children: t("ai_assistant.chat.send", "Send message") })
925
+ ]
926
+ }
927
+ )
928
+ ] })
929
+ ] })
930
+ ]
931
+ }
932
+ ),
933
+ debug ? /* @__PURE__ */ jsx(
934
+ AiChatDebugPanel,
935
+ {
936
+ tools: debugTools,
937
+ promptSections: debugPromptSections,
938
+ lastRequestDebug: chat.lastRequestDebug,
939
+ lastResponseDebug: chat.lastResponseDebug,
940
+ status: chat.status,
941
+ errorCode: chat.error?.code
942
+ }
943
+ ) : null
944
+ ]
945
+ }
946
+ );
947
+ }
948
+ function AiChatDebugPanel({
949
+ tools,
950
+ promptSections,
951
+ lastRequestDebug,
952
+ lastResponseDebug,
953
+ status,
954
+ errorCode
955
+ }) {
956
+ const t = useT();
957
+ return /* @__PURE__ */ jsxs(
958
+ "div",
959
+ {
960
+ className: "flex flex-col gap-2 rounded-md border border-border bg-muted/60 p-2 text-xs",
961
+ "data-ai-chat-debug": "true",
962
+ children: [
963
+ /* @__PURE__ */ jsx("div", { className: "font-semibold", children: t("ai_assistant.chat.debug.panelTitle", "Debug panel") }),
964
+ /* @__PURE__ */ jsxs("details", { className: "rounded border border-border bg-background", "data-ai-chat-debug-section": "tools", open: true, children: [
965
+ /* @__PURE__ */ jsxs("summary", { className: "cursor-pointer px-2 py-1 font-semibold", children: [
966
+ t("ai_assistant.chat.debug.toolsSection", "Resolved tools"),
967
+ tools ? /* @__PURE__ */ jsxs("span", { className: "ml-2 font-mono text-muted-foreground", children: [
968
+ "(",
969
+ tools.length,
970
+ ")"
971
+ ] }) : null
972
+ ] }),
973
+ /* @__PURE__ */ jsx("div", { className: "px-2 pb-2", children: tools && tools.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-1", "data-ai-chat-debug-tools": true, children: tools.map((tool) => /* @__PURE__ */ jsxs(
974
+ "li",
975
+ {
976
+ className: "flex flex-col rounded border border-border bg-muted/40 px-2 py-1",
977
+ "data-ai-chat-debug-tool": tool.name,
978
+ children: [
979
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: tool.name }),
980
+ tool.displayName ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: tool.displayName }) : null,
981
+ /* @__PURE__ */ jsxs("span", { className: "mt-1 flex flex-wrap gap-2 text-muted-foreground", children: [
982
+ /* @__PURE__ */ jsx("span", { children: tool.isMutation ? t("ai_assistant.chat.debug.toolMutation", "mutation") : t("ai_assistant.chat.debug.toolRead", "read") }),
983
+ tool.requiredFeatures && tool.requiredFeatures.length > 0 ? /* @__PURE__ */ jsxs("span", { className: "font-mono", children: [
984
+ "[",
985
+ tool.requiredFeatures.join(", "),
986
+ "]"
987
+ ] }) : /* @__PURE__ */ jsx("span", { children: t("ai_assistant.chat.debug.toolNoFeatures", "no required features") })
988
+ ] })
989
+ ]
990
+ },
991
+ tool.name
992
+ )) }) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t(
993
+ "ai_assistant.chat.debug.toolsEmpty",
994
+ "No tools resolved for this agent yet."
995
+ ) }) })
996
+ ] }),
997
+ /* @__PURE__ */ jsxs(
998
+ "details",
999
+ {
1000
+ className: "rounded border border-border bg-background",
1001
+ "data-ai-chat-debug-section": "promptSections",
1002
+ children: [
1003
+ /* @__PURE__ */ jsxs("summary", { className: "cursor-pointer px-2 py-1 font-semibold", children: [
1004
+ t("ai_assistant.chat.debug.promptSection", "Prompt sections"),
1005
+ promptSections ? /* @__PURE__ */ jsxs("span", { className: "ml-2 font-mono text-muted-foreground", children: [
1006
+ "(",
1007
+ promptSections.length,
1008
+ ")"
1009
+ ] }) : null
1010
+ ] }),
1011
+ /* @__PURE__ */ jsx("div", { className: "px-2 pb-2", children: promptSections && promptSections.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-1", "data-ai-chat-debug-prompt-sections": true, children: promptSections.map((section) => /* @__PURE__ */ jsxs(
1012
+ "li",
1013
+ {
1014
+ className: "rounded border border-border bg-muted/40 px-2 py-1",
1015
+ "data-ai-chat-debug-prompt-section-id": section.id,
1016
+ children: [
1017
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
1018
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: section.id }),
1019
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: section.source === "override" ? t("ai_assistant.chat.debug.promptOverride", "override") : section.source === "placeholder" ? t("ai_assistant.chat.debug.promptPlaceholder", "placeholder") : t("ai_assistant.chat.debug.promptDefault", "default") })
1020
+ ] }),
1021
+ section.text ? /* @__PURE__ */ jsx("pre", { className: "mt-1 max-h-24 overflow-auto whitespace-pre-wrap font-mono text-muted-foreground", children: section.text }) : null
1022
+ ]
1023
+ },
1024
+ section.id
1025
+ )) }) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t(
1026
+ "ai_assistant.chat.debug.promptEmpty",
1027
+ "No prompt sections resolved for this agent."
1028
+ ) }) })
1029
+ ]
1030
+ }
1031
+ ),
1032
+ /* @__PURE__ */ jsxs(
1033
+ "details",
1034
+ {
1035
+ className: "rounded border border-border bg-background",
1036
+ "data-ai-chat-debug-section": "lastRequest",
1037
+ children: [
1038
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer px-2 py-1 font-semibold", children: t("ai_assistant.chat.debug.lastRequestSection", "Last request") }),
1039
+ /* @__PURE__ */ jsx("div", { className: "px-2 pb-2", children: lastRequestDebug ? /* @__PURE__ */ jsx(
1040
+ "pre",
1041
+ {
1042
+ className: "max-h-40 overflow-auto whitespace-pre-wrap font-mono",
1043
+ "data-ai-chat-debug-last-request": true,
1044
+ children: JSON.stringify(lastRequestDebug, null, 2)
1045
+ }
1046
+ ) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t(
1047
+ "ai_assistant.chat.debug.lastRequestEmpty",
1048
+ "No request has been sent yet."
1049
+ ) }) })
1050
+ ]
1051
+ }
1052
+ ),
1053
+ /* @__PURE__ */ jsxs(
1054
+ "details",
1055
+ {
1056
+ className: "rounded border border-border bg-background",
1057
+ "data-ai-chat-debug-section": "lastResponse",
1058
+ children: [
1059
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer px-2 py-1 font-semibold", children: t("ai_assistant.chat.debug.lastResponseSection", "Last response") }),
1060
+ /* @__PURE__ */ jsx("div", { className: "px-2 pb-2", children: lastResponseDebug ? /* @__PURE__ */ jsx(
1061
+ "pre",
1062
+ {
1063
+ className: "max-h-40 overflow-auto whitespace-pre-wrap font-mono",
1064
+ "data-ai-chat-debug-last-response": true,
1065
+ children: JSON.stringify(
1066
+ { status: lastResponseDebug.status, text: lastResponseDebug.text, errorCode },
1067
+ null,
1068
+ 2
1069
+ )
1070
+ }
1071
+ ) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t(
1072
+ "ai_assistant.chat.debug.lastResponseEmpty",
1073
+ "No response received yet."
1074
+ ) }) })
1075
+ ]
1076
+ }
1077
+ ),
1078
+ /* @__PURE__ */ jsxs("div", { className: "text-muted-foreground", "data-ai-chat-debug-status": status, children: [
1079
+ t("ai_assistant.chat.debug.statusLabel", "Status:"),
1080
+ " ",
1081
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: status })
1082
+ ] })
1083
+ ]
1084
+ }
1085
+ );
1086
+ }
1087
+ var AiChat_default = AiChat;
1088
+ export {
1089
+ AiChat,
1090
+ AiChat_default as default
1091
+ };
1092
+ //# sourceMappingURL=AiChat.js.map