@contractspec/module.ai-chat 4.0.3 → 4.1.2
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 +130 -10
- package/dist/adapters/ai-sdk-bundle-adapter.d.ts +18 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/browser/core/index.js +1143 -21
- package/dist/browser/index.js +2813 -631
- package/dist/browser/presentation/components/index.js +3160 -358
- package/dist/browser/presentation/hooks/index.js +978 -43
- package/dist/browser/presentation/index.js +2801 -666
- package/dist/core/agent-adapter.d.ts +53 -0
- package/dist/core/agent-tools-adapter.d.ts +12 -0
- package/dist/core/chat-service.d.ts +49 -1
- package/dist/core/contracts-context.d.ts +46 -0
- package/dist/core/contracts-context.test.d.ts +1 -0
- package/dist/core/conversation-store.d.ts +16 -2
- package/dist/core/create-chat-route.d.ts +3 -0
- package/dist/core/export-formatters.d.ts +29 -0
- package/dist/core/export-formatters.test.d.ts +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +1143 -21
- package/dist/core/local-storage-conversation-store.d.ts +33 -0
- package/dist/core/message-types.d.ts +6 -0
- package/dist/core/surface-planner-tools.d.ts +23 -0
- package/dist/core/surface-planner-tools.test.d.ts +1 -0
- package/dist/core/thinking-levels.d.ts +38 -0
- package/dist/core/thinking-levels.test.d.ts +1 -0
- package/dist/core/workflow-tools.d.ts +18 -0
- package/dist/core/workflow-tools.test.d.ts +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2813 -631
- package/dist/node/core/index.js +1143 -21
- package/dist/node/index.js +2813 -631
- package/dist/node/presentation/components/index.js +3160 -358
- package/dist/node/presentation/hooks/index.js +978 -43
- package/dist/node/presentation/index.js +2804 -669
- package/dist/presentation/components/ChatContainer.d.ts +3 -1
- package/dist/presentation/components/ChatExportToolbar.d.ts +25 -0
- package/dist/presentation/components/ChatMessage.d.ts +16 -1
- package/dist/presentation/components/ChatSidebar.d.ts +26 -0
- package/dist/presentation/components/ChatWithExport.d.ts +34 -0
- package/dist/presentation/components/ChatWithSidebar.d.ts +19 -0
- package/dist/presentation/components/ThinkingLevelPicker.d.ts +16 -0
- package/dist/presentation/components/ToolResultRenderer.d.ts +33 -0
- package/dist/presentation/components/index.d.ts +6 -0
- package/dist/presentation/components/index.js +3160 -358
- package/dist/presentation/hooks/index.d.ts +2 -0
- package/dist/presentation/hooks/index.js +978 -43
- package/dist/presentation/hooks/useChat.d.ts +44 -2
- package/dist/presentation/hooks/useConversations.d.ts +18 -0
- package/dist/presentation/hooks/useMessageSelection.d.ts +13 -0
- package/dist/presentation/index.js +2804 -669
- package/package.json +14 -18
|
@@ -10,12 +10,13 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
10
10
|
import * as React from "react";
|
|
11
11
|
import { ScrollArea } from "@contractspec/lib.ui-kit-web/ui/scroll-area";
|
|
12
12
|
import { cn } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
13
|
-
import {
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
"use client";
|
|
15
15
|
function ChatContainer({
|
|
16
16
|
children,
|
|
17
17
|
className,
|
|
18
|
-
showScrollButton = true
|
|
18
|
+
showScrollButton = true,
|
|
19
|
+
headerContent
|
|
19
20
|
}) {
|
|
20
21
|
const scrollRef = React.useRef(null);
|
|
21
22
|
const [showScrollDown, setShowScrollDown] = React.useState(false);
|
|
@@ -42,24 +43,28 @@ function ChatContainer({
|
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
}, []);
|
|
45
|
-
return /* @__PURE__ */
|
|
46
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
46
47
|
className: cn("relative flex flex-1 flex-col", className),
|
|
47
48
|
children: [
|
|
48
|
-
/* @__PURE__ */
|
|
49
|
+
headerContent && /* @__PURE__ */ jsx("div", {
|
|
50
|
+
className: "border-border flex shrink-0 items-center justify-end gap-2 border-b px-4 py-2",
|
|
51
|
+
children: headerContent
|
|
52
|
+
}),
|
|
53
|
+
/* @__PURE__ */ jsx(ScrollArea, {
|
|
49
54
|
ref: scrollRef,
|
|
50
55
|
className: "flex-1",
|
|
51
56
|
onScroll: handleScroll,
|
|
52
|
-
children: /* @__PURE__ */
|
|
57
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
53
58
|
className: "flex flex-col gap-4 p-4",
|
|
54
59
|
children
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
showScrollButton && showScrollDown && /* @__PURE__ */
|
|
60
|
+
})
|
|
61
|
+
}),
|
|
62
|
+
showScrollButton && showScrollDown && /* @__PURE__ */ jsxs("button", {
|
|
58
63
|
onClick: scrollToBottom,
|
|
59
64
|
className: cn("absolute bottom-4 left-1/2 -translate-x-1/2", "bg-primary text-primary-foreground", "rounded-full px-3 py-1.5 text-sm font-medium shadow-lg", "hover:bg-primary/90 transition-colors", "flex items-center gap-1.5"),
|
|
60
65
|
"aria-label": "Scroll to bottom",
|
|
61
66
|
children: [
|
|
62
|
-
/* @__PURE__ */
|
|
67
|
+
/* @__PURE__ */ jsx("svg", {
|
|
63
68
|
xmlns: "http://www.w3.org/2000/svg",
|
|
64
69
|
width: "16",
|
|
65
70
|
height: "16",
|
|
@@ -69,15 +74,15 @@ function ChatContainer({
|
|
|
69
74
|
strokeWidth: "2",
|
|
70
75
|
strokeLinecap: "round",
|
|
71
76
|
strokeLinejoin: "round",
|
|
72
|
-
children: /* @__PURE__ */
|
|
77
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
73
78
|
d: "m6 9 6 6 6-6"
|
|
74
|
-
}
|
|
75
|
-
}
|
|
79
|
+
})
|
|
80
|
+
}),
|
|
76
81
|
"New messages"
|
|
77
82
|
]
|
|
78
|
-
}
|
|
83
|
+
})
|
|
79
84
|
]
|
|
80
|
-
}
|
|
85
|
+
});
|
|
81
86
|
}
|
|
82
87
|
// src/presentation/components/ChatMessage.tsx
|
|
83
88
|
import * as React3 from "react";
|
|
@@ -91,16 +96,19 @@ import {
|
|
|
91
96
|
Copy as Copy2,
|
|
92
97
|
Check as Check2,
|
|
93
98
|
ExternalLink,
|
|
94
|
-
Wrench
|
|
99
|
+
Wrench,
|
|
100
|
+
Pencil,
|
|
101
|
+
X
|
|
95
102
|
} from "lucide-react";
|
|
96
103
|
import { Button as Button2 } from "@contractspec/lib.design-system";
|
|
104
|
+
import { Checkbox } from "@contractspec/lib.ui-kit-web/ui/checkbox";
|
|
97
105
|
|
|
98
106
|
// src/presentation/components/CodePreview.tsx
|
|
99
107
|
import * as React2 from "react";
|
|
100
108
|
import { cn as cn2 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
101
109
|
import { Button } from "@contractspec/lib.design-system";
|
|
102
110
|
import { Copy, Check, Play, Download } from "lucide-react";
|
|
103
|
-
import {
|
|
111
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
104
112
|
"use client";
|
|
105
113
|
var LANGUAGE_NAMES = {
|
|
106
114
|
ts: "TypeScript",
|
|
@@ -153,93 +161,147 @@ function CodePreview({
|
|
|
153
161
|
document.body.removeChild(a);
|
|
154
162
|
URL.revokeObjectURL(url);
|
|
155
163
|
}, [code, filename, language]);
|
|
156
|
-
return /* @__PURE__ */
|
|
164
|
+
return /* @__PURE__ */ jsxs2("div", {
|
|
157
165
|
className: cn2("overflow-hidden rounded-lg border", "bg-muted/50", className),
|
|
158
166
|
children: [
|
|
159
|
-
/* @__PURE__ */
|
|
167
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
160
168
|
className: cn2("flex items-center justify-between px-3 py-1.5", "bg-muted/80 border-b"),
|
|
161
169
|
children: [
|
|
162
|
-
/* @__PURE__ */
|
|
170
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
163
171
|
className: "flex items-center gap-2 text-sm",
|
|
164
172
|
children: [
|
|
165
|
-
filename && /* @__PURE__ */
|
|
173
|
+
filename && /* @__PURE__ */ jsx2("span", {
|
|
166
174
|
className: "text-foreground font-mono",
|
|
167
175
|
children: filename
|
|
168
|
-
}
|
|
169
|
-
/* @__PURE__ */
|
|
176
|
+
}),
|
|
177
|
+
/* @__PURE__ */ jsx2("span", {
|
|
170
178
|
className: "text-muted-foreground",
|
|
171
179
|
children: displayLanguage
|
|
172
|
-
}
|
|
180
|
+
})
|
|
173
181
|
]
|
|
174
|
-
}
|
|
175
|
-
/* @__PURE__ */
|
|
182
|
+
}),
|
|
183
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
176
184
|
className: "flex items-center gap-1",
|
|
177
185
|
children: [
|
|
178
|
-
showExecute && onExecute && /* @__PURE__ */
|
|
186
|
+
showExecute && onExecute && /* @__PURE__ */ jsx2(Button, {
|
|
179
187
|
variant: "ghost",
|
|
180
188
|
size: "sm",
|
|
181
189
|
onPress: () => onExecute(code),
|
|
182
190
|
className: "h-7 w-7 p-0",
|
|
183
191
|
"aria-label": "Execute code",
|
|
184
|
-
children: /* @__PURE__ */
|
|
192
|
+
children: /* @__PURE__ */ jsx2(Play, {
|
|
185
193
|
className: "h-3.5 w-3.5"
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
showDownload && /* @__PURE__ */
|
|
194
|
+
})
|
|
195
|
+
}),
|
|
196
|
+
showDownload && /* @__PURE__ */ jsx2(Button, {
|
|
189
197
|
variant: "ghost",
|
|
190
198
|
size: "sm",
|
|
191
199
|
onPress: handleDownload,
|
|
192
200
|
className: "h-7 w-7 p-0",
|
|
193
201
|
"aria-label": "Download code",
|
|
194
|
-
children: /* @__PURE__ */
|
|
202
|
+
children: /* @__PURE__ */ jsx2(Download, {
|
|
195
203
|
className: "h-3.5 w-3.5"
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
showCopy && /* @__PURE__ */
|
|
204
|
+
})
|
|
205
|
+
}),
|
|
206
|
+
showCopy && /* @__PURE__ */ jsx2(Button, {
|
|
199
207
|
variant: "ghost",
|
|
200
208
|
size: "sm",
|
|
201
209
|
onPress: handleCopy,
|
|
202
210
|
className: "h-7 w-7 p-0",
|
|
203
211
|
"aria-label": copied ? "Copied" : "Copy code",
|
|
204
|
-
children: copied ? /* @__PURE__ */
|
|
212
|
+
children: copied ? /* @__PURE__ */ jsx2(Check, {
|
|
205
213
|
className: "h-3.5 w-3.5 text-green-500"
|
|
206
|
-
}
|
|
214
|
+
}) : /* @__PURE__ */ jsx2(Copy, {
|
|
207
215
|
className: "h-3.5 w-3.5"
|
|
208
|
-
}
|
|
209
|
-
}
|
|
216
|
+
})
|
|
217
|
+
})
|
|
210
218
|
]
|
|
211
|
-
}
|
|
219
|
+
})
|
|
212
220
|
]
|
|
213
|
-
}
|
|
214
|
-
/* @__PURE__ */
|
|
221
|
+
}),
|
|
222
|
+
/* @__PURE__ */ jsx2("div", {
|
|
215
223
|
className: "overflow-auto",
|
|
216
224
|
style: { maxHeight },
|
|
217
|
-
children: /* @__PURE__ */
|
|
225
|
+
children: /* @__PURE__ */ jsx2("pre", {
|
|
218
226
|
className: "p-3",
|
|
219
|
-
children: /* @__PURE__ */
|
|
227
|
+
children: /* @__PURE__ */ jsx2("code", {
|
|
220
228
|
className: "text-sm",
|
|
221
|
-
children: lines.map((line, i) => /* @__PURE__ */
|
|
229
|
+
children: lines.map((line, i) => /* @__PURE__ */ jsxs2("div", {
|
|
222
230
|
className: "flex",
|
|
223
231
|
children: [
|
|
224
|
-
/* @__PURE__ */
|
|
232
|
+
/* @__PURE__ */ jsx2("span", {
|
|
225
233
|
className: "text-muted-foreground mr-4 w-8 text-right select-none",
|
|
226
234
|
children: i + 1
|
|
227
|
-
}
|
|
228
|
-
/* @__PURE__ */
|
|
235
|
+
}),
|
|
236
|
+
/* @__PURE__ */ jsx2("span", {
|
|
229
237
|
className: "flex-1",
|
|
230
238
|
children: line || " "
|
|
231
|
-
}
|
|
239
|
+
})
|
|
232
240
|
]
|
|
233
|
-
}, i
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
241
|
+
}, i))
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
]
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/presentation/components/ToolResultRenderer.tsx
|
|
250
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
251
|
+
"use client";
|
|
252
|
+
function isPresentationToolResult(result) {
|
|
253
|
+
return typeof result === "object" && result !== null && "presentationKey" in result && typeof result.presentationKey === "string";
|
|
254
|
+
}
|
|
255
|
+
function isFormToolResult(result) {
|
|
256
|
+
return typeof result === "object" && result !== null && "formKey" in result && typeof result.formKey === "string";
|
|
257
|
+
}
|
|
258
|
+
function ToolResultRenderer({
|
|
259
|
+
toolName,
|
|
260
|
+
result,
|
|
261
|
+
presentationRenderer,
|
|
262
|
+
formRenderer,
|
|
263
|
+
showRawFallback = true
|
|
264
|
+
}) {
|
|
265
|
+
if (result === undefined || result === null) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
if (isPresentationToolResult(result) && presentationRenderer) {
|
|
269
|
+
const rendered = presentationRenderer(result.presentationKey, result.data);
|
|
270
|
+
if (rendered != null) {
|
|
271
|
+
return /* @__PURE__ */ jsx3("div", {
|
|
272
|
+
className: "border-border bg-background/50 mt-2 rounded-md border p-3",
|
|
273
|
+
children: rendered
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (isFormToolResult(result) && formRenderer) {
|
|
278
|
+
const rendered = formRenderer(result.formKey, result.defaultValues);
|
|
279
|
+
if (rendered != null) {
|
|
280
|
+
return /* @__PURE__ */ jsx3("div", {
|
|
281
|
+
className: "border-border bg-background/50 mt-2 rounded-md border p-3",
|
|
282
|
+
children: rendered
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!showRawFallback) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return /* @__PURE__ */ jsxs3("div", {
|
|
290
|
+
children: [
|
|
291
|
+
/* @__PURE__ */ jsx3("span", {
|
|
292
|
+
className: "text-muted-foreground font-medium",
|
|
293
|
+
children: "Output:"
|
|
294
|
+
}),
|
|
295
|
+
/* @__PURE__ */ jsx3("pre", {
|
|
296
|
+
className: "bg-background mt-1 overflow-x-auto rounded p-2 text-xs",
|
|
297
|
+
children: typeof result === "object" ? JSON.stringify(result, null, 2) : String(result)
|
|
298
|
+
})
|
|
237
299
|
]
|
|
238
|
-
}
|
|
300
|
+
});
|
|
239
301
|
}
|
|
240
302
|
|
|
241
303
|
// src/presentation/components/ChatMessage.tsx
|
|
242
|
-
import {
|
|
304
|
+
import { jsx as jsx4, jsxs as jsxs4, Fragment } from "react/jsx-runtime";
|
|
243
305
|
"use client";
|
|
244
306
|
function extractCodeBlocks(content) {
|
|
245
307
|
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
@@ -262,33 +324,33 @@ function renderInlineMarkdown(text) {
|
|
|
262
324
|
let key = 0;
|
|
263
325
|
while ((match = linkRegex.exec(text)) !== null) {
|
|
264
326
|
if (match.index > lastIndex) {
|
|
265
|
-
parts.push(/* @__PURE__ */
|
|
327
|
+
parts.push(/* @__PURE__ */ jsx4("span", {
|
|
266
328
|
children: text.slice(lastIndex, match.index)
|
|
267
|
-
}, key
|
|
329
|
+
}, key++));
|
|
268
330
|
}
|
|
269
|
-
parts.push(/* @__PURE__ */
|
|
331
|
+
parts.push(/* @__PURE__ */ jsx4("a", {
|
|
270
332
|
href: match[2],
|
|
271
333
|
target: "_blank",
|
|
272
334
|
rel: "noopener noreferrer",
|
|
273
335
|
className: "text-primary underline hover:no-underline",
|
|
274
336
|
children: match[1]
|
|
275
|
-
}, key
|
|
337
|
+
}, key++));
|
|
276
338
|
lastIndex = match.index + match[0].length;
|
|
277
339
|
}
|
|
278
340
|
if (lastIndex < text.length) {
|
|
279
|
-
parts.push(/* @__PURE__ */
|
|
341
|
+
parts.push(/* @__PURE__ */ jsx4("span", {
|
|
280
342
|
children: text.slice(lastIndex)
|
|
281
|
-
}, key
|
|
343
|
+
}, key++));
|
|
282
344
|
}
|
|
283
345
|
return parts.length > 0 ? parts : [text];
|
|
284
346
|
}
|
|
285
347
|
function MessageContent({ content }) {
|
|
286
348
|
const codeBlocks = extractCodeBlocks(content);
|
|
287
349
|
if (codeBlocks.length === 0) {
|
|
288
|
-
return /* @__PURE__ */
|
|
350
|
+
return /* @__PURE__ */ jsx4("p", {
|
|
289
351
|
className: "whitespace-pre-wrap",
|
|
290
352
|
children: renderInlineMarkdown(content)
|
|
291
|
-
}
|
|
353
|
+
});
|
|
292
354
|
}
|
|
293
355
|
let remaining = content;
|
|
294
356
|
const parts = [];
|
|
@@ -296,33 +358,40 @@ function MessageContent({ content }) {
|
|
|
296
358
|
for (const block of codeBlocks) {
|
|
297
359
|
const [before, after] = remaining.split(block.raw);
|
|
298
360
|
if (before) {
|
|
299
|
-
parts.push(/* @__PURE__ */
|
|
361
|
+
parts.push(/* @__PURE__ */ jsx4("p", {
|
|
300
362
|
className: "whitespace-pre-wrap",
|
|
301
363
|
children: renderInlineMarkdown(before.trim())
|
|
302
|
-
}, key
|
|
364
|
+
}, key++));
|
|
303
365
|
}
|
|
304
|
-
parts.push(/* @__PURE__ */
|
|
366
|
+
parts.push(/* @__PURE__ */ jsx4(CodePreview, {
|
|
305
367
|
code: block.code,
|
|
306
368
|
language: block.language,
|
|
307
369
|
className: "my-2"
|
|
308
|
-
}, key
|
|
370
|
+
}, key++));
|
|
309
371
|
remaining = after ?? "";
|
|
310
372
|
}
|
|
311
373
|
if (remaining.trim()) {
|
|
312
|
-
parts.push(/* @__PURE__ */
|
|
374
|
+
parts.push(/* @__PURE__ */ jsx4("p", {
|
|
313
375
|
className: "whitespace-pre-wrap",
|
|
314
376
|
children: renderInlineMarkdown(remaining.trim())
|
|
315
|
-
}, key
|
|
377
|
+
}, key++));
|
|
316
378
|
}
|
|
317
|
-
return /* @__PURE__ */
|
|
379
|
+
return /* @__PURE__ */ jsx4(Fragment, {
|
|
318
380
|
children: parts
|
|
319
|
-
}
|
|
381
|
+
});
|
|
320
382
|
}
|
|
321
383
|
function ChatMessage({
|
|
322
384
|
message,
|
|
323
385
|
className,
|
|
324
386
|
showCopy = true,
|
|
325
|
-
showAvatar = true
|
|
387
|
+
showAvatar = true,
|
|
388
|
+
selectable = false,
|
|
389
|
+
selected = false,
|
|
390
|
+
onSelect,
|
|
391
|
+
editable = false,
|
|
392
|
+
onEdit,
|
|
393
|
+
presentationRenderer,
|
|
394
|
+
formRenderer
|
|
326
395
|
}) {
|
|
327
396
|
const [copied, setCopied] = React3.useState(false);
|
|
328
397
|
const isUser = message.role === "user";
|
|
@@ -333,185 +402,262 @@ function ChatMessage({
|
|
|
333
402
|
setCopied(true);
|
|
334
403
|
setTimeout(() => setCopied(false), 2000);
|
|
335
404
|
}, [message.content]);
|
|
336
|
-
|
|
405
|
+
const handleSelectChange = React3.useCallback((checked) => {
|
|
406
|
+
if (checked !== "indeterminate")
|
|
407
|
+
onSelect?.(message.id);
|
|
408
|
+
}, [message.id, onSelect]);
|
|
409
|
+
const [isEditing, setIsEditing] = React3.useState(false);
|
|
410
|
+
const [editContent, setEditContent] = React3.useState(message.content);
|
|
411
|
+
React3.useEffect(() => {
|
|
412
|
+
setEditContent(message.content);
|
|
413
|
+
}, [message.content]);
|
|
414
|
+
const handleStartEdit = React3.useCallback(() => {
|
|
415
|
+
setEditContent(message.content);
|
|
416
|
+
setIsEditing(true);
|
|
417
|
+
}, [message.content]);
|
|
418
|
+
const handleSaveEdit = React3.useCallback(async () => {
|
|
419
|
+
const trimmed = editContent.trim();
|
|
420
|
+
if (trimmed !== message.content) {
|
|
421
|
+
await onEdit?.(message.id, trimmed);
|
|
422
|
+
}
|
|
423
|
+
setIsEditing(false);
|
|
424
|
+
}, [editContent, message.id, message.content, onEdit]);
|
|
425
|
+
const handleCancelEdit = React3.useCallback(() => {
|
|
426
|
+
setEditContent(message.content);
|
|
427
|
+
setIsEditing(false);
|
|
428
|
+
}, [message.content]);
|
|
429
|
+
return /* @__PURE__ */ jsxs4("div", {
|
|
337
430
|
className: cn3("group flex gap-3", isUser && "flex-row-reverse", className),
|
|
338
431
|
children: [
|
|
339
|
-
|
|
432
|
+
selectable && /* @__PURE__ */ jsx4("div", {
|
|
433
|
+
className: cn3("flex shrink-0 items-start pt-1", "opacity-0 transition-opacity group-hover:opacity-100"),
|
|
434
|
+
children: /* @__PURE__ */ jsx4(Checkbox, {
|
|
435
|
+
checked: selected,
|
|
436
|
+
onCheckedChange: handleSelectChange,
|
|
437
|
+
"aria-label": selected ? "Deselect message" : "Select message"
|
|
438
|
+
})
|
|
439
|
+
}),
|
|
440
|
+
showAvatar && /* @__PURE__ */ jsx4(Avatar, {
|
|
340
441
|
className: "h-8 w-8 shrink-0",
|
|
341
|
-
children: /* @__PURE__ */
|
|
442
|
+
children: /* @__PURE__ */ jsx4(AvatarFallback, {
|
|
342
443
|
className: cn3(isUser ? "bg-primary text-primary-foreground" : "bg-muted"),
|
|
343
|
-
children: isUser ? /* @__PURE__ */
|
|
444
|
+
children: isUser ? /* @__PURE__ */ jsx4(User, {
|
|
344
445
|
className: "h-4 w-4"
|
|
345
|
-
}
|
|
446
|
+
}) : /* @__PURE__ */ jsx4(Bot, {
|
|
346
447
|
className: "h-4 w-4"
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
/* @__PURE__ */
|
|
448
|
+
})
|
|
449
|
+
})
|
|
450
|
+
}),
|
|
451
|
+
/* @__PURE__ */ jsxs4("div", {
|
|
351
452
|
className: cn3("flex max-w-[80%] flex-col gap-1", isUser && "items-end"),
|
|
352
453
|
children: [
|
|
353
|
-
/* @__PURE__ */
|
|
454
|
+
/* @__PURE__ */ jsx4("div", {
|
|
354
455
|
className: cn3("rounded-2xl px-4 py-2", isUser ? "bg-primary text-primary-foreground" : "bg-muted text-foreground", isError && "border-destructive bg-destructive/10 border"),
|
|
355
|
-
children: isError && message.error ? /* @__PURE__ */
|
|
456
|
+
children: isError && message.error ? /* @__PURE__ */ jsxs4("div", {
|
|
356
457
|
className: "flex items-start gap-2",
|
|
357
458
|
children: [
|
|
358
|
-
/* @__PURE__ */
|
|
459
|
+
/* @__PURE__ */ jsx4(AlertCircle, {
|
|
359
460
|
className: "text-destructive mt-0.5 h-4 w-4 shrink-0"
|
|
360
|
-
}
|
|
361
|
-
/* @__PURE__ */
|
|
461
|
+
}),
|
|
462
|
+
/* @__PURE__ */ jsxs4("div", {
|
|
362
463
|
children: [
|
|
363
|
-
/* @__PURE__ */
|
|
464
|
+
/* @__PURE__ */ jsx4("p", {
|
|
364
465
|
className: "text-destructive font-medium",
|
|
365
466
|
children: message.error.code
|
|
366
|
-
}
|
|
367
|
-
/* @__PURE__ */
|
|
467
|
+
}),
|
|
468
|
+
/* @__PURE__ */ jsx4("p", {
|
|
368
469
|
className: "text-muted-foreground text-sm",
|
|
369
470
|
children: message.error.message
|
|
370
|
-
}
|
|
471
|
+
})
|
|
371
472
|
]
|
|
372
|
-
}
|
|
473
|
+
})
|
|
373
474
|
]
|
|
374
|
-
}
|
|
475
|
+
}) : isEditing ? /* @__PURE__ */ jsxs4("div", {
|
|
375
476
|
className: "flex flex-col gap-2",
|
|
376
477
|
children: [
|
|
377
|
-
/* @__PURE__ */
|
|
478
|
+
/* @__PURE__ */ jsx4("textarea", {
|
|
479
|
+
value: editContent,
|
|
480
|
+
onChange: (e) => setEditContent(e.target.value),
|
|
481
|
+
className: "bg-background/50 min-h-[80px] w-full resize-y rounded-md border px-3 py-2 text-sm",
|
|
482
|
+
rows: 4,
|
|
483
|
+
autoFocus: true
|
|
484
|
+
}),
|
|
485
|
+
/* @__PURE__ */ jsxs4("div", {
|
|
486
|
+
className: "flex gap-2",
|
|
487
|
+
children: [
|
|
488
|
+
/* @__PURE__ */ jsxs4(Button2, {
|
|
489
|
+
variant: "default",
|
|
490
|
+
size: "sm",
|
|
491
|
+
onPress: handleSaveEdit,
|
|
492
|
+
"aria-label": "Save edit",
|
|
493
|
+
children: [
|
|
494
|
+
/* @__PURE__ */ jsx4(Check2, {
|
|
495
|
+
className: "h-3 w-3"
|
|
496
|
+
}),
|
|
497
|
+
"Save"
|
|
498
|
+
]
|
|
499
|
+
}),
|
|
500
|
+
/* @__PURE__ */ jsxs4(Button2, {
|
|
501
|
+
variant: "ghost",
|
|
502
|
+
size: "sm",
|
|
503
|
+
onPress: handleCancelEdit,
|
|
504
|
+
"aria-label": "Cancel edit",
|
|
505
|
+
children: [
|
|
506
|
+
/* @__PURE__ */ jsx4(X, {
|
|
507
|
+
className: "h-3 w-3"
|
|
508
|
+
}),
|
|
509
|
+
"Cancel"
|
|
510
|
+
]
|
|
511
|
+
})
|
|
512
|
+
]
|
|
513
|
+
})
|
|
514
|
+
]
|
|
515
|
+
}) : isStreaming && !message.content ? /* @__PURE__ */ jsxs4("div", {
|
|
516
|
+
className: "flex flex-col gap-2",
|
|
517
|
+
children: [
|
|
518
|
+
/* @__PURE__ */ jsx4(Skeleton, {
|
|
378
519
|
className: "h-4 w-48"
|
|
379
|
-
}
|
|
380
|
-
/* @__PURE__ */
|
|
520
|
+
}),
|
|
521
|
+
/* @__PURE__ */ jsx4(Skeleton, {
|
|
381
522
|
className: "h-4 w-32"
|
|
382
|
-
}
|
|
523
|
+
})
|
|
383
524
|
]
|
|
384
|
-
}
|
|
525
|
+
}) : /* @__PURE__ */ jsx4(MessageContent, {
|
|
385
526
|
content: message.content
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
/* @__PURE__ */
|
|
527
|
+
})
|
|
528
|
+
}),
|
|
529
|
+
/* @__PURE__ */ jsxs4("div", {
|
|
389
530
|
className: cn3("flex items-center gap-2 text-xs", "text-muted-foreground opacity-0 transition-opacity", "group-hover:opacity-100"),
|
|
390
531
|
children: [
|
|
391
|
-
/* @__PURE__ */
|
|
532
|
+
/* @__PURE__ */ jsx4("span", {
|
|
392
533
|
children: new Date(message.createdAt).toLocaleTimeString([], {
|
|
393
534
|
hour: "2-digit",
|
|
394
535
|
minute: "2-digit"
|
|
395
536
|
})
|
|
396
|
-
}
|
|
397
|
-
message.usage && /* @__PURE__ */
|
|
537
|
+
}),
|
|
538
|
+
message.usage && /* @__PURE__ */ jsxs4("span", {
|
|
398
539
|
children: [
|
|
399
540
|
message.usage.inputTokens + message.usage.outputTokens,
|
|
400
541
|
" tokens"
|
|
401
542
|
]
|
|
402
|
-
}
|
|
403
|
-
showCopy && !isUser && message.content && /* @__PURE__ */
|
|
543
|
+
}),
|
|
544
|
+
showCopy && !isUser && message.content && /* @__PURE__ */ jsx4(Button2, {
|
|
404
545
|
variant: "ghost",
|
|
405
546
|
size: "sm",
|
|
406
547
|
className: "h-6 w-6 p-0",
|
|
407
548
|
onPress: handleCopy,
|
|
408
549
|
"aria-label": copied ? "Copied" : "Copy message",
|
|
409
|
-
children: copied ? /* @__PURE__ */
|
|
550
|
+
children: copied ? /* @__PURE__ */ jsx4(Check2, {
|
|
410
551
|
className: "h-3 w-3"
|
|
411
|
-
}
|
|
552
|
+
}) : /* @__PURE__ */ jsx4(Copy2, {
|
|
553
|
+
className: "h-3 w-3"
|
|
554
|
+
})
|
|
555
|
+
}),
|
|
556
|
+
editable && isUser && !isEditing && /* @__PURE__ */ jsx4(Button2, {
|
|
557
|
+
variant: "ghost",
|
|
558
|
+
size: "sm",
|
|
559
|
+
className: "h-6 w-6 p-0",
|
|
560
|
+
onPress: handleStartEdit,
|
|
561
|
+
"aria-label": "Edit message",
|
|
562
|
+
children: /* @__PURE__ */ jsx4(Pencil, {
|
|
412
563
|
className: "h-3 w-3"
|
|
413
|
-
}
|
|
414
|
-
}
|
|
564
|
+
})
|
|
565
|
+
})
|
|
415
566
|
]
|
|
416
|
-
}
|
|
417
|
-
message.reasoning && /* @__PURE__ */
|
|
567
|
+
}),
|
|
568
|
+
message.reasoning && /* @__PURE__ */ jsxs4("details", {
|
|
418
569
|
className: "text-muted-foreground mt-2 text-sm",
|
|
419
570
|
children: [
|
|
420
|
-
/* @__PURE__ */
|
|
571
|
+
/* @__PURE__ */ jsx4("summary", {
|
|
421
572
|
className: "cursor-pointer hover:underline",
|
|
422
573
|
children: "View reasoning"
|
|
423
|
-
}
|
|
424
|
-
/* @__PURE__ */
|
|
574
|
+
}),
|
|
575
|
+
/* @__PURE__ */ jsx4("div", {
|
|
425
576
|
className: "bg-muted mt-1 rounded-md p-2",
|
|
426
|
-
children: /* @__PURE__ */
|
|
577
|
+
children: /* @__PURE__ */ jsx4("p", {
|
|
427
578
|
className: "whitespace-pre-wrap",
|
|
428
579
|
children: message.reasoning
|
|
429
|
-
}
|
|
430
|
-
}
|
|
580
|
+
})
|
|
581
|
+
})
|
|
431
582
|
]
|
|
432
|
-
}
|
|
433
|
-
message.sources && message.sources.length > 0 && /* @__PURE__ */
|
|
583
|
+
}),
|
|
584
|
+
message.sources && message.sources.length > 0 && /* @__PURE__ */ jsx4("div", {
|
|
434
585
|
className: "mt-2 flex flex-wrap gap-2",
|
|
435
|
-
children: message.sources.map((source) => /* @__PURE__ */
|
|
586
|
+
children: message.sources.map((source) => /* @__PURE__ */ jsxs4("a", {
|
|
436
587
|
href: source.url ?? "#",
|
|
437
588
|
target: "_blank",
|
|
438
589
|
rel: "noopener noreferrer",
|
|
439
590
|
className: "text-muted-foreground hover:text-foreground bg-muted inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors",
|
|
440
591
|
children: [
|
|
441
|
-
/* @__PURE__ */
|
|
592
|
+
/* @__PURE__ */ jsx4(ExternalLink, {
|
|
442
593
|
className: "h-3 w-3"
|
|
443
|
-
}
|
|
594
|
+
}),
|
|
444
595
|
source.title || source.url || source.id
|
|
445
596
|
]
|
|
446
|
-
}, source.id
|
|
447
|
-
}
|
|
448
|
-
message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */
|
|
597
|
+
}, source.id))
|
|
598
|
+
}),
|
|
599
|
+
message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsx4("div", {
|
|
449
600
|
className: "mt-2 space-y-2",
|
|
450
|
-
children: message.toolCalls.map((tc) => /* @__PURE__ */
|
|
601
|
+
children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxs4("details", {
|
|
451
602
|
className: "bg-muted border-border rounded-md border",
|
|
452
603
|
children: [
|
|
453
|
-
/* @__PURE__ */
|
|
604
|
+
/* @__PURE__ */ jsxs4("summary", {
|
|
454
605
|
className: "flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium",
|
|
455
606
|
children: [
|
|
456
|
-
/* @__PURE__ */
|
|
607
|
+
/* @__PURE__ */ jsx4(Wrench, {
|
|
457
608
|
className: "text-muted-foreground h-4 w-4"
|
|
458
|
-
}
|
|
609
|
+
}),
|
|
459
610
|
tc.name,
|
|
460
|
-
/* @__PURE__ */
|
|
611
|
+
/* @__PURE__ */ jsx4("span", {
|
|
461
612
|
className: cn3("ml-auto rounded px-1.5 py-0.5 text-xs", tc.status === "completed" && "bg-green-500/20 text-green-700 dark:text-green-400", tc.status === "error" && "bg-destructive/20 text-destructive", tc.status === "running" && "bg-blue-500/20 text-blue-700 dark:text-blue-400"),
|
|
462
613
|
children: tc.status
|
|
463
|
-
}
|
|
614
|
+
})
|
|
464
615
|
]
|
|
465
|
-
}
|
|
466
|
-
/* @__PURE__ */
|
|
616
|
+
}),
|
|
617
|
+
/* @__PURE__ */ jsxs4("div", {
|
|
467
618
|
className: "border-border border-t px-3 py-2 text-xs",
|
|
468
619
|
children: [
|
|
469
|
-
Object.keys(tc.args).length > 0 && /* @__PURE__ */
|
|
620
|
+
Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxs4("div", {
|
|
470
621
|
className: "mb-2",
|
|
471
622
|
children: [
|
|
472
|
-
/* @__PURE__ */
|
|
623
|
+
/* @__PURE__ */ jsx4("span", {
|
|
473
624
|
className: "text-muted-foreground font-medium",
|
|
474
625
|
children: "Input:"
|
|
475
|
-
}
|
|
476
|
-
/* @__PURE__ */
|
|
626
|
+
}),
|
|
627
|
+
/* @__PURE__ */ jsx4("pre", {
|
|
477
628
|
className: "bg-background mt-1 overflow-x-auto rounded p-2",
|
|
478
629
|
children: JSON.stringify(tc.args, null, 2)
|
|
479
|
-
}
|
|
630
|
+
})
|
|
480
631
|
]
|
|
481
|
-
}
|
|
482
|
-
tc.result !== undefined && /* @__PURE__ */
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
children: typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)
|
|
491
|
-
}, undefined, false, undefined, this)
|
|
492
|
-
]
|
|
493
|
-
}, undefined, true, undefined, this),
|
|
494
|
-
tc.error && /* @__PURE__ */ jsxDEV3("p", {
|
|
632
|
+
}),
|
|
633
|
+
tc.result !== undefined && /* @__PURE__ */ jsx4(ToolResultRenderer, {
|
|
634
|
+
toolName: tc.name,
|
|
635
|
+
result: tc.result,
|
|
636
|
+
presentationRenderer,
|
|
637
|
+
formRenderer,
|
|
638
|
+
showRawFallback: true
|
|
639
|
+
}),
|
|
640
|
+
tc.error && /* @__PURE__ */ jsx4("p", {
|
|
495
641
|
className: "text-destructive mt-1",
|
|
496
642
|
children: tc.error
|
|
497
|
-
}
|
|
643
|
+
})
|
|
498
644
|
]
|
|
499
|
-
}
|
|
645
|
+
})
|
|
500
646
|
]
|
|
501
|
-
}, tc.id
|
|
502
|
-
}
|
|
647
|
+
}, tc.id))
|
|
648
|
+
})
|
|
503
649
|
]
|
|
504
|
-
}
|
|
650
|
+
})
|
|
505
651
|
]
|
|
506
|
-
}
|
|
652
|
+
});
|
|
507
653
|
}
|
|
508
654
|
// src/presentation/components/ChatInput.tsx
|
|
509
655
|
import * as React4 from "react";
|
|
510
656
|
import { cn as cn4 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
511
657
|
import { Textarea } from "@contractspec/lib.design-system";
|
|
512
658
|
import { Button as Button3 } from "@contractspec/lib.design-system";
|
|
513
|
-
import { Send, Paperclip, X, Loader2, FileText, Code } from "lucide-react";
|
|
514
|
-
import {
|
|
659
|
+
import { Send, Paperclip, X as X2, Loader2, FileText, Code } from "lucide-react";
|
|
660
|
+
import { jsx as jsx5, jsxs as jsxs5, Fragment as Fragment2 } from "react/jsx-runtime";
|
|
515
661
|
"use client";
|
|
516
662
|
function ChatInput({
|
|
517
663
|
onSend,
|
|
@@ -577,42 +723,42 @@ function ChatInput({
|
|
|
577
723
|
const removeAttachment = React4.useCallback((id) => {
|
|
578
724
|
setAttachments((prev) => prev.filter((a) => a.id !== id));
|
|
579
725
|
}, []);
|
|
580
|
-
return /* @__PURE__ */
|
|
726
|
+
return /* @__PURE__ */ jsxs5("div", {
|
|
581
727
|
className: cn4("flex flex-col gap-2", className),
|
|
582
728
|
children: [
|
|
583
|
-
attachments.length > 0 && /* @__PURE__ */
|
|
729
|
+
attachments.length > 0 && /* @__PURE__ */ jsx5("div", {
|
|
584
730
|
className: "flex flex-wrap gap-2",
|
|
585
|
-
children: attachments.map((attachment) => /* @__PURE__ */
|
|
731
|
+
children: attachments.map((attachment) => /* @__PURE__ */ jsxs5("div", {
|
|
586
732
|
className: cn4("flex items-center gap-1.5 rounded-md px-2 py-1", "bg-muted text-muted-foreground text-sm"),
|
|
587
733
|
children: [
|
|
588
|
-
attachment.type === "code" ? /* @__PURE__ */
|
|
734
|
+
attachment.type === "code" ? /* @__PURE__ */ jsx5(Code, {
|
|
589
735
|
className: "h-3.5 w-3.5"
|
|
590
|
-
}
|
|
736
|
+
}) : /* @__PURE__ */ jsx5(FileText, {
|
|
591
737
|
className: "h-3.5 w-3.5"
|
|
592
|
-
}
|
|
593
|
-
/* @__PURE__ */
|
|
738
|
+
}),
|
|
739
|
+
/* @__PURE__ */ jsx5("span", {
|
|
594
740
|
className: "max-w-[150px] truncate",
|
|
595
741
|
children: attachment.name
|
|
596
|
-
}
|
|
597
|
-
/* @__PURE__ */
|
|
742
|
+
}),
|
|
743
|
+
/* @__PURE__ */ jsx5("button", {
|
|
598
744
|
type: "button",
|
|
599
745
|
onClick: () => removeAttachment(attachment.id),
|
|
600
746
|
className: "hover:text-foreground",
|
|
601
747
|
"aria-label": `Remove ${attachment.name}`,
|
|
602
|
-
children: /* @__PURE__ */
|
|
748
|
+
children: /* @__PURE__ */ jsx5(X2, {
|
|
603
749
|
className: "h-3.5 w-3.5"
|
|
604
|
-
}
|
|
605
|
-
}
|
|
750
|
+
})
|
|
751
|
+
})
|
|
606
752
|
]
|
|
607
|
-
}, attachment.id
|
|
608
|
-
}
|
|
609
|
-
/* @__PURE__ */
|
|
753
|
+
}, attachment.id))
|
|
754
|
+
}),
|
|
755
|
+
/* @__PURE__ */ jsxs5("form", {
|
|
610
756
|
onSubmit: handleSubmit,
|
|
611
757
|
className: "flex items-end gap-2",
|
|
612
758
|
children: [
|
|
613
|
-
showAttachments && /* @__PURE__ */
|
|
759
|
+
showAttachments && /* @__PURE__ */ jsxs5(Fragment2, {
|
|
614
760
|
children: [
|
|
615
|
-
/* @__PURE__ */
|
|
761
|
+
/* @__PURE__ */ jsx5("input", {
|
|
616
762
|
ref: fileInputRef,
|
|
617
763
|
type: "file",
|
|
618
764
|
multiple: true,
|
|
@@ -620,23 +766,23 @@ function ChatInput({
|
|
|
620
766
|
onChange: handleFileSelect,
|
|
621
767
|
className: "hidden",
|
|
622
768
|
"aria-label": "Attach files"
|
|
623
|
-
}
|
|
624
|
-
/* @__PURE__ */
|
|
769
|
+
}),
|
|
770
|
+
/* @__PURE__ */ jsx5(Button3, {
|
|
625
771
|
type: "button",
|
|
626
772
|
variant: "ghost",
|
|
627
773
|
size: "sm",
|
|
628
774
|
onPress: () => fileInputRef.current?.click(),
|
|
629
775
|
disabled: disabled || attachments.length >= maxAttachments,
|
|
630
776
|
"aria-label": "Attach files",
|
|
631
|
-
children: /* @__PURE__ */
|
|
777
|
+
children: /* @__PURE__ */ jsx5(Paperclip, {
|
|
632
778
|
className: "h-4 w-4"
|
|
633
|
-
}
|
|
634
|
-
}
|
|
779
|
+
})
|
|
780
|
+
})
|
|
635
781
|
]
|
|
636
|
-
}
|
|
637
|
-
/* @__PURE__ */
|
|
782
|
+
}),
|
|
783
|
+
/* @__PURE__ */ jsx5("div", {
|
|
638
784
|
className: "relative flex-1",
|
|
639
|
-
children: /* @__PURE__ */
|
|
785
|
+
children: /* @__PURE__ */ jsx5(Textarea, {
|
|
640
786
|
value: content,
|
|
641
787
|
onChange: (e) => setContent(e.target.value),
|
|
642
788
|
onKeyDown: handleKeyDown,
|
|
@@ -645,32 +791,418 @@ function ChatInput({
|
|
|
645
791
|
className: cn4("max-h-[200px] min-h-[44px] resize-none pr-12", "focus-visible:ring-1"),
|
|
646
792
|
rows: 1,
|
|
647
793
|
"aria-label": "Chat message"
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
/* @__PURE__ */
|
|
794
|
+
})
|
|
795
|
+
}),
|
|
796
|
+
/* @__PURE__ */ jsx5(Button3, {
|
|
651
797
|
type: "submit",
|
|
652
798
|
disabled: !canSend || disabled || isLoading,
|
|
653
799
|
size: "sm",
|
|
654
800
|
"aria-label": isLoading ? "Sending..." : "Send message",
|
|
655
|
-
children: isLoading ? /* @__PURE__ */
|
|
801
|
+
children: isLoading ? /* @__PURE__ */ jsx5(Loader2, {
|
|
656
802
|
className: "h-4 w-4 animate-spin"
|
|
657
|
-
}
|
|
803
|
+
}) : /* @__PURE__ */ jsx5(Send, {
|
|
658
804
|
className: "h-4 w-4"
|
|
659
|
-
}
|
|
660
|
-
}
|
|
805
|
+
})
|
|
806
|
+
})
|
|
661
807
|
]
|
|
662
|
-
}
|
|
663
|
-
/* @__PURE__ */
|
|
808
|
+
}),
|
|
809
|
+
/* @__PURE__ */ jsx5("p", {
|
|
664
810
|
className: "text-muted-foreground text-xs",
|
|
665
811
|
children: "Press Enter to send, Shift+Enter for new line"
|
|
666
|
-
}
|
|
812
|
+
})
|
|
667
813
|
]
|
|
668
|
-
}
|
|
814
|
+
});
|
|
669
815
|
}
|
|
670
|
-
// src/presentation/components/
|
|
816
|
+
// src/presentation/components/ChatExportToolbar.tsx
|
|
671
817
|
import * as React5 from "react";
|
|
672
|
-
import {
|
|
818
|
+
import { Download as Download2, FileText as FileText2, Copy as Copy3, Check as Check3, Plus, GitFork } from "lucide-react";
|
|
673
819
|
import { Button as Button4 } from "@contractspec/lib.design-system";
|
|
820
|
+
import {
|
|
821
|
+
DropdownMenu,
|
|
822
|
+
DropdownMenuContent,
|
|
823
|
+
DropdownMenuItem,
|
|
824
|
+
DropdownMenuSeparator,
|
|
825
|
+
DropdownMenuTrigger
|
|
826
|
+
} from "@contractspec/lib.ui-kit-web/ui/dropdown-menu";
|
|
827
|
+
|
|
828
|
+
// src/core/export-formatters.ts
|
|
829
|
+
function formatTimestamp(date) {
|
|
830
|
+
return date.toLocaleTimeString([], {
|
|
831
|
+
hour: "2-digit",
|
|
832
|
+
minute: "2-digit"
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
function toIsoString(date) {
|
|
836
|
+
return date.toISOString();
|
|
837
|
+
}
|
|
838
|
+
function messageToJsonSerializable(msg) {
|
|
839
|
+
return {
|
|
840
|
+
id: msg.id,
|
|
841
|
+
conversationId: msg.conversationId,
|
|
842
|
+
role: msg.role,
|
|
843
|
+
content: msg.content,
|
|
844
|
+
status: msg.status,
|
|
845
|
+
createdAt: toIsoString(msg.createdAt),
|
|
846
|
+
updatedAt: toIsoString(msg.updatedAt),
|
|
847
|
+
...msg.attachments && { attachments: msg.attachments },
|
|
848
|
+
...msg.codeBlocks && { codeBlocks: msg.codeBlocks },
|
|
849
|
+
...msg.toolCalls && { toolCalls: msg.toolCalls },
|
|
850
|
+
...msg.sources && { sources: msg.sources },
|
|
851
|
+
...msg.reasoning && { reasoning: msg.reasoning },
|
|
852
|
+
...msg.usage && { usage: msg.usage },
|
|
853
|
+
...msg.error && { error: msg.error },
|
|
854
|
+
...msg.metadata && { metadata: msg.metadata }
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
function formatSourcesMarkdown(sources) {
|
|
858
|
+
if (sources.length === 0)
|
|
859
|
+
return "";
|
|
860
|
+
return `
|
|
861
|
+
|
|
862
|
+
**Sources:**
|
|
863
|
+
` + sources.map((s) => `- [${s.title}](${s.url ?? "#"})`).join(`
|
|
864
|
+
`);
|
|
865
|
+
}
|
|
866
|
+
function formatSourcesTxt(sources) {
|
|
867
|
+
if (sources.length === 0)
|
|
868
|
+
return "";
|
|
869
|
+
return `
|
|
870
|
+
|
|
871
|
+
Sources:
|
|
872
|
+
` + sources.map((s) => `- ${s.title}${s.url ? ` - ${s.url}` : ""}`).join(`
|
|
873
|
+
`);
|
|
874
|
+
}
|
|
875
|
+
function formatToolCallsMarkdown(toolCalls) {
|
|
876
|
+
if (toolCalls.length === 0)
|
|
877
|
+
return "";
|
|
878
|
+
return `
|
|
879
|
+
|
|
880
|
+
**Tool calls:**
|
|
881
|
+
` + toolCalls.map((tc) => `**${tc.name}** (${tc.status})
|
|
882
|
+
\`\`\`json
|
|
883
|
+
${JSON.stringify(tc.args, null, 2)}
|
|
884
|
+
\`\`\`` + (tc.result !== undefined ? `
|
|
885
|
+
Output:
|
|
886
|
+
\`\`\`json
|
|
887
|
+
${typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)}
|
|
888
|
+
\`\`\`` : "") + (tc.error ? `
|
|
889
|
+
Error: ${tc.error}` : "")).join(`
|
|
890
|
+
|
|
891
|
+
`);
|
|
892
|
+
}
|
|
893
|
+
function formatToolCallsTxt(toolCalls) {
|
|
894
|
+
if (toolCalls.length === 0)
|
|
895
|
+
return "";
|
|
896
|
+
return `
|
|
897
|
+
|
|
898
|
+
Tool calls:
|
|
899
|
+
` + toolCalls.map((tc) => `- ${tc.name} (${tc.status}): ${JSON.stringify(tc.args)}` + (tc.result !== undefined ? ` -> ${typeof tc.result === "object" ? JSON.stringify(tc.result) : String(tc.result)}` : "") + (tc.error ? ` [Error: ${tc.error}]` : "")).join(`
|
|
900
|
+
`);
|
|
901
|
+
}
|
|
902
|
+
function formatUsage(usage) {
|
|
903
|
+
const total = usage.inputTokens + usage.outputTokens;
|
|
904
|
+
return ` (${total} tokens)`;
|
|
905
|
+
}
|
|
906
|
+
function formatMessagesAsMarkdown(messages) {
|
|
907
|
+
const parts = [];
|
|
908
|
+
for (const msg of messages) {
|
|
909
|
+
const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
|
|
910
|
+
const header = `## ${roleLabel}`;
|
|
911
|
+
const timestamp = `*${formatTimestamp(msg.createdAt)}*`;
|
|
912
|
+
const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
|
|
913
|
+
const meta = `${timestamp}${usageSuffix}
|
|
914
|
+
|
|
915
|
+
`;
|
|
916
|
+
let body = msg.content;
|
|
917
|
+
if (msg.error) {
|
|
918
|
+
body += `
|
|
919
|
+
|
|
920
|
+
**Error:** ${msg.error.code} - ${msg.error.message}`;
|
|
921
|
+
}
|
|
922
|
+
if (msg.reasoning) {
|
|
923
|
+
body += `
|
|
924
|
+
|
|
925
|
+
> **Reasoning:**
|
|
926
|
+
> ${msg.reasoning.replace(/\n/g, `
|
|
927
|
+
> `)}`;
|
|
928
|
+
}
|
|
929
|
+
body += formatSourcesMarkdown(msg.sources ?? []);
|
|
930
|
+
body += formatToolCallsMarkdown(msg.toolCalls ?? []);
|
|
931
|
+
parts.push(`${header}
|
|
932
|
+
|
|
933
|
+
${meta}${body}`);
|
|
934
|
+
}
|
|
935
|
+
return parts.join(`
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
`);
|
|
940
|
+
}
|
|
941
|
+
function formatMessagesAsTxt(messages) {
|
|
942
|
+
const parts = [];
|
|
943
|
+
for (const msg of messages) {
|
|
944
|
+
const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
|
|
945
|
+
const timestamp = `(${formatTimestamp(msg.createdAt)})`;
|
|
946
|
+
const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
|
|
947
|
+
const header = `[${roleLabel}] ${timestamp}${usageSuffix}
|
|
948
|
+
|
|
949
|
+
`;
|
|
950
|
+
let body = msg.content;
|
|
951
|
+
if (msg.error) {
|
|
952
|
+
body += `
|
|
953
|
+
|
|
954
|
+
Error: ${msg.error.code} - ${msg.error.message}`;
|
|
955
|
+
}
|
|
956
|
+
if (msg.reasoning) {
|
|
957
|
+
body += `
|
|
958
|
+
|
|
959
|
+
Reasoning: ${msg.reasoning}`;
|
|
960
|
+
}
|
|
961
|
+
body += formatSourcesTxt(msg.sources ?? []);
|
|
962
|
+
body += formatToolCallsTxt(msg.toolCalls ?? []);
|
|
963
|
+
parts.push(`${header}${body}`);
|
|
964
|
+
}
|
|
965
|
+
return parts.join(`
|
|
966
|
+
|
|
967
|
+
---
|
|
968
|
+
|
|
969
|
+
`);
|
|
970
|
+
}
|
|
971
|
+
function formatMessagesAsJson(messages, conversation) {
|
|
972
|
+
const payload = {
|
|
973
|
+
messages: messages.map(messageToJsonSerializable)
|
|
974
|
+
};
|
|
975
|
+
if (conversation) {
|
|
976
|
+
payload.conversation = {
|
|
977
|
+
id: conversation.id,
|
|
978
|
+
title: conversation.title,
|
|
979
|
+
status: conversation.status,
|
|
980
|
+
createdAt: toIsoString(conversation.createdAt),
|
|
981
|
+
updatedAt: toIsoString(conversation.updatedAt),
|
|
982
|
+
provider: conversation.provider,
|
|
983
|
+
model: conversation.model,
|
|
984
|
+
workspacePath: conversation.workspacePath,
|
|
985
|
+
contextFiles: conversation.contextFiles,
|
|
986
|
+
summary: conversation.summary,
|
|
987
|
+
metadata: conversation.metadata
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
return JSON.stringify(payload, null, 2);
|
|
991
|
+
}
|
|
992
|
+
function getExportFilename(format, conversation) {
|
|
993
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
994
|
+
const base = conversation?.title ? conversation.title.replace(/[^a-zA-Z0-9-_]/g, "_").slice(0, 40) : "chat-export";
|
|
995
|
+
const ext = format === "markdown" ? "md" : format === "txt" ? "txt" : "json";
|
|
996
|
+
return `${base}-${timestamp}.${ext}`;
|
|
997
|
+
}
|
|
998
|
+
var MIME_TYPES = {
|
|
999
|
+
markdown: "text/markdown",
|
|
1000
|
+
txt: "text/plain",
|
|
1001
|
+
json: "application/json"
|
|
1002
|
+
};
|
|
1003
|
+
function downloadAsFile(content, filename, mimeType) {
|
|
1004
|
+
const blob = new Blob([content], { type: mimeType });
|
|
1005
|
+
const url = URL.createObjectURL(blob);
|
|
1006
|
+
const a = document.createElement("a");
|
|
1007
|
+
a.href = url;
|
|
1008
|
+
a.download = filename;
|
|
1009
|
+
document.body.appendChild(a);
|
|
1010
|
+
a.click();
|
|
1011
|
+
document.body.removeChild(a);
|
|
1012
|
+
URL.revokeObjectURL(url);
|
|
1013
|
+
}
|
|
1014
|
+
function exportToFile(messages, format, conversation) {
|
|
1015
|
+
let content;
|
|
1016
|
+
if (format === "markdown") {
|
|
1017
|
+
content = formatMessagesAsMarkdown(messages);
|
|
1018
|
+
} else if (format === "txt") {
|
|
1019
|
+
content = formatMessagesAsTxt(messages);
|
|
1020
|
+
} else {
|
|
1021
|
+
content = formatMessagesAsJson(messages, conversation);
|
|
1022
|
+
}
|
|
1023
|
+
const filename = getExportFilename(format, conversation);
|
|
1024
|
+
const mimeType = MIME_TYPES[format];
|
|
1025
|
+
downloadAsFile(content, filename, mimeType);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/presentation/components/ChatExportToolbar.tsx
|
|
1029
|
+
import { jsx as jsx6, jsxs as jsxs6, Fragment as Fragment3 } from "react/jsx-runtime";
|
|
1030
|
+
"use client";
|
|
1031
|
+
function ChatExportToolbar({
|
|
1032
|
+
messages,
|
|
1033
|
+
conversation,
|
|
1034
|
+
selectedIds,
|
|
1035
|
+
onExported,
|
|
1036
|
+
showSelectionSummary = true,
|
|
1037
|
+
onSelectAll,
|
|
1038
|
+
onClearSelection,
|
|
1039
|
+
selectedCount = selectedIds.size,
|
|
1040
|
+
totalCount = messages.length,
|
|
1041
|
+
onCreateNew,
|
|
1042
|
+
onFork
|
|
1043
|
+
}) {
|
|
1044
|
+
const [copied, setCopied] = React5.useState(false);
|
|
1045
|
+
const toExport = React5.useMemo(() => {
|
|
1046
|
+
if (selectedIds.size > 0) {
|
|
1047
|
+
const idSet = selectedIds;
|
|
1048
|
+
return messages.filter((m) => idSet.has(m.id));
|
|
1049
|
+
}
|
|
1050
|
+
return messages;
|
|
1051
|
+
}, [messages, selectedIds]);
|
|
1052
|
+
const handleExport = React5.useCallback((format) => {
|
|
1053
|
+
exportToFile(toExport, format, conversation);
|
|
1054
|
+
onExported?.(format, toExport.length);
|
|
1055
|
+
}, [toExport, conversation, onExported]);
|
|
1056
|
+
const handleCopy = React5.useCallback(async () => {
|
|
1057
|
+
const content = formatMessagesAsMarkdown(toExport);
|
|
1058
|
+
await navigator.clipboard.writeText(content);
|
|
1059
|
+
setCopied(true);
|
|
1060
|
+
setTimeout(() => setCopied(false), 2000);
|
|
1061
|
+
onExported?.("markdown", toExport.length);
|
|
1062
|
+
}, [toExport, onExported]);
|
|
1063
|
+
const disabled = messages.length === 0;
|
|
1064
|
+
const [forking, setForking] = React5.useState(false);
|
|
1065
|
+
const handleFork = React5.useCallback(async (upToMessageId) => {
|
|
1066
|
+
if (!onFork)
|
|
1067
|
+
return;
|
|
1068
|
+
setForking(true);
|
|
1069
|
+
try {
|
|
1070
|
+
await onFork(upToMessageId);
|
|
1071
|
+
} finally {
|
|
1072
|
+
setForking(false);
|
|
1073
|
+
}
|
|
1074
|
+
}, [onFork]);
|
|
1075
|
+
return /* @__PURE__ */ jsxs6("div", {
|
|
1076
|
+
className: "flex items-center gap-2",
|
|
1077
|
+
children: [
|
|
1078
|
+
onCreateNew && /* @__PURE__ */ jsxs6(Button4, {
|
|
1079
|
+
variant: "outline",
|
|
1080
|
+
size: "sm",
|
|
1081
|
+
onPress: onCreateNew,
|
|
1082
|
+
"aria-label": "New conversation",
|
|
1083
|
+
children: [
|
|
1084
|
+
/* @__PURE__ */ jsx6(Plus, {
|
|
1085
|
+
className: "h-4 w-4"
|
|
1086
|
+
}),
|
|
1087
|
+
"New"
|
|
1088
|
+
]
|
|
1089
|
+
}),
|
|
1090
|
+
onFork && messages.length > 0 && /* @__PURE__ */ jsxs6(Button4, {
|
|
1091
|
+
variant: "outline",
|
|
1092
|
+
size: "sm",
|
|
1093
|
+
disabled: forking,
|
|
1094
|
+
onPress: () => handleFork(),
|
|
1095
|
+
"aria-label": "Fork conversation",
|
|
1096
|
+
children: [
|
|
1097
|
+
/* @__PURE__ */ jsx6(GitFork, {
|
|
1098
|
+
className: "h-4 w-4"
|
|
1099
|
+
}),
|
|
1100
|
+
"Fork"
|
|
1101
|
+
]
|
|
1102
|
+
}),
|
|
1103
|
+
showSelectionSummary && selectedCount > 0 && /* @__PURE__ */ jsxs6("span", {
|
|
1104
|
+
className: "text-muted-foreground text-sm",
|
|
1105
|
+
children: [
|
|
1106
|
+
selectedCount,
|
|
1107
|
+
" message",
|
|
1108
|
+
selectedCount !== 1 ? "s" : "",
|
|
1109
|
+
" selected"
|
|
1110
|
+
]
|
|
1111
|
+
}),
|
|
1112
|
+
onSelectAll && onClearSelection && totalCount > 0 && /* @__PURE__ */ jsxs6(Fragment3, {
|
|
1113
|
+
children: [
|
|
1114
|
+
/* @__PURE__ */ jsx6(Button4, {
|
|
1115
|
+
variant: "ghost",
|
|
1116
|
+
size: "sm",
|
|
1117
|
+
onPress: onSelectAll,
|
|
1118
|
+
className: "text-xs",
|
|
1119
|
+
children: "Select all"
|
|
1120
|
+
}),
|
|
1121
|
+
selectedCount > 0 && /* @__PURE__ */ jsx6(Button4, {
|
|
1122
|
+
variant: "ghost",
|
|
1123
|
+
size: "sm",
|
|
1124
|
+
onPress: onClearSelection,
|
|
1125
|
+
className: "text-xs",
|
|
1126
|
+
children: "Clear"
|
|
1127
|
+
})
|
|
1128
|
+
]
|
|
1129
|
+
}),
|
|
1130
|
+
/* @__PURE__ */ jsxs6(DropdownMenu, {
|
|
1131
|
+
children: [
|
|
1132
|
+
/* @__PURE__ */ jsx6(DropdownMenuTrigger, {
|
|
1133
|
+
asChild: true,
|
|
1134
|
+
children: /* @__PURE__ */ jsxs6(Button4, {
|
|
1135
|
+
variant: "outline",
|
|
1136
|
+
size: "sm",
|
|
1137
|
+
disabled,
|
|
1138
|
+
"aria-label": selectedCount > 0 ? "Export selected messages" : "Export conversation",
|
|
1139
|
+
children: [
|
|
1140
|
+
/* @__PURE__ */ jsx6(Download2, {
|
|
1141
|
+
className: "h-4 w-4"
|
|
1142
|
+
}),
|
|
1143
|
+
"Export"
|
|
1144
|
+
]
|
|
1145
|
+
})
|
|
1146
|
+
}),
|
|
1147
|
+
/* @__PURE__ */ jsxs6(DropdownMenuContent, {
|
|
1148
|
+
align: "end",
|
|
1149
|
+
children: [
|
|
1150
|
+
/* @__PURE__ */ jsxs6(DropdownMenuItem, {
|
|
1151
|
+
onSelect: () => handleExport("markdown"),
|
|
1152
|
+
disabled,
|
|
1153
|
+
children: [
|
|
1154
|
+
/* @__PURE__ */ jsx6(FileText2, {
|
|
1155
|
+
className: "h-4 w-4"
|
|
1156
|
+
}),
|
|
1157
|
+
"Export as Markdown (.md)"
|
|
1158
|
+
]
|
|
1159
|
+
}),
|
|
1160
|
+
/* @__PURE__ */ jsxs6(DropdownMenuItem, {
|
|
1161
|
+
onSelect: () => handleExport("txt"),
|
|
1162
|
+
disabled,
|
|
1163
|
+
children: [
|
|
1164
|
+
/* @__PURE__ */ jsx6(FileText2, {
|
|
1165
|
+
className: "h-4 w-4"
|
|
1166
|
+
}),
|
|
1167
|
+
"Export as Plain Text (.txt)"
|
|
1168
|
+
]
|
|
1169
|
+
}),
|
|
1170
|
+
/* @__PURE__ */ jsxs6(DropdownMenuItem, {
|
|
1171
|
+
onSelect: () => handleExport("json"),
|
|
1172
|
+
disabled,
|
|
1173
|
+
children: [
|
|
1174
|
+
/* @__PURE__ */ jsx6(FileText2, {
|
|
1175
|
+
className: "h-4 w-4"
|
|
1176
|
+
}),
|
|
1177
|
+
"Export as JSON (.json)"
|
|
1178
|
+
]
|
|
1179
|
+
}),
|
|
1180
|
+
/* @__PURE__ */ jsx6(DropdownMenuSeparator, {}),
|
|
1181
|
+
/* @__PURE__ */ jsxs6(DropdownMenuItem, {
|
|
1182
|
+
onSelect: () => handleCopy(),
|
|
1183
|
+
disabled,
|
|
1184
|
+
children: [
|
|
1185
|
+
copied ? /* @__PURE__ */ jsx6(Check3, {
|
|
1186
|
+
className: "h-4 w-4 text-green-500"
|
|
1187
|
+
}) : /* @__PURE__ */ jsx6(Copy3, {
|
|
1188
|
+
className: "h-4 w-4"
|
|
1189
|
+
}),
|
|
1190
|
+
copied ? "Copied to clipboard" : "Copy to clipboard"
|
|
1191
|
+
]
|
|
1192
|
+
})
|
|
1193
|
+
]
|
|
1194
|
+
})
|
|
1195
|
+
]
|
|
1196
|
+
})
|
|
1197
|
+
]
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
// src/presentation/components/ChatWithExport.tsx
|
|
1201
|
+
import * as React8 from "react";
|
|
1202
|
+
|
|
1203
|
+
// src/presentation/components/ThinkingLevelPicker.tsx
|
|
1204
|
+
import * as React6 from "react";
|
|
1205
|
+
import { cn as cn5 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
674
1206
|
import {
|
|
675
1207
|
Select,
|
|
676
1208
|
SelectContent,
|
|
@@ -678,30 +1210,2292 @@ import {
|
|
|
678
1210
|
SelectTrigger,
|
|
679
1211
|
SelectValue
|
|
680
1212
|
} from "@contractspec/lib.ui-kit-web/ui/select";
|
|
681
|
-
import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
682
1213
|
import { Label } from "@contractspec/lib.ui-kit-web/ui/label";
|
|
1214
|
+
|
|
1215
|
+
// src/core/thinking-levels.ts
|
|
1216
|
+
var THINKING_LEVEL_LABELS = {
|
|
1217
|
+
instant: "Instant",
|
|
1218
|
+
thinking: "Thinking",
|
|
1219
|
+
extra_thinking: "Extra Thinking",
|
|
1220
|
+
max: "Max"
|
|
1221
|
+
};
|
|
1222
|
+
var THINKING_LEVEL_DESCRIPTIONS = {
|
|
1223
|
+
instant: "Fast responses, minimal reasoning",
|
|
1224
|
+
thinking: "Standard reasoning depth",
|
|
1225
|
+
extra_thinking: "More thorough reasoning",
|
|
1226
|
+
max: "Maximum reasoning depth"
|
|
1227
|
+
};
|
|
1228
|
+
function getProviderOptions(level, providerName) {
|
|
1229
|
+
if (!level || level === "instant") {
|
|
1230
|
+
return {};
|
|
1231
|
+
}
|
|
1232
|
+
switch (providerName) {
|
|
1233
|
+
case "anthropic": {
|
|
1234
|
+
const budgetMap = {
|
|
1235
|
+
thinking: 8000,
|
|
1236
|
+
extra_thinking: 16000,
|
|
1237
|
+
max: 32000
|
|
1238
|
+
};
|
|
1239
|
+
return {
|
|
1240
|
+
anthropic: {
|
|
1241
|
+
thinking: { type: "enabled", budgetTokens: budgetMap[level] }
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
case "openai": {
|
|
1246
|
+
const effortMap = {
|
|
1247
|
+
thinking: "low",
|
|
1248
|
+
extra_thinking: "medium",
|
|
1249
|
+
max: "high"
|
|
1250
|
+
};
|
|
1251
|
+
return {
|
|
1252
|
+
openai: {
|
|
1253
|
+
reasoningEffort: effortMap[level]
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
case "ollama":
|
|
1258
|
+
case "mistral":
|
|
1259
|
+
case "gemini":
|
|
1260
|
+
return {};
|
|
1261
|
+
default:
|
|
1262
|
+
return {};
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/presentation/components/ThinkingLevelPicker.tsx
|
|
1267
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1268
|
+
"use client";
|
|
1269
|
+
var THINKING_LEVELS = [
|
|
1270
|
+
"instant",
|
|
1271
|
+
"thinking",
|
|
1272
|
+
"extra_thinking",
|
|
1273
|
+
"max"
|
|
1274
|
+
];
|
|
1275
|
+
function ThinkingLevelPicker({
|
|
1276
|
+
value,
|
|
1277
|
+
onChange,
|
|
1278
|
+
className,
|
|
1279
|
+
compact = false
|
|
1280
|
+
}) {
|
|
1281
|
+
const handleChange = React6.useCallback((v) => {
|
|
1282
|
+
onChange(v);
|
|
1283
|
+
}, [onChange]);
|
|
1284
|
+
if (compact) {
|
|
1285
|
+
return /* @__PURE__ */ jsxs7(Select, {
|
|
1286
|
+
value,
|
|
1287
|
+
onValueChange: handleChange,
|
|
1288
|
+
children: [
|
|
1289
|
+
/* @__PURE__ */ jsx7(SelectTrigger, {
|
|
1290
|
+
className: cn5("w-[140px]", className),
|
|
1291
|
+
children: /* @__PURE__ */ jsx7(SelectValue, {})
|
|
1292
|
+
}),
|
|
1293
|
+
/* @__PURE__ */ jsx7(SelectContent, {
|
|
1294
|
+
children: THINKING_LEVELS.map((level) => /* @__PURE__ */ jsx7(SelectItem, {
|
|
1295
|
+
value: level,
|
|
1296
|
+
children: THINKING_LEVEL_LABELS[level]
|
|
1297
|
+
}, level))
|
|
1298
|
+
})
|
|
1299
|
+
]
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
return /* @__PURE__ */ jsxs7("div", {
|
|
1303
|
+
className: cn5("flex flex-col gap-1.5", className),
|
|
1304
|
+
children: [
|
|
1305
|
+
/* @__PURE__ */ jsx7(Label, {
|
|
1306
|
+
htmlFor: "thinking-level-picker",
|
|
1307
|
+
className: "text-sm font-medium",
|
|
1308
|
+
children: "Thinking Level"
|
|
1309
|
+
}),
|
|
1310
|
+
/* @__PURE__ */ jsxs7(Select, {
|
|
1311
|
+
name: "thinking-level-picker",
|
|
1312
|
+
value,
|
|
1313
|
+
onValueChange: handleChange,
|
|
1314
|
+
children: [
|
|
1315
|
+
/* @__PURE__ */ jsx7(SelectTrigger, {
|
|
1316
|
+
children: /* @__PURE__ */ jsx7(SelectValue, {
|
|
1317
|
+
placeholder: "Select thinking level"
|
|
1318
|
+
})
|
|
1319
|
+
}),
|
|
1320
|
+
/* @__PURE__ */ jsx7(SelectContent, {
|
|
1321
|
+
children: THINKING_LEVELS.map((level) => /* @__PURE__ */ jsx7(SelectItem, {
|
|
1322
|
+
value: level,
|
|
1323
|
+
title: THINKING_LEVEL_DESCRIPTIONS[level],
|
|
1324
|
+
children: THINKING_LEVEL_LABELS[level]
|
|
1325
|
+
}, level))
|
|
1326
|
+
})
|
|
1327
|
+
]
|
|
1328
|
+
})
|
|
1329
|
+
]
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// src/presentation/hooks/useMessageSelection.ts
|
|
1334
|
+
import * as React7 from "react";
|
|
1335
|
+
"use client";
|
|
1336
|
+
function useMessageSelection(messageIds) {
|
|
1337
|
+
const [selectedIds, setSelectedIds] = React7.useState(() => new Set);
|
|
1338
|
+
const idSet = React7.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
|
|
1339
|
+
React7.useEffect(() => {
|
|
1340
|
+
setSelectedIds((prev) => {
|
|
1341
|
+
const next = new Set;
|
|
1342
|
+
for (const id of prev) {
|
|
1343
|
+
if (idSet.has(id))
|
|
1344
|
+
next.add(id);
|
|
1345
|
+
}
|
|
1346
|
+
return next.size === prev.size ? prev : next;
|
|
1347
|
+
});
|
|
1348
|
+
}, [idSet]);
|
|
1349
|
+
const toggle = React7.useCallback((id) => {
|
|
1350
|
+
setSelectedIds((prev) => {
|
|
1351
|
+
const next = new Set(prev);
|
|
1352
|
+
if (next.has(id))
|
|
1353
|
+
next.delete(id);
|
|
1354
|
+
else
|
|
1355
|
+
next.add(id);
|
|
1356
|
+
return next;
|
|
1357
|
+
});
|
|
1358
|
+
}, []);
|
|
1359
|
+
const selectAll = React7.useCallback(() => {
|
|
1360
|
+
setSelectedIds(new Set(messageIds));
|
|
1361
|
+
}, [messageIds.join(",")]);
|
|
1362
|
+
const clearSelection = React7.useCallback(() => {
|
|
1363
|
+
setSelectedIds(new Set);
|
|
1364
|
+
}, []);
|
|
1365
|
+
const isSelected = React7.useCallback((id) => selectedIds.has(id), [selectedIds]);
|
|
1366
|
+
const selectedCount = selectedIds.size;
|
|
1367
|
+
return {
|
|
1368
|
+
selectedIds,
|
|
1369
|
+
toggle,
|
|
1370
|
+
selectAll,
|
|
1371
|
+
clearSelection,
|
|
1372
|
+
isSelected,
|
|
1373
|
+
selectedCount
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/presentation/components/ChatWithExport.tsx
|
|
1378
|
+
import { jsx as jsx8, jsxs as jsxs8, Fragment as Fragment4 } from "react/jsx-runtime";
|
|
1379
|
+
"use client";
|
|
1380
|
+
function ChatWithExport({
|
|
1381
|
+
messages,
|
|
1382
|
+
conversation,
|
|
1383
|
+
children,
|
|
1384
|
+
className,
|
|
1385
|
+
showExport = true,
|
|
1386
|
+
showMessageSelection = true,
|
|
1387
|
+
showScrollButton = true,
|
|
1388
|
+
onCreateNew,
|
|
1389
|
+
onFork,
|
|
1390
|
+
onEditMessage,
|
|
1391
|
+
thinkingLevel = "thinking",
|
|
1392
|
+
onThinkingLevelChange,
|
|
1393
|
+
presentationRenderer,
|
|
1394
|
+
formRenderer
|
|
1395
|
+
}) {
|
|
1396
|
+
const messageIds = React8.useMemo(() => messages.map((m) => m.id), [messages]);
|
|
1397
|
+
const selection = useMessageSelection(messageIds);
|
|
1398
|
+
const hasToolbar = showExport || showMessageSelection;
|
|
1399
|
+
const hasPicker = Boolean(onThinkingLevelChange);
|
|
1400
|
+
const headerContent = hasPicker || hasToolbar ? /* @__PURE__ */ jsxs8(Fragment4, {
|
|
1401
|
+
children: [
|
|
1402
|
+
hasPicker && /* @__PURE__ */ jsx8(ThinkingLevelPicker, {
|
|
1403
|
+
value: thinkingLevel,
|
|
1404
|
+
onChange: onThinkingLevelChange,
|
|
1405
|
+
compact: true
|
|
1406
|
+
}),
|
|
1407
|
+
hasToolbar && /* @__PURE__ */ jsx8(ChatExportToolbar, {
|
|
1408
|
+
messages,
|
|
1409
|
+
conversation,
|
|
1410
|
+
selectedIds: selection.selectedIds,
|
|
1411
|
+
showSelectionSummary: showMessageSelection,
|
|
1412
|
+
onSelectAll: showMessageSelection ? selection.selectAll : undefined,
|
|
1413
|
+
onClearSelection: showMessageSelection ? selection.clearSelection : undefined,
|
|
1414
|
+
selectedCount: selection.selectedCount,
|
|
1415
|
+
totalCount: messages.length,
|
|
1416
|
+
onCreateNew,
|
|
1417
|
+
onFork
|
|
1418
|
+
})
|
|
1419
|
+
]
|
|
1420
|
+
}) : null;
|
|
1421
|
+
return /* @__PURE__ */ jsxs8(ChatContainer, {
|
|
1422
|
+
className,
|
|
1423
|
+
headerContent,
|
|
1424
|
+
showScrollButton,
|
|
1425
|
+
children: [
|
|
1426
|
+
messages.map((msg) => /* @__PURE__ */ jsx8(ChatMessage, {
|
|
1427
|
+
message: msg,
|
|
1428
|
+
selectable: showMessageSelection,
|
|
1429
|
+
selected: selection.isSelected(msg.id),
|
|
1430
|
+
onSelect: showMessageSelection ? selection.toggle : undefined,
|
|
1431
|
+
editable: msg.role === "user" && !!onEditMessage,
|
|
1432
|
+
onEdit: onEditMessage,
|
|
1433
|
+
presentationRenderer,
|
|
1434
|
+
formRenderer
|
|
1435
|
+
}, msg.id)),
|
|
1436
|
+
children
|
|
1437
|
+
]
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
// src/presentation/components/ChatSidebar.tsx
|
|
1441
|
+
import * as React10 from "react";
|
|
1442
|
+
import { Plus as Plus2, Trash2, MessageSquare } from "lucide-react";
|
|
1443
|
+
import { Button as Button5 } from "@contractspec/lib.design-system";
|
|
1444
|
+
import { cn as cn6 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
1445
|
+
|
|
1446
|
+
// src/presentation/hooks/useConversations.ts
|
|
1447
|
+
import * as React9 from "react";
|
|
1448
|
+
"use client";
|
|
1449
|
+
function useConversations(options) {
|
|
1450
|
+
const { store, projectId, tags, limit = 50 } = options;
|
|
1451
|
+
const [conversations, setConversations] = React9.useState([]);
|
|
1452
|
+
const [isLoading, setIsLoading] = React9.useState(true);
|
|
1453
|
+
const refresh = React9.useCallback(async () => {
|
|
1454
|
+
setIsLoading(true);
|
|
1455
|
+
try {
|
|
1456
|
+
const list = await store.list({
|
|
1457
|
+
status: "active",
|
|
1458
|
+
projectId,
|
|
1459
|
+
tags,
|
|
1460
|
+
limit
|
|
1461
|
+
});
|
|
1462
|
+
setConversations(list);
|
|
1463
|
+
} finally {
|
|
1464
|
+
setIsLoading(false);
|
|
1465
|
+
}
|
|
1466
|
+
}, [store, projectId, tags, limit]);
|
|
1467
|
+
React9.useEffect(() => {
|
|
1468
|
+
refresh();
|
|
1469
|
+
}, [refresh]);
|
|
1470
|
+
const deleteConversation = React9.useCallback(async (id) => {
|
|
1471
|
+
const ok = await store.delete(id);
|
|
1472
|
+
if (ok) {
|
|
1473
|
+
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
1474
|
+
}
|
|
1475
|
+
return ok;
|
|
1476
|
+
}, [store]);
|
|
1477
|
+
return {
|
|
1478
|
+
conversations,
|
|
1479
|
+
isLoading,
|
|
1480
|
+
refresh,
|
|
1481
|
+
deleteConversation
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/presentation/components/ChatSidebar.tsx
|
|
1486
|
+
import { jsx as jsx9, jsxs as jsxs9, Fragment as Fragment5 } from "react/jsx-runtime";
|
|
1487
|
+
"use client";
|
|
1488
|
+
function formatDate(date) {
|
|
1489
|
+
const d = new Date(date);
|
|
1490
|
+
const now = new Date;
|
|
1491
|
+
const diff = now.getTime() - d.getTime();
|
|
1492
|
+
if (diff < 86400000) {
|
|
1493
|
+
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1494
|
+
}
|
|
1495
|
+
if (diff < 604800000) {
|
|
1496
|
+
return d.toLocaleDateString([], { weekday: "short" });
|
|
1497
|
+
}
|
|
1498
|
+
return d.toLocaleDateString([], { month: "short", day: "numeric" });
|
|
1499
|
+
}
|
|
1500
|
+
function ConversationItem({
|
|
1501
|
+
conversation,
|
|
1502
|
+
selected,
|
|
1503
|
+
onSelect,
|
|
1504
|
+
onDelete
|
|
1505
|
+
}) {
|
|
1506
|
+
const title = conversation.title ?? conversation.messages[0]?.content?.slice(0, 50) ?? "New chat";
|
|
1507
|
+
const displayTitle = title.length > 40 ? `${title.slice(0, 40)}…` : title;
|
|
1508
|
+
return /* @__PURE__ */ jsxs9("div", {
|
|
1509
|
+
role: "button",
|
|
1510
|
+
tabIndex: 0,
|
|
1511
|
+
onClick: onSelect,
|
|
1512
|
+
onKeyDown: (e) => {
|
|
1513
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1514
|
+
e.preventDefault();
|
|
1515
|
+
onSelect();
|
|
1516
|
+
}
|
|
1517
|
+
},
|
|
1518
|
+
className: cn6("group flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors", selected ? "bg-accent text-accent-foreground" : "hover:bg-accent/50"),
|
|
1519
|
+
children: [
|
|
1520
|
+
/* @__PURE__ */ jsx9(MessageSquare, {
|
|
1521
|
+
className: "text-muted-foreground h-4 w-4 shrink-0"
|
|
1522
|
+
}),
|
|
1523
|
+
/* @__PURE__ */ jsxs9("div", {
|
|
1524
|
+
className: "min-w-0 flex-1",
|
|
1525
|
+
children: [
|
|
1526
|
+
/* @__PURE__ */ jsx9("p", {
|
|
1527
|
+
className: "truncate",
|
|
1528
|
+
children: displayTitle
|
|
1529
|
+
}),
|
|
1530
|
+
/* @__PURE__ */ jsxs9("p", {
|
|
1531
|
+
className: "text-muted-foreground text-xs",
|
|
1532
|
+
children: [
|
|
1533
|
+
formatDate(conversation.updatedAt),
|
|
1534
|
+
conversation.projectName && ` · ${conversation.projectName}`,
|
|
1535
|
+
conversation.tags && conversation.tags.length > 0 && /* @__PURE__ */ jsxs9(Fragment5, {
|
|
1536
|
+
children: [
|
|
1537
|
+
" · ",
|
|
1538
|
+
conversation.tags.slice(0, 2).join(", ")
|
|
1539
|
+
]
|
|
1540
|
+
})
|
|
1541
|
+
]
|
|
1542
|
+
})
|
|
1543
|
+
]
|
|
1544
|
+
}),
|
|
1545
|
+
/* @__PURE__ */ jsx9("span", {
|
|
1546
|
+
onClick: (e) => e.stopPropagation(),
|
|
1547
|
+
children: /* @__PURE__ */ jsx9(Button5, {
|
|
1548
|
+
variant: "ghost",
|
|
1549
|
+
size: "sm",
|
|
1550
|
+
className: "h-6 w-6 shrink-0 p-0 opacity-0 group-hover:opacity-100",
|
|
1551
|
+
onPress: onDelete,
|
|
1552
|
+
"aria-label": "Delete conversation",
|
|
1553
|
+
children: /* @__PURE__ */ jsx9(Trash2, {
|
|
1554
|
+
className: "h-3 w-3"
|
|
1555
|
+
})
|
|
1556
|
+
})
|
|
1557
|
+
})
|
|
1558
|
+
]
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
function ChatSidebar({
|
|
1562
|
+
store,
|
|
1563
|
+
selectedConversationId,
|
|
1564
|
+
onSelectConversation,
|
|
1565
|
+
onCreateNew,
|
|
1566
|
+
projectId,
|
|
1567
|
+
tags,
|
|
1568
|
+
limit = 50,
|
|
1569
|
+
className,
|
|
1570
|
+
collapsed = false,
|
|
1571
|
+
onUpdateConversation,
|
|
1572
|
+
selectedConversation
|
|
1573
|
+
}) {
|
|
1574
|
+
const { conversations, isLoading, refresh, deleteConversation } = useConversations({ store, projectId, tags, limit });
|
|
1575
|
+
const handleDelete = React10.useCallback(async (id) => {
|
|
1576
|
+
const ok = await deleteConversation(id);
|
|
1577
|
+
if (ok && selectedConversationId === id) {
|
|
1578
|
+
onSelectConversation(null);
|
|
1579
|
+
}
|
|
1580
|
+
}, [deleteConversation, selectedConversationId, onSelectConversation]);
|
|
1581
|
+
if (collapsed)
|
|
1582
|
+
return null;
|
|
1583
|
+
return /* @__PURE__ */ jsxs9("div", {
|
|
1584
|
+
className: cn6("border-border flex w-64 shrink-0 flex-col border-r", className),
|
|
1585
|
+
children: [
|
|
1586
|
+
/* @__PURE__ */ jsxs9("div", {
|
|
1587
|
+
className: "border-border flex shrink-0 items-center justify-between border-b p-2",
|
|
1588
|
+
children: [
|
|
1589
|
+
/* @__PURE__ */ jsx9("span", {
|
|
1590
|
+
className: "text-muted-foreground text-sm font-medium",
|
|
1591
|
+
children: "Conversations"
|
|
1592
|
+
}),
|
|
1593
|
+
/* @__PURE__ */ jsx9(Button5, {
|
|
1594
|
+
variant: "ghost",
|
|
1595
|
+
size: "sm",
|
|
1596
|
+
className: "h-8 w-8 p-0",
|
|
1597
|
+
onPress: onCreateNew,
|
|
1598
|
+
"aria-label": "New conversation",
|
|
1599
|
+
children: /* @__PURE__ */ jsx9(Plus2, {
|
|
1600
|
+
className: "h-4 w-4"
|
|
1601
|
+
})
|
|
1602
|
+
})
|
|
1603
|
+
]
|
|
1604
|
+
}),
|
|
1605
|
+
/* @__PURE__ */ jsx9("div", {
|
|
1606
|
+
className: "flex-1 overflow-y-auto p-2",
|
|
1607
|
+
children: isLoading ? /* @__PURE__ */ jsx9("div", {
|
|
1608
|
+
className: "text-muted-foreground py-4 text-center text-sm",
|
|
1609
|
+
children: "Loading…"
|
|
1610
|
+
}) : conversations.length === 0 ? /* @__PURE__ */ jsx9("div", {
|
|
1611
|
+
className: "text-muted-foreground py-4 text-center text-sm",
|
|
1612
|
+
children: "No conversations yet"
|
|
1613
|
+
}) : /* @__PURE__ */ jsx9("div", {
|
|
1614
|
+
className: "flex flex-col gap-1",
|
|
1615
|
+
children: conversations.map((conv) => /* @__PURE__ */ jsx9(ConversationItem, {
|
|
1616
|
+
conversation: conv,
|
|
1617
|
+
selected: conv.id === selectedConversationId,
|
|
1618
|
+
onSelect: () => onSelectConversation(conv.id),
|
|
1619
|
+
onDelete: () => handleDelete(conv.id)
|
|
1620
|
+
}, conv.id))
|
|
1621
|
+
})
|
|
1622
|
+
}),
|
|
1623
|
+
selectedConversation && onUpdateConversation && /* @__PURE__ */ jsx9(ConversationMeta, {
|
|
1624
|
+
conversation: selectedConversation,
|
|
1625
|
+
onUpdate: onUpdateConversation
|
|
1626
|
+
})
|
|
1627
|
+
]
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
function ConversationMeta({
|
|
1631
|
+
conversation,
|
|
1632
|
+
onUpdate
|
|
1633
|
+
}) {
|
|
1634
|
+
const [projectName, setProjectName] = React10.useState(conversation.projectName ?? "");
|
|
1635
|
+
const [tagsStr, setTagsStr] = React10.useState(conversation.tags?.join(", ") ?? "");
|
|
1636
|
+
React10.useEffect(() => {
|
|
1637
|
+
setProjectName(conversation.projectName ?? "");
|
|
1638
|
+
setTagsStr(conversation.tags?.join(", ") ?? "");
|
|
1639
|
+
}, [conversation.id, conversation.projectName, conversation.tags]);
|
|
1640
|
+
const handleBlur = React10.useCallback(() => {
|
|
1641
|
+
const tags = tagsStr.split(",").map((t) => t.trim()).filter(Boolean);
|
|
1642
|
+
if (projectName !== (conversation.projectName ?? "") || JSON.stringify(tags) !== JSON.stringify(conversation.tags ?? [])) {
|
|
1643
|
+
onUpdate(conversation.id, {
|
|
1644
|
+
projectName: projectName || undefined,
|
|
1645
|
+
projectId: projectName ? projectName.replace(/\s+/g, "-") : undefined,
|
|
1646
|
+
tags: tags.length > 0 ? tags : undefined
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
}, [
|
|
1650
|
+
conversation.id,
|
|
1651
|
+
conversation.projectName,
|
|
1652
|
+
conversation.tags,
|
|
1653
|
+
projectName,
|
|
1654
|
+
tagsStr,
|
|
1655
|
+
onUpdate
|
|
1656
|
+
]);
|
|
1657
|
+
return /* @__PURE__ */ jsxs9("div", {
|
|
1658
|
+
className: "border-border shrink-0 border-t p-2",
|
|
1659
|
+
children: [
|
|
1660
|
+
/* @__PURE__ */ jsx9("p", {
|
|
1661
|
+
className: "text-muted-foreground mb-1 text-xs font-medium",
|
|
1662
|
+
children: "Project"
|
|
1663
|
+
}),
|
|
1664
|
+
/* @__PURE__ */ jsx9("input", {
|
|
1665
|
+
type: "text",
|
|
1666
|
+
value: projectName,
|
|
1667
|
+
onChange: (e) => setProjectName(e.target.value),
|
|
1668
|
+
onBlur: handleBlur,
|
|
1669
|
+
placeholder: "Project name",
|
|
1670
|
+
className: "border-input bg-background mb-2 w-full rounded px-2 py-1 text-xs"
|
|
1671
|
+
}),
|
|
1672
|
+
/* @__PURE__ */ jsx9("p", {
|
|
1673
|
+
className: "text-muted-foreground mb-1 text-xs font-medium",
|
|
1674
|
+
children: "Tags"
|
|
1675
|
+
}),
|
|
1676
|
+
/* @__PURE__ */ jsx9("input", {
|
|
1677
|
+
type: "text",
|
|
1678
|
+
value: tagsStr,
|
|
1679
|
+
onChange: (e) => setTagsStr(e.target.value),
|
|
1680
|
+
onBlur: handleBlur,
|
|
1681
|
+
placeholder: "tag1, tag2",
|
|
1682
|
+
className: "border-input bg-background w-full rounded px-2 py-1 text-xs"
|
|
1683
|
+
})
|
|
1684
|
+
]
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
// src/presentation/components/ChatWithSidebar.tsx
|
|
1688
|
+
import * as React12 from "react";
|
|
1689
|
+
|
|
1690
|
+
// src/presentation/hooks/useChat.tsx
|
|
1691
|
+
import * as React11 from "react";
|
|
1692
|
+
import { tool as tool4 } from "ai";
|
|
1693
|
+
import { z as z4 } from "zod";
|
|
1694
|
+
|
|
1695
|
+
// src/core/chat-service.ts
|
|
1696
|
+
import { generateText, streamText } from "ai";
|
|
1697
|
+
|
|
1698
|
+
// src/core/conversation-store.ts
|
|
1699
|
+
function generateId(prefix) {
|
|
1700
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
class InMemoryConversationStore {
|
|
1704
|
+
conversations = new Map;
|
|
1705
|
+
async get(conversationId) {
|
|
1706
|
+
return this.conversations.get(conversationId) ?? null;
|
|
1707
|
+
}
|
|
1708
|
+
async create(conversation) {
|
|
1709
|
+
const now = new Date;
|
|
1710
|
+
const fullConversation = {
|
|
1711
|
+
...conversation,
|
|
1712
|
+
id: generateId("conv"),
|
|
1713
|
+
createdAt: now,
|
|
1714
|
+
updatedAt: now
|
|
1715
|
+
};
|
|
1716
|
+
this.conversations.set(fullConversation.id, fullConversation);
|
|
1717
|
+
return fullConversation;
|
|
1718
|
+
}
|
|
1719
|
+
async update(conversationId, updates) {
|
|
1720
|
+
const conversation = this.conversations.get(conversationId);
|
|
1721
|
+
if (!conversation)
|
|
1722
|
+
return null;
|
|
1723
|
+
const updated = {
|
|
1724
|
+
...conversation,
|
|
1725
|
+
...updates,
|
|
1726
|
+
updatedAt: new Date
|
|
1727
|
+
};
|
|
1728
|
+
this.conversations.set(conversationId, updated);
|
|
1729
|
+
return updated;
|
|
1730
|
+
}
|
|
1731
|
+
async appendMessage(conversationId, message) {
|
|
1732
|
+
const conversation = this.conversations.get(conversationId);
|
|
1733
|
+
if (!conversation) {
|
|
1734
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
1735
|
+
}
|
|
1736
|
+
const now = new Date;
|
|
1737
|
+
const fullMessage = {
|
|
1738
|
+
...message,
|
|
1739
|
+
id: generateId("msg"),
|
|
1740
|
+
conversationId,
|
|
1741
|
+
createdAt: now,
|
|
1742
|
+
updatedAt: now
|
|
1743
|
+
};
|
|
1744
|
+
conversation.messages.push(fullMessage);
|
|
1745
|
+
conversation.updatedAt = now;
|
|
1746
|
+
return fullMessage;
|
|
1747
|
+
}
|
|
1748
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
1749
|
+
const conversation = this.conversations.get(conversationId);
|
|
1750
|
+
if (!conversation)
|
|
1751
|
+
return null;
|
|
1752
|
+
const messageIndex = conversation.messages.findIndex((m) => m.id === messageId);
|
|
1753
|
+
if (messageIndex === -1)
|
|
1754
|
+
return null;
|
|
1755
|
+
const message = conversation.messages[messageIndex];
|
|
1756
|
+
if (!message)
|
|
1757
|
+
return null;
|
|
1758
|
+
const updated = {
|
|
1759
|
+
...message,
|
|
1760
|
+
...updates,
|
|
1761
|
+
updatedAt: new Date
|
|
1762
|
+
};
|
|
1763
|
+
conversation.messages[messageIndex] = updated;
|
|
1764
|
+
conversation.updatedAt = new Date;
|
|
1765
|
+
return updated;
|
|
1766
|
+
}
|
|
1767
|
+
async delete(conversationId) {
|
|
1768
|
+
return this.conversations.delete(conversationId);
|
|
1769
|
+
}
|
|
1770
|
+
async list(options) {
|
|
1771
|
+
let results = Array.from(this.conversations.values());
|
|
1772
|
+
if (options?.status) {
|
|
1773
|
+
results = results.filter((c) => c.status === options.status);
|
|
1774
|
+
}
|
|
1775
|
+
if (options?.projectId) {
|
|
1776
|
+
results = results.filter((c) => c.projectId === options.projectId);
|
|
1777
|
+
}
|
|
1778
|
+
if (options?.tags && options.tags.length > 0) {
|
|
1779
|
+
const tagSet = new Set(options.tags);
|
|
1780
|
+
results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
|
|
1781
|
+
}
|
|
1782
|
+
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
1783
|
+
const offset = options?.offset ?? 0;
|
|
1784
|
+
const limit = options?.limit ?? 100;
|
|
1785
|
+
return results.slice(offset, offset + limit);
|
|
1786
|
+
}
|
|
1787
|
+
async fork(conversationId, upToMessageId) {
|
|
1788
|
+
const source = this.conversations.get(conversationId);
|
|
1789
|
+
if (!source) {
|
|
1790
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
1791
|
+
}
|
|
1792
|
+
let messagesToCopy = source.messages;
|
|
1793
|
+
if (upToMessageId) {
|
|
1794
|
+
const idx = source.messages.findIndex((m) => m.id === upToMessageId);
|
|
1795
|
+
if (idx === -1) {
|
|
1796
|
+
throw new Error(`Message ${upToMessageId} not found`);
|
|
1797
|
+
}
|
|
1798
|
+
messagesToCopy = source.messages.slice(0, idx + 1);
|
|
1799
|
+
}
|
|
1800
|
+
const now = new Date;
|
|
1801
|
+
const forkedMessages = messagesToCopy.map((m) => ({
|
|
1802
|
+
...m,
|
|
1803
|
+
id: generateId("msg"),
|
|
1804
|
+
conversationId: "",
|
|
1805
|
+
createdAt: new Date(m.createdAt),
|
|
1806
|
+
updatedAt: new Date(m.updatedAt)
|
|
1807
|
+
}));
|
|
1808
|
+
const forked = {
|
|
1809
|
+
...source,
|
|
1810
|
+
id: generateId("conv"),
|
|
1811
|
+
title: source.title ? `${source.title} (fork)` : undefined,
|
|
1812
|
+
forkedFromId: source.id,
|
|
1813
|
+
createdAt: now,
|
|
1814
|
+
updatedAt: now,
|
|
1815
|
+
messages: forkedMessages
|
|
1816
|
+
};
|
|
1817
|
+
for (const m of forked.messages) {
|
|
1818
|
+
m.conversationId = forked.id;
|
|
1819
|
+
}
|
|
1820
|
+
this.conversations.set(forked.id, forked);
|
|
1821
|
+
return forked;
|
|
1822
|
+
}
|
|
1823
|
+
async truncateAfter(conversationId, messageId) {
|
|
1824
|
+
const conv = this.conversations.get(conversationId);
|
|
1825
|
+
if (!conv)
|
|
1826
|
+
return null;
|
|
1827
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
1828
|
+
if (idx === -1)
|
|
1829
|
+
return null;
|
|
1830
|
+
conv.messages = conv.messages.slice(0, idx + 1);
|
|
1831
|
+
conv.updatedAt = new Date;
|
|
1832
|
+
return conv;
|
|
1833
|
+
}
|
|
1834
|
+
async search(query, limit = 20) {
|
|
1835
|
+
const lowerQuery = query.toLowerCase();
|
|
1836
|
+
const results = [];
|
|
1837
|
+
for (const conversation of this.conversations.values()) {
|
|
1838
|
+
if (conversation.title?.toLowerCase().includes(lowerQuery)) {
|
|
1839
|
+
results.push(conversation);
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
const hasMatch = conversation.messages.some((m) => m.content.toLowerCase().includes(lowerQuery));
|
|
1843
|
+
if (hasMatch) {
|
|
1844
|
+
results.push(conversation);
|
|
1845
|
+
}
|
|
1846
|
+
if (results.length >= limit)
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1849
|
+
return results;
|
|
1850
|
+
}
|
|
1851
|
+
clear() {
|
|
1852
|
+
this.conversations.clear();
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
function createInMemoryConversationStore() {
|
|
1856
|
+
return new InMemoryConversationStore;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// src/core/workflow-tools.ts
|
|
1860
|
+
import { tool } from "ai";
|
|
1861
|
+
import { z } from "zod";
|
|
1862
|
+
import {
|
|
1863
|
+
WorkflowComposer,
|
|
1864
|
+
validateExtension
|
|
1865
|
+
} from "@contractspec/lib.workflow-composer";
|
|
1866
|
+
var StepTypeSchema = z.enum(["human", "automation", "decision"]);
|
|
1867
|
+
var StepActionSchema = z.object({
|
|
1868
|
+
operation: z.object({
|
|
1869
|
+
name: z.string(),
|
|
1870
|
+
version: z.number()
|
|
1871
|
+
}).optional(),
|
|
1872
|
+
form: z.object({
|
|
1873
|
+
key: z.string(),
|
|
1874
|
+
version: z.number()
|
|
1875
|
+
}).optional()
|
|
1876
|
+
}).optional();
|
|
1877
|
+
var StepSchema = z.object({
|
|
1878
|
+
id: z.string(),
|
|
1879
|
+
type: StepTypeSchema,
|
|
1880
|
+
label: z.string(),
|
|
1881
|
+
description: z.string().optional(),
|
|
1882
|
+
action: StepActionSchema
|
|
1883
|
+
});
|
|
1884
|
+
var StepInjectionSchema = z.object({
|
|
1885
|
+
after: z.string().optional(),
|
|
1886
|
+
before: z.string().optional(),
|
|
1887
|
+
inject: StepSchema,
|
|
1888
|
+
transitionTo: z.string().optional(),
|
|
1889
|
+
transitionFrom: z.string().optional(),
|
|
1890
|
+
when: z.string().optional()
|
|
1891
|
+
});
|
|
1892
|
+
var WorkflowExtensionInputSchema = z.object({
|
|
1893
|
+
workflow: z.string(),
|
|
1894
|
+
tenantId: z.string().optional(),
|
|
1895
|
+
role: z.string().optional(),
|
|
1896
|
+
priority: z.number().optional(),
|
|
1897
|
+
customSteps: z.array(StepInjectionSchema).optional(),
|
|
1898
|
+
hiddenSteps: z.array(z.string()).optional()
|
|
1899
|
+
});
|
|
1900
|
+
function createWorkflowTools(config) {
|
|
1901
|
+
const { baseWorkflows, composer } = config;
|
|
1902
|
+
const baseByKey = new Map(baseWorkflows.map((b) => [b.meta.key, b]));
|
|
1903
|
+
const createWorkflowExtensionTool = tool({
|
|
1904
|
+
description: "Create or validate a workflow extension. Use when the user asks to add steps, modify a workflow, or create a tenant-specific extension. The extension targets an existing base workflow.",
|
|
1905
|
+
inputSchema: WorkflowExtensionInputSchema,
|
|
1906
|
+
execute: async (input) => {
|
|
1907
|
+
const extension = {
|
|
1908
|
+
workflow: input.workflow,
|
|
1909
|
+
tenantId: input.tenantId,
|
|
1910
|
+
role: input.role,
|
|
1911
|
+
priority: input.priority,
|
|
1912
|
+
customSteps: input.customSteps,
|
|
1913
|
+
hiddenSteps: input.hiddenSteps
|
|
1914
|
+
};
|
|
1915
|
+
const base = baseByKey.get(input.workflow);
|
|
1916
|
+
if (!base) {
|
|
1917
|
+
return {
|
|
1918
|
+
success: false,
|
|
1919
|
+
error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
1920
|
+
extension
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
try {
|
|
1924
|
+
validateExtension(extension, base);
|
|
1925
|
+
return {
|
|
1926
|
+
success: true,
|
|
1927
|
+
message: "Extension validated successfully",
|
|
1928
|
+
extension
|
|
1929
|
+
};
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
return {
|
|
1932
|
+
success: false,
|
|
1933
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1934
|
+
extension
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
const composeWorkflowInputSchema = z.object({
|
|
1940
|
+
workflowKey: z.string().describe("Base workflow meta.key"),
|
|
1941
|
+
tenantId: z.string().optional(),
|
|
1942
|
+
role: z.string().optional(),
|
|
1943
|
+
extensions: z.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
|
|
1944
|
+
});
|
|
1945
|
+
const composeWorkflowTool = tool({
|
|
1946
|
+
description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
|
|
1947
|
+
inputSchema: composeWorkflowInputSchema,
|
|
1948
|
+
execute: async (input) => {
|
|
1949
|
+
const base = baseByKey.get(input.workflowKey);
|
|
1950
|
+
if (!base) {
|
|
1951
|
+
return {
|
|
1952
|
+
success: false,
|
|
1953
|
+
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
const comp = composer ?? new WorkflowComposer;
|
|
1957
|
+
if (input.extensions?.length) {
|
|
1958
|
+
for (const ext of input.extensions) {
|
|
1959
|
+
comp.register({
|
|
1960
|
+
workflow: ext.workflow,
|
|
1961
|
+
tenantId: ext.tenantId,
|
|
1962
|
+
role: ext.role,
|
|
1963
|
+
priority: ext.priority,
|
|
1964
|
+
customSteps: ext.customSteps,
|
|
1965
|
+
hiddenSteps: ext.hiddenSteps
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
try {
|
|
1970
|
+
const composed = comp.compose({
|
|
1971
|
+
base,
|
|
1972
|
+
tenantId: input.tenantId,
|
|
1973
|
+
role: input.role
|
|
1974
|
+
});
|
|
1975
|
+
return {
|
|
1976
|
+
success: true,
|
|
1977
|
+
workflow: composed,
|
|
1978
|
+
meta: composed.meta,
|
|
1979
|
+
stepIds: composed.definition.steps.map((s) => s.id)
|
|
1980
|
+
};
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
return {
|
|
1983
|
+
success: false,
|
|
1984
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
const generateWorkflowSpecCodeInputSchema = z.object({
|
|
1990
|
+
workflowKey: z.string().describe("Workflow meta.key"),
|
|
1991
|
+
composedSteps: z.array(z.object({
|
|
1992
|
+
id: z.string(),
|
|
1993
|
+
type: z.enum(["human", "automation", "decision"]),
|
|
1994
|
+
label: z.string(),
|
|
1995
|
+
description: z.string().optional()
|
|
1996
|
+
})).optional().describe("Steps to include; if omitted, uses the base workflow")
|
|
1997
|
+
});
|
|
1998
|
+
const generateWorkflowSpecCodeTool = tool({
|
|
1999
|
+
description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
|
|
2000
|
+
inputSchema: generateWorkflowSpecCodeInputSchema,
|
|
2001
|
+
execute: async (input) => {
|
|
2002
|
+
const base = baseByKey.get(input.workflowKey);
|
|
2003
|
+
if (!base) {
|
|
2004
|
+
return {
|
|
2005
|
+
success: false,
|
|
2006
|
+
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
2007
|
+
code: null
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
const steps = input.composedSteps ?? base.definition.steps;
|
|
2011
|
+
const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
|
|
2012
|
+
const stepsCode = steps.map((s) => ` {
|
|
2013
|
+
id: '${s.id}',
|
|
2014
|
+
type: '${s.type}',
|
|
2015
|
+
label: '${escapeString(s.label)}',${s.description ? `
|
|
2016
|
+
description: '${escapeString(s.description)}',` : ""}
|
|
2017
|
+
}`).join(`,
|
|
2018
|
+
`);
|
|
2019
|
+
const meta = base.meta;
|
|
2020
|
+
const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
|
|
2021
|
+
const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow';
|
|
2022
|
+
|
|
2023
|
+
/**
|
|
2024
|
+
* Workflow: ${base.meta.key}
|
|
2025
|
+
* Generated via AI chat workflow tools.
|
|
2026
|
+
*/
|
|
2027
|
+
export const ${specVarName}: WorkflowSpec = {
|
|
2028
|
+
meta: {
|
|
2029
|
+
key: '${base.meta.key}',
|
|
2030
|
+
version: '${String(base.meta.version)}',
|
|
2031
|
+
title: '${escapeString(meta.title ?? base.meta.key)}',
|
|
2032
|
+
description: '${escapeString(meta.description ?? "")}',
|
|
2033
|
+
},
|
|
2034
|
+
definition: {
|
|
2035
|
+
entryStepId: '${base.definition.entryStepId ?? base.definition.steps[0]?.id ?? ""}',
|
|
2036
|
+
steps: [
|
|
2037
|
+
${stepsCode}
|
|
2038
|
+
],
|
|
2039
|
+
transitions: ${transitionsJson},
|
|
2040
|
+
},
|
|
2041
|
+
};
|
|
2042
|
+
`;
|
|
2043
|
+
return {
|
|
2044
|
+
success: true,
|
|
2045
|
+
code,
|
|
2046
|
+
workflowKey: input.workflowKey
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
return {
|
|
2051
|
+
create_workflow_extension: createWorkflowExtensionTool,
|
|
2052
|
+
compose_workflow: composeWorkflowTool,
|
|
2053
|
+
generate_workflow_spec_code: generateWorkflowSpecCodeTool
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
function toPascalCase(value) {
|
|
2057
|
+
return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
2058
|
+
}
|
|
2059
|
+
function escapeString(value) {
|
|
2060
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// src/core/contracts-context.ts
|
|
2064
|
+
function buildContractsContextPrompt(config) {
|
|
2065
|
+
const parts = [];
|
|
2066
|
+
if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
|
|
2067
|
+
return "";
|
|
2068
|
+
}
|
|
2069
|
+
parts.push(`
|
|
2070
|
+
|
|
2071
|
+
## Available resources`);
|
|
2072
|
+
if (config.agentSpecs?.length) {
|
|
2073
|
+
parts.push(`
|
|
2074
|
+
### Agent tools`);
|
|
2075
|
+
for (const agent of config.agentSpecs) {
|
|
2076
|
+
const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
|
|
2077
|
+
parts.push(`- **${agent.key}**: tools: ${toolNames}`);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
if (config.dataViewSpecs?.length) {
|
|
2081
|
+
parts.push(`
|
|
2082
|
+
### Data views`);
|
|
2083
|
+
for (const dv of config.dataViewSpecs) {
|
|
2084
|
+
parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (config.formSpecs?.length) {
|
|
2088
|
+
parts.push(`
|
|
2089
|
+
### Forms`);
|
|
2090
|
+
for (const form of config.formSpecs) {
|
|
2091
|
+
parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (config.presentationSpecs?.length) {
|
|
2095
|
+
parts.push(`
|
|
2096
|
+
### Presentations`);
|
|
2097
|
+
for (const pres of config.presentationSpecs) {
|
|
2098
|
+
parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
if (config.operationRefs?.length) {
|
|
2102
|
+
parts.push(`
|
|
2103
|
+
### Operations`);
|
|
2104
|
+
for (const op of config.operationRefs) {
|
|
2105
|
+
parts.push(`- **${op.key}@${op.version}**`);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
parts.push(`
|
|
2109
|
+
Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
|
|
2110
|
+
return parts.join(`
|
|
2111
|
+
`);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// src/core/agent-tools-adapter.ts
|
|
2115
|
+
import { tool as tool2 } from "ai";
|
|
2116
|
+
import { z as z2 } from "zod";
|
|
2117
|
+
function getInputSchema(_schema) {
|
|
2118
|
+
return z2.object({}).passthrough();
|
|
2119
|
+
}
|
|
2120
|
+
function agentToolConfigsToToolSet(configs, handlers) {
|
|
2121
|
+
const result = {};
|
|
2122
|
+
for (const config of configs) {
|
|
2123
|
+
const handler = handlers?.[config.name];
|
|
2124
|
+
const inputSchema = getInputSchema(config.schema);
|
|
2125
|
+
result[config.name] = tool2({
|
|
2126
|
+
description: config.description ?? config.name,
|
|
2127
|
+
inputSchema,
|
|
2128
|
+
execute: async (input) => {
|
|
2129
|
+
if (!handler) {
|
|
2130
|
+
return {
|
|
2131
|
+
status: "unimplemented",
|
|
2132
|
+
message: "Wire handler in host",
|
|
2133
|
+
toolName: config.name
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
try {
|
|
2137
|
+
const output = await Promise.resolve(handler(input));
|
|
2138
|
+
return typeof output === "string" ? output : output;
|
|
2139
|
+
} catch (err) {
|
|
2140
|
+
return {
|
|
2141
|
+
status: "error",
|
|
2142
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2143
|
+
toolName: config.name
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
return result;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// src/core/surface-planner-tools.ts
|
|
2153
|
+
import { tool as tool3 } from "ai";
|
|
2154
|
+
import { z as z3 } from "zod";
|
|
2155
|
+
import {
|
|
2156
|
+
validatePatchProposal
|
|
2157
|
+
} from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
|
|
2158
|
+
import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
|
|
2159
|
+
var VALID_OPS = [
|
|
2160
|
+
"insert-node",
|
|
2161
|
+
"replace-node",
|
|
2162
|
+
"remove-node",
|
|
2163
|
+
"move-node",
|
|
2164
|
+
"resize-panel",
|
|
2165
|
+
"set-layout",
|
|
2166
|
+
"reveal-field",
|
|
2167
|
+
"hide-field",
|
|
2168
|
+
"promote-action",
|
|
2169
|
+
"set-focus"
|
|
2170
|
+
];
|
|
2171
|
+
var DEFAULT_NODE_KINDS = [
|
|
2172
|
+
"entity-section",
|
|
2173
|
+
"entity-card",
|
|
2174
|
+
"data-view",
|
|
2175
|
+
"assistant-panel",
|
|
2176
|
+
"chat-thread",
|
|
2177
|
+
"action-bar",
|
|
2178
|
+
"timeline",
|
|
2179
|
+
"table",
|
|
2180
|
+
"rich-doc",
|
|
2181
|
+
"form",
|
|
2182
|
+
"chart",
|
|
2183
|
+
"custom-widget"
|
|
2184
|
+
];
|
|
2185
|
+
function collectSlotIdsFromRegion(node) {
|
|
2186
|
+
const ids = [];
|
|
2187
|
+
if (node.type === "slot") {
|
|
2188
|
+
ids.push(node.slotId);
|
|
2189
|
+
}
|
|
2190
|
+
if (node.type === "panel-group" || node.type === "stack") {
|
|
2191
|
+
for (const child of node.children) {
|
|
2192
|
+
ids.push(...collectSlotIdsFromRegion(child));
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
if (node.type === "tabs") {
|
|
2196
|
+
for (const tab of node.tabs) {
|
|
2197
|
+
ids.push(...collectSlotIdsFromRegion(tab.child));
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
if (node.type === "floating") {
|
|
2201
|
+
ids.push(node.anchorSlotId);
|
|
2202
|
+
ids.push(...collectSlotIdsFromRegion(node.child));
|
|
2203
|
+
}
|
|
2204
|
+
return ids;
|
|
2205
|
+
}
|
|
2206
|
+
function deriveConstraints(plan) {
|
|
2207
|
+
const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
|
|
2208
|
+
const uniqueSlots = [...new Set(slotIds)];
|
|
2209
|
+
return {
|
|
2210
|
+
allowedOps: VALID_OPS,
|
|
2211
|
+
allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
|
|
2212
|
+
allowedNodeKinds: DEFAULT_NODE_KINDS
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
var ProposePatchInputSchema = z3.object({
|
|
2216
|
+
proposalId: z3.string().describe("Unique proposal identifier"),
|
|
2217
|
+
ops: z3.array(z3.object({
|
|
2218
|
+
op: z3.enum([
|
|
2219
|
+
"insert-node",
|
|
2220
|
+
"replace-node",
|
|
2221
|
+
"remove-node",
|
|
2222
|
+
"move-node",
|
|
2223
|
+
"resize-panel",
|
|
2224
|
+
"set-layout",
|
|
2225
|
+
"reveal-field",
|
|
2226
|
+
"hide-field",
|
|
2227
|
+
"promote-action",
|
|
2228
|
+
"set-focus"
|
|
2229
|
+
]),
|
|
2230
|
+
slotId: z3.string().optional(),
|
|
2231
|
+
nodeId: z3.string().optional(),
|
|
2232
|
+
toSlotId: z3.string().optional(),
|
|
2233
|
+
index: z3.number().optional(),
|
|
2234
|
+
node: z3.object({
|
|
2235
|
+
nodeId: z3.string(),
|
|
2236
|
+
kind: z3.string(),
|
|
2237
|
+
title: z3.string().optional(),
|
|
2238
|
+
props: z3.record(z3.string(), z3.unknown()).optional(),
|
|
2239
|
+
children: z3.array(z3.unknown()).optional()
|
|
2240
|
+
}).optional(),
|
|
2241
|
+
persistKey: z3.string().optional(),
|
|
2242
|
+
sizes: z3.array(z3.number()).optional(),
|
|
2243
|
+
layoutId: z3.string().optional(),
|
|
2244
|
+
fieldId: z3.string().optional(),
|
|
2245
|
+
actionId: z3.string().optional(),
|
|
2246
|
+
placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
|
|
2247
|
+
targetId: z3.string().optional()
|
|
2248
|
+
}))
|
|
2249
|
+
});
|
|
2250
|
+
function createSurfacePlannerTools(config) {
|
|
2251
|
+
const { plan, constraints, onPatchProposal } = config;
|
|
2252
|
+
const resolvedConstraints = constraints ?? deriveConstraints(plan);
|
|
2253
|
+
const proposePatchTool = tool3({
|
|
2254
|
+
description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
|
|
2255
|
+
inputSchema: ProposePatchInputSchema,
|
|
2256
|
+
execute: async (input) => {
|
|
2257
|
+
const ops = input.ops;
|
|
2258
|
+
try {
|
|
2259
|
+
validatePatchProposal(ops, resolvedConstraints);
|
|
2260
|
+
const proposal = buildSurfacePatchProposal(input.proposalId, ops);
|
|
2261
|
+
onPatchProposal?.(proposal);
|
|
2262
|
+
return {
|
|
2263
|
+
success: true,
|
|
2264
|
+
proposalId: proposal.proposalId,
|
|
2265
|
+
opsCount: proposal.ops.length,
|
|
2266
|
+
message: "Patch proposal validated; awaiting user approval"
|
|
2267
|
+
};
|
|
2268
|
+
} catch (err) {
|
|
2269
|
+
return {
|
|
2270
|
+
success: false,
|
|
2271
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2272
|
+
proposalId: input.proposalId
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
});
|
|
2277
|
+
return {
|
|
2278
|
+
"propose-patch": proposePatchTool
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
function buildPlannerPromptInput(plan) {
|
|
2282
|
+
const constraints = deriveConstraints(plan);
|
|
2283
|
+
return {
|
|
2284
|
+
bundleMeta: {
|
|
2285
|
+
key: plan.bundleKey,
|
|
2286
|
+
version: "0.0.0",
|
|
2287
|
+
title: plan.bundleKey
|
|
2288
|
+
},
|
|
2289
|
+
surfaceId: plan.surfaceId,
|
|
2290
|
+
allowedPatchOps: constraints.allowedOps,
|
|
2291
|
+
allowedSlots: [...constraints.allowedSlots],
|
|
2292
|
+
allowedNodeKinds: [...constraints.allowedNodeKinds],
|
|
2293
|
+
actions: plan.actions.map((a) => ({
|
|
2294
|
+
actionId: a.actionId,
|
|
2295
|
+
title: a.title
|
|
2296
|
+
})),
|
|
2297
|
+
preferences: {
|
|
2298
|
+
guidance: "hints",
|
|
2299
|
+
density: "standard",
|
|
2300
|
+
dataDepth: "detailed",
|
|
2301
|
+
control: "standard",
|
|
2302
|
+
media: "text",
|
|
2303
|
+
pace: "balanced",
|
|
2304
|
+
narrative: "top-down"
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/core/chat-service.ts
|
|
2310
|
+
import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
|
|
2311
|
+
var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
|
|
2312
|
+
|
|
2313
|
+
Your capabilities:
|
|
2314
|
+
- Help users create, modify, and understand ContractSpec specifications
|
|
2315
|
+
- Generate code that follows ContractSpec patterns and best practices
|
|
2316
|
+
- Explain concepts from the ContractSpec documentation
|
|
2317
|
+
- Suggest improvements and identify issues in specs and implementations
|
|
2318
|
+
|
|
2319
|
+
Guidelines:
|
|
2320
|
+
- Be concise but thorough
|
|
2321
|
+
- Provide code examples when helpful
|
|
2322
|
+
- Reference relevant ContractSpec concepts and patterns
|
|
2323
|
+
- Ask clarifying questions when the user's intent is unclear
|
|
2324
|
+
- When suggesting code changes, explain the rationale`;
|
|
2325
|
+
var WORKFLOW_TOOLS_PROMPT = `
|
|
2326
|
+
|
|
2327
|
+
Workflow creation: You can create and modify workflows. Use create_workflow_extension when the user asks to add steps, change a workflow, or create a tenant-specific extension. Use compose_workflow to apply extensions to a base workflow. Use generate_workflow_spec_code to output TypeScript for the user to save.`;
|
|
2328
|
+
|
|
2329
|
+
class ChatService {
|
|
2330
|
+
provider;
|
|
2331
|
+
context;
|
|
2332
|
+
store;
|
|
2333
|
+
systemPrompt;
|
|
2334
|
+
maxHistoryMessages;
|
|
2335
|
+
onUsage;
|
|
2336
|
+
tools;
|
|
2337
|
+
thinkingLevel;
|
|
2338
|
+
sendReasoning;
|
|
2339
|
+
sendSources;
|
|
2340
|
+
modelSelector;
|
|
2341
|
+
constructor(config) {
|
|
2342
|
+
this.provider = config.provider;
|
|
2343
|
+
this.context = config.context;
|
|
2344
|
+
this.store = config.store ?? new InMemoryConversationStore;
|
|
2345
|
+
this.systemPrompt = this.buildSystemPrompt(config);
|
|
2346
|
+
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
2347
|
+
this.onUsage = config.onUsage;
|
|
2348
|
+
this.tools = this.mergeTools(config);
|
|
2349
|
+
this.thinkingLevel = config.thinkingLevel;
|
|
2350
|
+
this.modelSelector = config.modelSelector;
|
|
2351
|
+
this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
|
|
2352
|
+
this.sendSources = config.sendSources ?? false;
|
|
2353
|
+
}
|
|
2354
|
+
buildSystemPrompt(config) {
|
|
2355
|
+
let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
2356
|
+
if (config.workflowToolsConfig?.baseWorkflows?.length) {
|
|
2357
|
+
base += WORKFLOW_TOOLS_PROMPT;
|
|
2358
|
+
}
|
|
2359
|
+
const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
|
|
2360
|
+
if (contractsPrompt) {
|
|
2361
|
+
base += contractsPrompt;
|
|
2362
|
+
}
|
|
2363
|
+
if (config.surfacePlanConfig?.plan) {
|
|
2364
|
+
const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
|
|
2365
|
+
base += `
|
|
2366
|
+
|
|
2367
|
+
` + compilePlannerPrompt(plannerInput);
|
|
2368
|
+
}
|
|
2369
|
+
return base;
|
|
2370
|
+
}
|
|
2371
|
+
mergeTools(config) {
|
|
2372
|
+
let merged = config.tools ?? {};
|
|
2373
|
+
const wfConfig = config.workflowToolsConfig;
|
|
2374
|
+
if (wfConfig?.baseWorkflows?.length) {
|
|
2375
|
+
const workflowTools = createWorkflowTools({
|
|
2376
|
+
baseWorkflows: wfConfig.baseWorkflows,
|
|
2377
|
+
composer: wfConfig.composer
|
|
2378
|
+
});
|
|
2379
|
+
merged = { ...merged, ...workflowTools };
|
|
2380
|
+
}
|
|
2381
|
+
const contractsCtx = config.contractsContext;
|
|
2382
|
+
if (contractsCtx?.agentSpecs?.length) {
|
|
2383
|
+
const allTools = [];
|
|
2384
|
+
for (const agent of contractsCtx.agentSpecs) {
|
|
2385
|
+
if (agent.tools?.length)
|
|
2386
|
+
allTools.push(...agent.tools);
|
|
2387
|
+
}
|
|
2388
|
+
if (allTools.length > 0) {
|
|
2389
|
+
const agentTools = agentToolConfigsToToolSet(allTools);
|
|
2390
|
+
merged = { ...merged, ...agentTools };
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
const surfaceConfig = config.surfacePlanConfig;
|
|
2394
|
+
if (surfaceConfig?.plan) {
|
|
2395
|
+
const plannerTools = createSurfacePlannerTools({
|
|
2396
|
+
plan: surfaceConfig.plan,
|
|
2397
|
+
onPatchProposal: surfaceConfig.onPatchProposal
|
|
2398
|
+
});
|
|
2399
|
+
merged = { ...merged, ...plannerTools };
|
|
2400
|
+
}
|
|
2401
|
+
if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
|
|
2402
|
+
merged = { ...merged, ...config.mcpTools };
|
|
2403
|
+
}
|
|
2404
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
2405
|
+
}
|
|
2406
|
+
async resolveModel() {
|
|
2407
|
+
if (this.modelSelector) {
|
|
2408
|
+
const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
|
|
2409
|
+
const { model, selection } = await this.modelSelector.selectAndCreate({
|
|
2410
|
+
taskDimension: dimension
|
|
2411
|
+
});
|
|
2412
|
+
return { model, providerName: selection.providerKey };
|
|
2413
|
+
}
|
|
2414
|
+
return {
|
|
2415
|
+
model: this.provider.getModel(),
|
|
2416
|
+
providerName: this.provider.name
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
thinkingLevelToDimension(level) {
|
|
2420
|
+
if (!level || level === "instant")
|
|
2421
|
+
return "latency";
|
|
2422
|
+
return "reasoning";
|
|
2423
|
+
}
|
|
2424
|
+
async send(options) {
|
|
2425
|
+
let conversation;
|
|
2426
|
+
if (options.conversationId) {
|
|
2427
|
+
const existing = await this.store.get(options.conversationId);
|
|
2428
|
+
if (!existing) {
|
|
2429
|
+
throw new Error(`Conversation ${options.conversationId} not found`);
|
|
2430
|
+
}
|
|
2431
|
+
conversation = existing;
|
|
2432
|
+
} else {
|
|
2433
|
+
conversation = await this.store.create({
|
|
2434
|
+
status: "active",
|
|
2435
|
+
provider: this.provider.name,
|
|
2436
|
+
model: this.provider.model,
|
|
2437
|
+
messages: [],
|
|
2438
|
+
workspacePath: this.context?.workspacePath
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
if (!options.skipUserAppend) {
|
|
2442
|
+
await this.store.appendMessage(conversation.id, {
|
|
2443
|
+
role: "user",
|
|
2444
|
+
content: options.content,
|
|
2445
|
+
status: "completed",
|
|
2446
|
+
attachments: options.attachments
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
2450
|
+
const messages = this.buildMessages(conversation, options);
|
|
2451
|
+
const { model, providerName } = await this.resolveModel();
|
|
2452
|
+
const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
2453
|
+
try {
|
|
2454
|
+
const result = await generateText({
|
|
2455
|
+
model,
|
|
2456
|
+
messages,
|
|
2457
|
+
system: this.systemPrompt,
|
|
2458
|
+
tools: this.tools,
|
|
2459
|
+
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
2460
|
+
});
|
|
2461
|
+
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
2462
|
+
role: "assistant",
|
|
2463
|
+
content: result.text,
|
|
2464
|
+
status: "completed"
|
|
2465
|
+
});
|
|
2466
|
+
const updatedConversation = await this.store.get(conversation.id);
|
|
2467
|
+
if (!updatedConversation) {
|
|
2468
|
+
throw new Error("Conversation lost after update");
|
|
2469
|
+
}
|
|
2470
|
+
return {
|
|
2471
|
+
message: assistantMessage,
|
|
2472
|
+
conversation: updatedConversation
|
|
2473
|
+
};
|
|
2474
|
+
} catch (error) {
|
|
2475
|
+
await this.store.appendMessage(conversation.id, {
|
|
2476
|
+
role: "assistant",
|
|
2477
|
+
content: "",
|
|
2478
|
+
status: "error",
|
|
2479
|
+
error: {
|
|
2480
|
+
code: "generation_failed",
|
|
2481
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
throw error;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
async stream(options) {
|
|
2488
|
+
let conversation;
|
|
2489
|
+
if (options.conversationId) {
|
|
2490
|
+
const existing = await this.store.get(options.conversationId);
|
|
2491
|
+
if (!existing) {
|
|
2492
|
+
throw new Error(`Conversation ${options.conversationId} not found`);
|
|
2493
|
+
}
|
|
2494
|
+
conversation = existing;
|
|
2495
|
+
} else {
|
|
2496
|
+
conversation = await this.store.create({
|
|
2497
|
+
status: "active",
|
|
2498
|
+
provider: this.provider.name,
|
|
2499
|
+
model: this.provider.model,
|
|
2500
|
+
messages: [],
|
|
2501
|
+
workspacePath: this.context?.workspacePath
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
if (!options.skipUserAppend) {
|
|
2505
|
+
await this.store.appendMessage(conversation.id, {
|
|
2506
|
+
role: "user",
|
|
2507
|
+
content: options.content,
|
|
2508
|
+
status: "completed",
|
|
2509
|
+
attachments: options.attachments
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
2513
|
+
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
2514
|
+
role: "assistant",
|
|
2515
|
+
content: "",
|
|
2516
|
+
status: "streaming"
|
|
2517
|
+
});
|
|
2518
|
+
const messages = this.buildMessages(conversation, options);
|
|
2519
|
+
const { model, providerName } = await this.resolveModel();
|
|
2520
|
+
const systemPrompt = this.systemPrompt;
|
|
2521
|
+
const tools = this.tools;
|
|
2522
|
+
const store = this.store;
|
|
2523
|
+
const onUsage = this.onUsage;
|
|
2524
|
+
const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
2525
|
+
async function* streamGenerator() {
|
|
2526
|
+
let fullContent = "";
|
|
2527
|
+
let fullReasoning = "";
|
|
2528
|
+
const toolCallsMap = new Map;
|
|
2529
|
+
const sources = [];
|
|
2530
|
+
try {
|
|
2531
|
+
const result = streamText({
|
|
2532
|
+
model,
|
|
2533
|
+
messages,
|
|
2534
|
+
system: systemPrompt,
|
|
2535
|
+
tools,
|
|
2536
|
+
providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
|
|
2537
|
+
});
|
|
2538
|
+
for await (const part of result.fullStream) {
|
|
2539
|
+
if (part.type === "text-delta") {
|
|
2540
|
+
const text = part.text ?? "";
|
|
2541
|
+
if (text) {
|
|
2542
|
+
fullContent += text;
|
|
2543
|
+
yield { type: "text", content: text };
|
|
2544
|
+
}
|
|
2545
|
+
} else if (part.type === "reasoning-delta") {
|
|
2546
|
+
const text = part.text ?? "";
|
|
2547
|
+
if (text) {
|
|
2548
|
+
fullReasoning += text;
|
|
2549
|
+
yield { type: "reasoning", content: text };
|
|
2550
|
+
}
|
|
2551
|
+
} else if (part.type === "source") {
|
|
2552
|
+
const src = part;
|
|
2553
|
+
const source = {
|
|
2554
|
+
id: src.id,
|
|
2555
|
+
title: src.title ?? "",
|
|
2556
|
+
url: src.url,
|
|
2557
|
+
type: "web"
|
|
2558
|
+
};
|
|
2559
|
+
sources.push(source);
|
|
2560
|
+
yield { type: "source", source };
|
|
2561
|
+
} else if (part.type === "tool-call") {
|
|
2562
|
+
const toolCall = {
|
|
2563
|
+
id: part.toolCallId,
|
|
2564
|
+
name: part.toolName,
|
|
2565
|
+
args: part.input ?? {},
|
|
2566
|
+
status: "running"
|
|
2567
|
+
};
|
|
2568
|
+
toolCallsMap.set(part.toolCallId, toolCall);
|
|
2569
|
+
yield { type: "tool_call", toolCall };
|
|
2570
|
+
} else if (part.type === "tool-result") {
|
|
2571
|
+
const tc = toolCallsMap.get(part.toolCallId);
|
|
2572
|
+
if (tc) {
|
|
2573
|
+
tc.result = part.output;
|
|
2574
|
+
tc.status = "completed";
|
|
2575
|
+
}
|
|
2576
|
+
yield {
|
|
2577
|
+
type: "tool_result",
|
|
2578
|
+
toolResult: {
|
|
2579
|
+
toolCallId: part.toolCallId,
|
|
2580
|
+
toolName: part.toolName,
|
|
2581
|
+
result: part.output
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
} else if (part.type === "tool-error") {
|
|
2585
|
+
const tc = toolCallsMap.get(part.toolCallId);
|
|
2586
|
+
if (tc) {
|
|
2587
|
+
tc.status = "error";
|
|
2588
|
+
tc.error = part.error ?? "Tool execution failed";
|
|
2589
|
+
}
|
|
2590
|
+
} else if (part.type === "finish") {
|
|
2591
|
+
const usage = part.usage;
|
|
2592
|
+
const inputTokens = usage?.inputTokens ?? 0;
|
|
2593
|
+
const outputTokens = usage?.completionTokens ?? 0;
|
|
2594
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
2595
|
+
content: fullContent,
|
|
2596
|
+
status: "completed",
|
|
2597
|
+
reasoning: fullReasoning || undefined,
|
|
2598
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
2599
|
+
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined,
|
|
2600
|
+
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
2601
|
+
});
|
|
2602
|
+
onUsage?.({ inputTokens, outputTokens });
|
|
2603
|
+
yield {
|
|
2604
|
+
type: "done",
|
|
2605
|
+
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
2606
|
+
};
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
2611
|
+
content: fullContent,
|
|
2612
|
+
status: "completed",
|
|
2613
|
+
reasoning: fullReasoning || undefined,
|
|
2614
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
2615
|
+
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined
|
|
2616
|
+
});
|
|
2617
|
+
yield { type: "done" };
|
|
2618
|
+
} catch (error) {
|
|
2619
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
2620
|
+
content: fullContent,
|
|
2621
|
+
status: "error",
|
|
2622
|
+
error: {
|
|
2623
|
+
code: "stream_failed",
|
|
2624
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
yield {
|
|
2628
|
+
type: "error",
|
|
2629
|
+
error: {
|
|
2630
|
+
code: "stream_failed",
|
|
2631
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
return {
|
|
2637
|
+
conversationId: conversation.id,
|
|
2638
|
+
messageId: assistantMessage.id,
|
|
2639
|
+
stream: streamGenerator()
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
async getConversation(conversationId) {
|
|
2643
|
+
return this.store.get(conversationId);
|
|
2644
|
+
}
|
|
2645
|
+
async listConversations(options) {
|
|
2646
|
+
return this.store.list({
|
|
2647
|
+
status: "active",
|
|
2648
|
+
...options
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
async updateConversation(conversationId, updates) {
|
|
2652
|
+
return this.store.update(conversationId, updates);
|
|
2653
|
+
}
|
|
2654
|
+
async forkConversation(conversationId, upToMessageId) {
|
|
2655
|
+
return this.store.fork(conversationId, upToMessageId);
|
|
2656
|
+
}
|
|
2657
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
2658
|
+
return this.store.updateMessage(conversationId, messageId, updates);
|
|
2659
|
+
}
|
|
2660
|
+
async truncateAfter(conversationId, messageId) {
|
|
2661
|
+
return this.store.truncateAfter(conversationId, messageId);
|
|
2662
|
+
}
|
|
2663
|
+
async deleteConversation(conversationId) {
|
|
2664
|
+
return this.store.delete(conversationId);
|
|
2665
|
+
}
|
|
2666
|
+
buildMessages(conversation, _options) {
|
|
2667
|
+
const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
|
|
2668
|
+
const messages = [];
|
|
2669
|
+
for (let i = historyStart;i < conversation.messages.length; i++) {
|
|
2670
|
+
const msg = conversation.messages[i];
|
|
2671
|
+
if (!msg)
|
|
2672
|
+
continue;
|
|
2673
|
+
if (msg.role === "user") {
|
|
2674
|
+
let content = msg.content;
|
|
2675
|
+
if (msg.attachments?.length) {
|
|
2676
|
+
const attachmentInfo = msg.attachments.map((a) => {
|
|
2677
|
+
if (a.type === "file" || a.type === "code") {
|
|
2678
|
+
return `
|
|
2679
|
+
|
|
2680
|
+
### ${a.name}
|
|
2681
|
+
\`\`\`
|
|
2682
|
+
${a.content ?? ""}
|
|
2683
|
+
\`\`\``;
|
|
2684
|
+
}
|
|
2685
|
+
return `
|
|
2686
|
+
|
|
2687
|
+
[Attachment: ${a.name}]`;
|
|
2688
|
+
}).join("");
|
|
2689
|
+
content += attachmentInfo;
|
|
2690
|
+
}
|
|
2691
|
+
messages.push({ role: "user", content });
|
|
2692
|
+
} else if (msg.role === "assistant") {
|
|
2693
|
+
if (msg.toolCalls?.length) {
|
|
2694
|
+
messages.push({
|
|
2695
|
+
role: "assistant",
|
|
2696
|
+
content: msg.content || "",
|
|
2697
|
+
toolCalls: msg.toolCalls.map((tc) => ({
|
|
2698
|
+
type: "tool-call",
|
|
2699
|
+
toolCallId: tc.id,
|
|
2700
|
+
toolName: tc.name,
|
|
2701
|
+
args: tc.args
|
|
2702
|
+
}))
|
|
2703
|
+
});
|
|
2704
|
+
messages.push({
|
|
2705
|
+
role: "tool",
|
|
2706
|
+
content: msg.toolCalls.map((tc) => ({
|
|
2707
|
+
type: "tool-result",
|
|
2708
|
+
toolCallId: tc.id,
|
|
2709
|
+
toolName: tc.name,
|
|
2710
|
+
output: tc.result
|
|
2711
|
+
}))
|
|
2712
|
+
});
|
|
2713
|
+
} else {
|
|
2714
|
+
messages.push({ role: "assistant", content: msg.content });
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
return messages;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
function createChatService(config) {
|
|
2722
|
+
return new ChatService(config);
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
// src/presentation/hooks/useChat.tsx
|
|
2726
|
+
import {
|
|
2727
|
+
createProvider
|
|
2728
|
+
} from "@contractspec/lib.ai-providers";
|
|
2729
|
+
"use client";
|
|
2730
|
+
function toolsToToolSet(defs) {
|
|
2731
|
+
const result = {};
|
|
2732
|
+
for (const def of defs) {
|
|
2733
|
+
result[def.name] = tool4({
|
|
2734
|
+
description: def.description ?? def.name,
|
|
2735
|
+
inputSchema: z4.object({}).passthrough(),
|
|
2736
|
+
execute: async () => ({})
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
return result;
|
|
2740
|
+
}
|
|
2741
|
+
function useChat(options = {}) {
|
|
2742
|
+
const {
|
|
2743
|
+
provider = "openai",
|
|
2744
|
+
mode = "byok",
|
|
2745
|
+
model,
|
|
2746
|
+
apiKey,
|
|
2747
|
+
proxyUrl,
|
|
2748
|
+
conversationId: initialConversationId,
|
|
2749
|
+
store,
|
|
2750
|
+
systemPrompt,
|
|
2751
|
+
streaming = true,
|
|
2752
|
+
onSend,
|
|
2753
|
+
onResponse,
|
|
2754
|
+
onError,
|
|
2755
|
+
onUsage,
|
|
2756
|
+
tools: toolsDefs,
|
|
2757
|
+
thinkingLevel,
|
|
2758
|
+
workflowToolsConfig,
|
|
2759
|
+
modelSelector,
|
|
2760
|
+
contractsContext,
|
|
2761
|
+
surfacePlanConfig,
|
|
2762
|
+
mcpServers,
|
|
2763
|
+
agentMode
|
|
2764
|
+
} = options;
|
|
2765
|
+
const [messages, setMessages] = React11.useState([]);
|
|
2766
|
+
const [mcpTools, setMcpTools] = React11.useState(null);
|
|
2767
|
+
const mcpCleanupRef = React11.useRef(null);
|
|
2768
|
+
const [conversation, setConversation] = React11.useState(null);
|
|
2769
|
+
const [isLoading, setIsLoading] = React11.useState(false);
|
|
2770
|
+
const [error, setError] = React11.useState(null);
|
|
2771
|
+
const [conversationId, setConversationId] = React11.useState(initialConversationId ?? null);
|
|
2772
|
+
const abortControllerRef = React11.useRef(null);
|
|
2773
|
+
const chatServiceRef = React11.useRef(null);
|
|
2774
|
+
React11.useEffect(() => {
|
|
2775
|
+
if (!mcpServers?.length) {
|
|
2776
|
+
setMcpTools(null);
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
let cancelled = false;
|
|
2780
|
+
import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
|
|
2781
|
+
createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
|
|
2782
|
+
if (!cancelled) {
|
|
2783
|
+
setMcpTools(tools);
|
|
2784
|
+
mcpCleanupRef.current = cleanup;
|
|
2785
|
+
} else {
|
|
2786
|
+
cleanup().catch(() => {
|
|
2787
|
+
return;
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
}).catch(() => {
|
|
2791
|
+
if (!cancelled)
|
|
2792
|
+
setMcpTools(null);
|
|
2793
|
+
});
|
|
2794
|
+
});
|
|
2795
|
+
return () => {
|
|
2796
|
+
cancelled = true;
|
|
2797
|
+
const cleanup = mcpCleanupRef.current;
|
|
2798
|
+
mcpCleanupRef.current = null;
|
|
2799
|
+
if (cleanup)
|
|
2800
|
+
cleanup().catch(() => {
|
|
2801
|
+
return;
|
|
2802
|
+
});
|
|
2803
|
+
setMcpTools(null);
|
|
2804
|
+
};
|
|
2805
|
+
}, [mcpServers]);
|
|
2806
|
+
React11.useEffect(() => {
|
|
2807
|
+
const chatProvider = createProvider({
|
|
2808
|
+
provider,
|
|
2809
|
+
model,
|
|
2810
|
+
apiKey,
|
|
2811
|
+
proxyUrl
|
|
2812
|
+
});
|
|
2813
|
+
chatServiceRef.current = new ChatService({
|
|
2814
|
+
provider: chatProvider,
|
|
2815
|
+
store,
|
|
2816
|
+
systemPrompt,
|
|
2817
|
+
onUsage,
|
|
2818
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
|
|
2819
|
+
thinkingLevel,
|
|
2820
|
+
workflowToolsConfig,
|
|
2821
|
+
modelSelector,
|
|
2822
|
+
contractsContext,
|
|
2823
|
+
surfacePlanConfig,
|
|
2824
|
+
mcpTools
|
|
2825
|
+
});
|
|
2826
|
+
}, [
|
|
2827
|
+
provider,
|
|
2828
|
+
mode,
|
|
2829
|
+
model,
|
|
2830
|
+
apiKey,
|
|
2831
|
+
proxyUrl,
|
|
2832
|
+
store,
|
|
2833
|
+
systemPrompt,
|
|
2834
|
+
onUsage,
|
|
2835
|
+
toolsDefs,
|
|
2836
|
+
thinkingLevel,
|
|
2837
|
+
workflowToolsConfig,
|
|
2838
|
+
modelSelector,
|
|
2839
|
+
contractsContext,
|
|
2840
|
+
surfacePlanConfig,
|
|
2841
|
+
mcpTools
|
|
2842
|
+
]);
|
|
2843
|
+
React11.useEffect(() => {
|
|
2844
|
+
if (!conversationId || !chatServiceRef.current)
|
|
2845
|
+
return;
|
|
2846
|
+
const loadConversation = async () => {
|
|
2847
|
+
if (!chatServiceRef.current)
|
|
2848
|
+
return;
|
|
2849
|
+
const conv = await chatServiceRef.current.getConversation(conversationId);
|
|
2850
|
+
if (conv) {
|
|
2851
|
+
setConversation(conv);
|
|
2852
|
+
setMessages(conv.messages);
|
|
2853
|
+
}
|
|
2854
|
+
};
|
|
2855
|
+
loadConversation().catch(console.error);
|
|
2856
|
+
}, [conversationId]);
|
|
2857
|
+
const sendMessage = React11.useCallback(async (content, attachments, opts) => {
|
|
2858
|
+
if (agentMode?.agent) {
|
|
2859
|
+
setIsLoading(true);
|
|
2860
|
+
setError(null);
|
|
2861
|
+
abortControllerRef.current = new AbortController;
|
|
2862
|
+
try {
|
|
2863
|
+
if (!opts?.skipUserAppend) {
|
|
2864
|
+
const userMessage = {
|
|
2865
|
+
id: `msg_${Date.now()}`,
|
|
2866
|
+
conversationId: conversationId ?? "",
|
|
2867
|
+
role: "user",
|
|
2868
|
+
content,
|
|
2869
|
+
status: "completed",
|
|
2870
|
+
createdAt: new Date,
|
|
2871
|
+
updatedAt: new Date,
|
|
2872
|
+
attachments
|
|
2873
|
+
};
|
|
2874
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
2875
|
+
onSend?.(userMessage);
|
|
2876
|
+
}
|
|
2877
|
+
const result = await agentMode.agent.generate({
|
|
2878
|
+
prompt: content,
|
|
2879
|
+
signal: abortControllerRef.current.signal
|
|
2880
|
+
});
|
|
2881
|
+
const toolCallsMap = new Map;
|
|
2882
|
+
for (const tc of result.toolCalls ?? []) {
|
|
2883
|
+
const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
|
|
2884
|
+
toolCallsMap.set(tc.toolCallId, {
|
|
2885
|
+
id: tc.toolCallId,
|
|
2886
|
+
name: tc.toolName,
|
|
2887
|
+
args: tc.args ?? {},
|
|
2888
|
+
result: tr?.output,
|
|
2889
|
+
status: "completed"
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
const assistantMessage = {
|
|
2893
|
+
id: `msg_${Date.now()}_a`,
|
|
2894
|
+
conversationId: conversationId ?? "",
|
|
2895
|
+
role: "assistant",
|
|
2896
|
+
content: result.text,
|
|
2897
|
+
status: "completed",
|
|
2898
|
+
createdAt: new Date,
|
|
2899
|
+
updatedAt: new Date,
|
|
2900
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
2901
|
+
usage: result.usage
|
|
2902
|
+
};
|
|
2903
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
2904
|
+
onResponse?.(assistantMessage);
|
|
2905
|
+
onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
|
|
2906
|
+
if (store && !conversationId) {
|
|
2907
|
+
const conv = await store.create({
|
|
2908
|
+
status: "active",
|
|
2909
|
+
provider: "agent",
|
|
2910
|
+
model: "agent",
|
|
2911
|
+
messages: []
|
|
2912
|
+
});
|
|
2913
|
+
if (!opts?.skipUserAppend) {
|
|
2914
|
+
await store.appendMessage(conv.id, {
|
|
2915
|
+
role: "user",
|
|
2916
|
+
content,
|
|
2917
|
+
status: "completed",
|
|
2918
|
+
attachments
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
await store.appendMessage(conv.id, {
|
|
2922
|
+
role: "assistant",
|
|
2923
|
+
content: result.text,
|
|
2924
|
+
status: "completed",
|
|
2925
|
+
toolCalls: assistantMessage.toolCalls,
|
|
2926
|
+
usage: result.usage
|
|
2927
|
+
});
|
|
2928
|
+
const updated = await store.get(conv.id);
|
|
2929
|
+
if (updated)
|
|
2930
|
+
setConversation(updated);
|
|
2931
|
+
setConversationId(conv.id);
|
|
2932
|
+
}
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2935
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
2936
|
+
} finally {
|
|
2937
|
+
setIsLoading(false);
|
|
2938
|
+
}
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
if (!chatServiceRef.current) {
|
|
2942
|
+
throw new Error("Chat service not initialized");
|
|
2943
|
+
}
|
|
2944
|
+
setIsLoading(true);
|
|
2945
|
+
setError(null);
|
|
2946
|
+
abortControllerRef.current = new AbortController;
|
|
2947
|
+
try {
|
|
2948
|
+
if (!opts?.skipUserAppend) {
|
|
2949
|
+
const userMessage = {
|
|
2950
|
+
id: `msg_${Date.now()}`,
|
|
2951
|
+
conversationId: conversationId ?? "",
|
|
2952
|
+
role: "user",
|
|
2953
|
+
content,
|
|
2954
|
+
status: "completed",
|
|
2955
|
+
createdAt: new Date,
|
|
2956
|
+
updatedAt: new Date,
|
|
2957
|
+
attachments
|
|
2958
|
+
};
|
|
2959
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
2960
|
+
onSend?.(userMessage);
|
|
2961
|
+
}
|
|
2962
|
+
if (streaming) {
|
|
2963
|
+
const result = await chatServiceRef.current.stream({
|
|
2964
|
+
conversationId: conversationId ?? undefined,
|
|
2965
|
+
content,
|
|
2966
|
+
attachments,
|
|
2967
|
+
skipUserAppend: opts?.skipUserAppend
|
|
2968
|
+
});
|
|
2969
|
+
if (!conversationId && !opts?.skipUserAppend) {
|
|
2970
|
+
setConversationId(result.conversationId);
|
|
2971
|
+
}
|
|
2972
|
+
const assistantMessage = {
|
|
2973
|
+
id: result.messageId,
|
|
2974
|
+
conversationId: result.conversationId,
|
|
2975
|
+
role: "assistant",
|
|
2976
|
+
content: "",
|
|
2977
|
+
status: "streaming",
|
|
2978
|
+
createdAt: new Date,
|
|
2979
|
+
updatedAt: new Date
|
|
2980
|
+
};
|
|
2981
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
2982
|
+
let fullContent = "";
|
|
2983
|
+
let fullReasoning = "";
|
|
2984
|
+
const toolCallsMap = new Map;
|
|
2985
|
+
const sources = [];
|
|
2986
|
+
for await (const chunk of result.stream) {
|
|
2987
|
+
if (chunk.type === "text" && chunk.content) {
|
|
2988
|
+
fullContent += chunk.content;
|
|
2989
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
2990
|
+
...m,
|
|
2991
|
+
content: fullContent,
|
|
2992
|
+
reasoning: fullReasoning || undefined,
|
|
2993
|
+
sources: sources.length ? sources : undefined,
|
|
2994
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined
|
|
2995
|
+
} : m));
|
|
2996
|
+
} else if (chunk.type === "reasoning" && chunk.content) {
|
|
2997
|
+
fullReasoning += chunk.content;
|
|
2998
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, reasoning: fullReasoning } : m));
|
|
2999
|
+
} else if (chunk.type === "source" && chunk.source) {
|
|
3000
|
+
sources.push(chunk.source);
|
|
3001
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, sources: [...sources] } : m));
|
|
3002
|
+
} else if (chunk.type === "tool_call" && chunk.toolCall) {
|
|
3003
|
+
const tc = chunk.toolCall;
|
|
3004
|
+
const chatTc = {
|
|
3005
|
+
id: tc.id,
|
|
3006
|
+
name: tc.name,
|
|
3007
|
+
args: tc.args,
|
|
3008
|
+
status: "running"
|
|
3009
|
+
};
|
|
3010
|
+
toolCallsMap.set(tc.id, chatTc);
|
|
3011
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
3012
|
+
} else if (chunk.type === "tool_result" && chunk.toolResult) {
|
|
3013
|
+
const tr = chunk.toolResult;
|
|
3014
|
+
const tc = toolCallsMap.get(tr.toolCallId);
|
|
3015
|
+
if (tc) {
|
|
3016
|
+
tc.result = tr.result;
|
|
3017
|
+
tc.status = "completed";
|
|
3018
|
+
}
|
|
3019
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
3020
|
+
} else if (chunk.type === "done") {
|
|
3021
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
3022
|
+
...m,
|
|
3023
|
+
content: fullContent,
|
|
3024
|
+
reasoning: fullReasoning || undefined,
|
|
3025
|
+
sources: sources.length ? sources : undefined,
|
|
3026
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
3027
|
+
status: "completed",
|
|
3028
|
+
usage: chunk.usage,
|
|
3029
|
+
updatedAt: new Date
|
|
3030
|
+
} : m));
|
|
3031
|
+
onResponse?.(messages.find((m) => m.id === result.messageId) ?? assistantMessage);
|
|
3032
|
+
} else if (chunk.type === "error") {
|
|
3033
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
3034
|
+
...m,
|
|
3035
|
+
status: "error",
|
|
3036
|
+
error: chunk.error,
|
|
3037
|
+
updatedAt: new Date
|
|
3038
|
+
} : m));
|
|
3039
|
+
if (chunk.error) {
|
|
3040
|
+
const err = new Error(chunk.error.message);
|
|
3041
|
+
setError(err);
|
|
3042
|
+
onError?.(err);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
} else {
|
|
3047
|
+
const result = await chatServiceRef.current.send({
|
|
3048
|
+
conversationId: conversationId ?? undefined,
|
|
3049
|
+
content,
|
|
3050
|
+
attachments,
|
|
3051
|
+
skipUserAppend: opts?.skipUserAppend
|
|
3052
|
+
});
|
|
3053
|
+
setConversation(result.conversation);
|
|
3054
|
+
setMessages(result.conversation.messages);
|
|
3055
|
+
if (!conversationId) {
|
|
3056
|
+
setConversationId(result.conversation.id);
|
|
3057
|
+
}
|
|
3058
|
+
onResponse?.(result.message);
|
|
3059
|
+
}
|
|
3060
|
+
} catch (err) {
|
|
3061
|
+
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
3062
|
+
setError(error2);
|
|
3063
|
+
onError?.(error2);
|
|
3064
|
+
} finally {
|
|
3065
|
+
setIsLoading(false);
|
|
3066
|
+
abortControllerRef.current = null;
|
|
3067
|
+
}
|
|
3068
|
+
}, [
|
|
3069
|
+
conversationId,
|
|
3070
|
+
streaming,
|
|
3071
|
+
onSend,
|
|
3072
|
+
onResponse,
|
|
3073
|
+
onError,
|
|
3074
|
+
onUsage,
|
|
3075
|
+
messages,
|
|
3076
|
+
agentMode,
|
|
3077
|
+
store
|
|
3078
|
+
]);
|
|
3079
|
+
const clearConversation = React11.useCallback(() => {
|
|
3080
|
+
setMessages([]);
|
|
3081
|
+
setConversation(null);
|
|
3082
|
+
setConversationId(null);
|
|
3083
|
+
setError(null);
|
|
3084
|
+
}, []);
|
|
3085
|
+
const regenerate = React11.useCallback(async () => {
|
|
3086
|
+
const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
|
|
3087
|
+
if (lastUserMessageIndex === -1)
|
|
3088
|
+
return;
|
|
3089
|
+
const lastUserMessage = messages[lastUserMessageIndex];
|
|
3090
|
+
if (!lastUserMessage)
|
|
3091
|
+
return;
|
|
3092
|
+
setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
|
|
3093
|
+
await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
|
|
3094
|
+
}, [messages, sendMessage]);
|
|
3095
|
+
const stop = React11.useCallback(() => {
|
|
3096
|
+
abortControllerRef.current?.abort();
|
|
3097
|
+
setIsLoading(false);
|
|
3098
|
+
}, []);
|
|
3099
|
+
const createNewConversation = clearConversation;
|
|
3100
|
+
const editMessage = React11.useCallback(async (messageId, newContent) => {
|
|
3101
|
+
if (!chatServiceRef.current || !conversationId)
|
|
3102
|
+
return;
|
|
3103
|
+
const msg = messages.find((m) => m.id === messageId);
|
|
3104
|
+
if (!msg || msg.role !== "user")
|
|
3105
|
+
return;
|
|
3106
|
+
await chatServiceRef.current.updateMessage(conversationId, messageId, {
|
|
3107
|
+
content: newContent
|
|
3108
|
+
});
|
|
3109
|
+
const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
|
|
3110
|
+
if (truncated) {
|
|
3111
|
+
setMessages(truncated.messages);
|
|
3112
|
+
}
|
|
3113
|
+
await sendMessage(newContent, undefined, { skipUserAppend: true });
|
|
3114
|
+
}, [conversationId, messages, sendMessage]);
|
|
3115
|
+
const forkConversation = React11.useCallback(async (upToMessageId) => {
|
|
3116
|
+
if (!chatServiceRef.current)
|
|
3117
|
+
return null;
|
|
3118
|
+
const idToFork = conversationId ?? conversation?.id;
|
|
3119
|
+
if (!idToFork)
|
|
3120
|
+
return null;
|
|
3121
|
+
try {
|
|
3122
|
+
const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
|
|
3123
|
+
setConversationId(forked.id);
|
|
3124
|
+
setConversation(forked);
|
|
3125
|
+
setMessages(forked.messages);
|
|
3126
|
+
return forked.id;
|
|
3127
|
+
} catch {
|
|
3128
|
+
return null;
|
|
3129
|
+
}
|
|
3130
|
+
}, [conversationId, conversation]);
|
|
3131
|
+
const updateConversationFn = React11.useCallback(async (updates) => {
|
|
3132
|
+
if (!chatServiceRef.current || !conversationId)
|
|
3133
|
+
return null;
|
|
3134
|
+
const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
|
|
3135
|
+
if (updated)
|
|
3136
|
+
setConversation(updated);
|
|
3137
|
+
return updated;
|
|
3138
|
+
}, [conversationId]);
|
|
3139
|
+
const addToolApprovalResponse = React11.useCallback((_toolCallId, _result) => {
|
|
3140
|
+
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
3141
|
+
}, []);
|
|
3142
|
+
const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
|
|
3143
|
+
return {
|
|
3144
|
+
messages,
|
|
3145
|
+
conversation,
|
|
3146
|
+
isLoading,
|
|
3147
|
+
error,
|
|
3148
|
+
sendMessage,
|
|
3149
|
+
clearConversation,
|
|
3150
|
+
setConversationId,
|
|
3151
|
+
regenerate,
|
|
3152
|
+
stop,
|
|
3153
|
+
createNewConversation,
|
|
3154
|
+
editMessage,
|
|
3155
|
+
forkConversation,
|
|
3156
|
+
updateConversation: updateConversationFn,
|
|
3157
|
+
...hasApprovalTools && { addToolApprovalResponse }
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
// src/core/local-storage-conversation-store.ts
|
|
3162
|
+
var DEFAULT_KEY = "contractspec:ai-chat:conversations";
|
|
3163
|
+
function generateId2(prefix) {
|
|
3164
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
3165
|
+
}
|
|
3166
|
+
function toSerializable(conv) {
|
|
3167
|
+
return {
|
|
3168
|
+
...conv,
|
|
3169
|
+
createdAt: conv.createdAt.toISOString(),
|
|
3170
|
+
updatedAt: conv.updatedAt.toISOString(),
|
|
3171
|
+
messages: conv.messages.map((m) => ({
|
|
3172
|
+
...m,
|
|
3173
|
+
createdAt: m.createdAt.toISOString(),
|
|
3174
|
+
updatedAt: m.updatedAt.toISOString()
|
|
3175
|
+
}))
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
function fromSerializable(raw) {
|
|
3179
|
+
const messages = raw.messages?.map((m) => ({
|
|
3180
|
+
...m,
|
|
3181
|
+
createdAt: new Date(m.createdAt),
|
|
3182
|
+
updatedAt: new Date(m.updatedAt)
|
|
3183
|
+
})) ?? [];
|
|
3184
|
+
return {
|
|
3185
|
+
...raw,
|
|
3186
|
+
createdAt: new Date(raw.createdAt),
|
|
3187
|
+
updatedAt: new Date(raw.updatedAt),
|
|
3188
|
+
messages
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
3191
|
+
function loadAll(key) {
|
|
3192
|
+
if (typeof window === "undefined")
|
|
3193
|
+
return new Map;
|
|
3194
|
+
try {
|
|
3195
|
+
const raw = window.localStorage.getItem(key);
|
|
3196
|
+
if (!raw)
|
|
3197
|
+
return new Map;
|
|
3198
|
+
const arr = JSON.parse(raw);
|
|
3199
|
+
const map = new Map;
|
|
3200
|
+
for (const item of arr) {
|
|
3201
|
+
const conv = fromSerializable(item);
|
|
3202
|
+
map.set(conv.id, conv);
|
|
3203
|
+
}
|
|
3204
|
+
return map;
|
|
3205
|
+
} catch {
|
|
3206
|
+
return new Map;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
function saveAll(key, map) {
|
|
3210
|
+
if (typeof window === "undefined")
|
|
3211
|
+
return;
|
|
3212
|
+
try {
|
|
3213
|
+
const arr = Array.from(map.values()).map(toSerializable);
|
|
3214
|
+
window.localStorage.setItem(key, JSON.stringify(arr));
|
|
3215
|
+
} catch {}
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
class LocalStorageConversationStore {
|
|
3219
|
+
key;
|
|
3220
|
+
cache = null;
|
|
3221
|
+
constructor(storageKey = DEFAULT_KEY) {
|
|
3222
|
+
this.key = storageKey;
|
|
3223
|
+
}
|
|
3224
|
+
getMap() {
|
|
3225
|
+
if (!this.cache) {
|
|
3226
|
+
this.cache = loadAll(this.key);
|
|
3227
|
+
}
|
|
3228
|
+
return this.cache;
|
|
3229
|
+
}
|
|
3230
|
+
persist() {
|
|
3231
|
+
saveAll(this.key, this.getMap());
|
|
3232
|
+
}
|
|
3233
|
+
async get(conversationId) {
|
|
3234
|
+
return this.getMap().get(conversationId) ?? null;
|
|
3235
|
+
}
|
|
3236
|
+
async create(conversation) {
|
|
3237
|
+
const now = new Date;
|
|
3238
|
+
const full = {
|
|
3239
|
+
...conversation,
|
|
3240
|
+
id: generateId2("conv"),
|
|
3241
|
+
createdAt: now,
|
|
3242
|
+
updatedAt: now
|
|
3243
|
+
};
|
|
3244
|
+
this.getMap().set(full.id, full);
|
|
3245
|
+
this.persist();
|
|
3246
|
+
return full;
|
|
3247
|
+
}
|
|
3248
|
+
async update(conversationId, updates) {
|
|
3249
|
+
const conv = this.getMap().get(conversationId);
|
|
3250
|
+
if (!conv)
|
|
3251
|
+
return null;
|
|
3252
|
+
const updated = {
|
|
3253
|
+
...conv,
|
|
3254
|
+
...updates,
|
|
3255
|
+
updatedAt: new Date
|
|
3256
|
+
};
|
|
3257
|
+
this.getMap().set(conversationId, updated);
|
|
3258
|
+
this.persist();
|
|
3259
|
+
return updated;
|
|
3260
|
+
}
|
|
3261
|
+
async appendMessage(conversationId, message) {
|
|
3262
|
+
const conv = this.getMap().get(conversationId);
|
|
3263
|
+
if (!conv)
|
|
3264
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
3265
|
+
const now = new Date;
|
|
3266
|
+
const fullMessage = {
|
|
3267
|
+
...message,
|
|
3268
|
+
id: generateId2("msg"),
|
|
3269
|
+
conversationId,
|
|
3270
|
+
createdAt: now,
|
|
3271
|
+
updatedAt: now
|
|
3272
|
+
};
|
|
3273
|
+
conv.messages.push(fullMessage);
|
|
3274
|
+
conv.updatedAt = now;
|
|
3275
|
+
this.persist();
|
|
3276
|
+
return fullMessage;
|
|
3277
|
+
}
|
|
3278
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
3279
|
+
const conv = this.getMap().get(conversationId);
|
|
3280
|
+
if (!conv)
|
|
3281
|
+
return null;
|
|
3282
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
3283
|
+
if (idx === -1)
|
|
3284
|
+
return null;
|
|
3285
|
+
const msg = conv.messages[idx];
|
|
3286
|
+
if (!msg)
|
|
3287
|
+
return null;
|
|
3288
|
+
const updated = {
|
|
3289
|
+
...msg,
|
|
3290
|
+
...updates,
|
|
3291
|
+
updatedAt: new Date
|
|
3292
|
+
};
|
|
3293
|
+
conv.messages[idx] = updated;
|
|
3294
|
+
conv.updatedAt = new Date;
|
|
3295
|
+
this.persist();
|
|
3296
|
+
return updated;
|
|
3297
|
+
}
|
|
3298
|
+
async delete(conversationId) {
|
|
3299
|
+
const deleted = this.getMap().delete(conversationId);
|
|
3300
|
+
if (deleted)
|
|
3301
|
+
this.persist();
|
|
3302
|
+
return deleted;
|
|
3303
|
+
}
|
|
3304
|
+
async list(options) {
|
|
3305
|
+
let results = Array.from(this.getMap().values());
|
|
3306
|
+
if (options?.status) {
|
|
3307
|
+
results = results.filter((c) => c.status === options.status);
|
|
3308
|
+
}
|
|
3309
|
+
if (options?.projectId) {
|
|
3310
|
+
results = results.filter((c) => c.projectId === options.projectId);
|
|
3311
|
+
}
|
|
3312
|
+
if (options?.tags && options.tags.length > 0) {
|
|
3313
|
+
const tagSet = new Set(options.tags);
|
|
3314
|
+
results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
|
|
3315
|
+
}
|
|
3316
|
+
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
3317
|
+
const offset = options?.offset ?? 0;
|
|
3318
|
+
const limit = options?.limit ?? 100;
|
|
3319
|
+
return results.slice(offset, offset + limit);
|
|
3320
|
+
}
|
|
3321
|
+
async fork(conversationId, upToMessageId) {
|
|
3322
|
+
const source = this.getMap().get(conversationId);
|
|
3323
|
+
if (!source)
|
|
3324
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
3325
|
+
let messagesToCopy = source.messages;
|
|
3326
|
+
if (upToMessageId) {
|
|
3327
|
+
const idx = source.messages.findIndex((m) => m.id === upToMessageId);
|
|
3328
|
+
if (idx === -1)
|
|
3329
|
+
throw new Error(`Message ${upToMessageId} not found`);
|
|
3330
|
+
messagesToCopy = source.messages.slice(0, idx + 1);
|
|
3331
|
+
}
|
|
3332
|
+
const now = new Date;
|
|
3333
|
+
const forkedMessages = messagesToCopy.map((m) => ({
|
|
3334
|
+
...m,
|
|
3335
|
+
id: generateId2("msg"),
|
|
3336
|
+
conversationId: "",
|
|
3337
|
+
createdAt: new Date(m.createdAt),
|
|
3338
|
+
updatedAt: new Date(m.updatedAt)
|
|
3339
|
+
}));
|
|
3340
|
+
const forked = {
|
|
3341
|
+
...source,
|
|
3342
|
+
id: generateId2("conv"),
|
|
3343
|
+
title: source.title ? `${source.title} (fork)` : undefined,
|
|
3344
|
+
forkedFromId: source.id,
|
|
3345
|
+
createdAt: now,
|
|
3346
|
+
updatedAt: now,
|
|
3347
|
+
messages: forkedMessages
|
|
3348
|
+
};
|
|
3349
|
+
for (const m of forked.messages) {
|
|
3350
|
+
m.conversationId = forked.id;
|
|
3351
|
+
}
|
|
3352
|
+
this.getMap().set(forked.id, forked);
|
|
3353
|
+
this.persist();
|
|
3354
|
+
return forked;
|
|
3355
|
+
}
|
|
3356
|
+
async truncateAfter(conversationId, messageId) {
|
|
3357
|
+
const conv = this.getMap().get(conversationId);
|
|
3358
|
+
if (!conv)
|
|
3359
|
+
return null;
|
|
3360
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
3361
|
+
if (idx === -1)
|
|
3362
|
+
return null;
|
|
3363
|
+
conv.messages = conv.messages.slice(0, idx + 1);
|
|
3364
|
+
conv.updatedAt = new Date;
|
|
3365
|
+
this.persist();
|
|
3366
|
+
return conv;
|
|
3367
|
+
}
|
|
3368
|
+
async search(query, limit = 20) {
|
|
3369
|
+
const lowerQuery = query.toLowerCase();
|
|
3370
|
+
const results = [];
|
|
3371
|
+
for (const conv of this.getMap().values()) {
|
|
3372
|
+
if (conv.title?.toLowerCase().includes(lowerQuery)) {
|
|
3373
|
+
results.push(conv);
|
|
3374
|
+
continue;
|
|
3375
|
+
}
|
|
3376
|
+
if (conv.messages.some((m) => m.content.toLowerCase().includes(lowerQuery))) {
|
|
3377
|
+
results.push(conv);
|
|
3378
|
+
}
|
|
3379
|
+
if (results.length >= limit)
|
|
3380
|
+
break;
|
|
3381
|
+
}
|
|
3382
|
+
return results;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
function createLocalStorageConversationStore(storageKey) {
|
|
3386
|
+
return new LocalStorageConversationStore(storageKey);
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
// src/presentation/components/ChatWithSidebar.tsx
|
|
3390
|
+
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3391
|
+
"use client";
|
|
3392
|
+
var defaultStore = createLocalStorageConversationStore();
|
|
3393
|
+
function ChatWithSidebar({
|
|
3394
|
+
store = defaultStore,
|
|
3395
|
+
projectId,
|
|
3396
|
+
tags,
|
|
3397
|
+
className,
|
|
3398
|
+
thinkingLevel: initialThinkingLevel = "thinking",
|
|
3399
|
+
presentationRenderer,
|
|
3400
|
+
formRenderer,
|
|
3401
|
+
...useChatOptions
|
|
3402
|
+
}) {
|
|
3403
|
+
const effectiveStore = store;
|
|
3404
|
+
const [thinkingLevel, setThinkingLevel] = React12.useState(initialThinkingLevel);
|
|
3405
|
+
const chat = useChat({
|
|
3406
|
+
...useChatOptions,
|
|
3407
|
+
store: effectiveStore,
|
|
3408
|
+
thinkingLevel
|
|
3409
|
+
});
|
|
3410
|
+
const {
|
|
3411
|
+
messages,
|
|
3412
|
+
conversation,
|
|
3413
|
+
sendMessage,
|
|
3414
|
+
isLoading,
|
|
3415
|
+
setConversationId,
|
|
3416
|
+
createNewConversation,
|
|
3417
|
+
editMessage,
|
|
3418
|
+
forkConversation,
|
|
3419
|
+
updateConversation
|
|
3420
|
+
} = chat;
|
|
3421
|
+
const selectedConversationId = conversation?.id ?? null;
|
|
3422
|
+
const handleSelectConversation = React12.useCallback((id) => {
|
|
3423
|
+
setConversationId(id);
|
|
3424
|
+
}, [setConversationId]);
|
|
3425
|
+
return /* @__PURE__ */ jsxs10("div", {
|
|
3426
|
+
className: className ?? "flex h-full w-full",
|
|
3427
|
+
children: [
|
|
3428
|
+
/* @__PURE__ */ jsx10(ChatSidebar, {
|
|
3429
|
+
store: effectiveStore,
|
|
3430
|
+
selectedConversationId,
|
|
3431
|
+
onSelectConversation: handleSelectConversation,
|
|
3432
|
+
onCreateNew: createNewConversation,
|
|
3433
|
+
projectId,
|
|
3434
|
+
tags,
|
|
3435
|
+
selectedConversation: conversation,
|
|
3436
|
+
onUpdateConversation: updateConversation ? async (id, updates) => {
|
|
3437
|
+
if (id === selectedConversationId) {
|
|
3438
|
+
await updateConversation(updates);
|
|
3439
|
+
}
|
|
3440
|
+
} : undefined
|
|
3441
|
+
}),
|
|
3442
|
+
/* @__PURE__ */ jsx10("div", {
|
|
3443
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
3444
|
+
children: /* @__PURE__ */ jsx10(ChatWithExport, {
|
|
3445
|
+
messages,
|
|
3446
|
+
conversation,
|
|
3447
|
+
onCreateNew: createNewConversation,
|
|
3448
|
+
onFork: forkConversation,
|
|
3449
|
+
onEditMessage: editMessage,
|
|
3450
|
+
thinkingLevel,
|
|
3451
|
+
onThinkingLevelChange: setThinkingLevel,
|
|
3452
|
+
presentationRenderer,
|
|
3453
|
+
formRenderer,
|
|
3454
|
+
children: /* @__PURE__ */ jsx10(ChatInput, {
|
|
3455
|
+
onSend: (content, att) => sendMessage(content, att),
|
|
3456
|
+
disabled: isLoading,
|
|
3457
|
+
isLoading
|
|
3458
|
+
})
|
|
3459
|
+
})
|
|
3460
|
+
})
|
|
3461
|
+
]
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
// src/presentation/components/ModelPicker.tsx
|
|
3465
|
+
import * as React13 from "react";
|
|
3466
|
+
import { cn as cn7 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
3467
|
+
import { Button as Button6 } from "@contractspec/lib.design-system";
|
|
3468
|
+
import {
|
|
3469
|
+
Select as Select2,
|
|
3470
|
+
SelectContent as SelectContent2,
|
|
3471
|
+
SelectItem as SelectItem2,
|
|
3472
|
+
SelectTrigger as SelectTrigger2,
|
|
3473
|
+
SelectValue as SelectValue2
|
|
3474
|
+
} from "@contractspec/lib.ui-kit-web/ui/select";
|
|
3475
|
+
import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
3476
|
+
import { Label as Label2 } from "@contractspec/lib.ui-kit-web/ui/label";
|
|
683
3477
|
import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
|
|
684
3478
|
import {
|
|
685
3479
|
getModelsForProvider
|
|
686
3480
|
} from "@contractspec/lib.ai-providers";
|
|
687
|
-
import {
|
|
3481
|
+
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
688
3482
|
"use client";
|
|
689
3483
|
var PROVIDER_ICONS = {
|
|
690
|
-
ollama: /* @__PURE__ */
|
|
3484
|
+
ollama: /* @__PURE__ */ jsx11(Cpu, {
|
|
691
3485
|
className: "h-4 w-4"
|
|
692
|
-
}
|
|
693
|
-
openai: /* @__PURE__ */
|
|
3486
|
+
}),
|
|
3487
|
+
openai: /* @__PURE__ */ jsx11(Bot2, {
|
|
694
3488
|
className: "h-4 w-4"
|
|
695
|
-
}
|
|
696
|
-
anthropic: /* @__PURE__ */
|
|
3489
|
+
}),
|
|
3490
|
+
anthropic: /* @__PURE__ */ jsx11(Sparkles, {
|
|
697
3491
|
className: "h-4 w-4"
|
|
698
|
-
}
|
|
699
|
-
mistral: /* @__PURE__ */
|
|
3492
|
+
}),
|
|
3493
|
+
mistral: /* @__PURE__ */ jsx11(Cloud, {
|
|
700
3494
|
className: "h-4 w-4"
|
|
701
|
-
}
|
|
702
|
-
gemini: /* @__PURE__ */
|
|
3495
|
+
}),
|
|
3496
|
+
gemini: /* @__PURE__ */ jsx11(Sparkles, {
|
|
703
3497
|
className: "h-4 w-4"
|
|
704
|
-
}
|
|
3498
|
+
})
|
|
705
3499
|
};
|
|
706
3500
|
var PROVIDER_NAMES = {
|
|
707
3501
|
ollama: "Ollama (Local)",
|
|
@@ -731,7 +3525,7 @@ function ModelPicker({
|
|
|
731
3525
|
];
|
|
732
3526
|
const models = getModelsForProvider(value.provider);
|
|
733
3527
|
const selectedModel = models.find((m) => m.id === value.model);
|
|
734
|
-
const handleProviderChange =
|
|
3528
|
+
const handleProviderChange = React13.useCallback((providerName) => {
|
|
735
3529
|
const provider = providerName;
|
|
736
3530
|
const providerInfo = providers.find((p) => p.provider === provider);
|
|
737
3531
|
const providerModels = getModelsForProvider(provider);
|
|
@@ -742,173 +3536,173 @@ function ModelPicker({
|
|
|
742
3536
|
mode: providerInfo?.mode ?? "byok"
|
|
743
3537
|
});
|
|
744
3538
|
}, [onChange, providers]);
|
|
745
|
-
const handleModelChange =
|
|
3539
|
+
const handleModelChange = React13.useCallback((modelId) => {
|
|
746
3540
|
onChange({
|
|
747
3541
|
...value,
|
|
748
3542
|
model: modelId
|
|
749
3543
|
});
|
|
750
3544
|
}, [onChange, value]);
|
|
751
3545
|
if (compact) {
|
|
752
|
-
return /* @__PURE__ */
|
|
753
|
-
className:
|
|
3546
|
+
return /* @__PURE__ */ jsxs11("div", {
|
|
3547
|
+
className: cn7("flex items-center gap-2", className),
|
|
754
3548
|
children: [
|
|
755
|
-
/* @__PURE__ */
|
|
3549
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
756
3550
|
value: value.provider,
|
|
757
3551
|
onValueChange: handleProviderChange,
|
|
758
3552
|
children: [
|
|
759
|
-
/* @__PURE__ */
|
|
3553
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
760
3554
|
className: "w-[140px]",
|
|
761
|
-
children: /* @__PURE__ */
|
|
762
|
-
}
|
|
763
|
-
/* @__PURE__ */
|
|
764
|
-
children: providers.map((p) => /* @__PURE__ */
|
|
3555
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {})
|
|
3556
|
+
}),
|
|
3557
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3558
|
+
children: providers.map((p) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
765
3559
|
value: p.provider,
|
|
766
3560
|
disabled: !p.available,
|
|
767
|
-
children: /* @__PURE__ */
|
|
3561
|
+
children: /* @__PURE__ */ jsxs11("div", {
|
|
768
3562
|
className: "flex items-center gap-2",
|
|
769
3563
|
children: [
|
|
770
3564
|
PROVIDER_ICONS[p.provider],
|
|
771
|
-
/* @__PURE__ */
|
|
3565
|
+
/* @__PURE__ */ jsx11("span", {
|
|
772
3566
|
children: PROVIDER_NAMES[p.provider]
|
|
773
|
-
}
|
|
3567
|
+
})
|
|
774
3568
|
]
|
|
775
|
-
}
|
|
776
|
-
}, p.provider
|
|
777
|
-
}
|
|
3569
|
+
})
|
|
3570
|
+
}, p.provider))
|
|
3571
|
+
})
|
|
778
3572
|
]
|
|
779
|
-
}
|
|
780
|
-
/* @__PURE__ */
|
|
3573
|
+
}),
|
|
3574
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
781
3575
|
value: value.model,
|
|
782
3576
|
onValueChange: handleModelChange,
|
|
783
3577
|
children: [
|
|
784
|
-
/* @__PURE__ */
|
|
3578
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
785
3579
|
className: "w-[160px]",
|
|
786
|
-
children: /* @__PURE__ */
|
|
787
|
-
}
|
|
788
|
-
/* @__PURE__ */
|
|
789
|
-
children: models.map((m) => /* @__PURE__ */
|
|
3580
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {})
|
|
3581
|
+
}),
|
|
3582
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3583
|
+
children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
790
3584
|
value: m.id,
|
|
791
3585
|
children: m.name
|
|
792
|
-
}, m.id
|
|
793
|
-
}
|
|
3586
|
+
}, m.id))
|
|
3587
|
+
})
|
|
794
3588
|
]
|
|
795
|
-
}
|
|
3589
|
+
})
|
|
796
3590
|
]
|
|
797
|
-
}
|
|
3591
|
+
});
|
|
798
3592
|
}
|
|
799
|
-
return /* @__PURE__ */
|
|
800
|
-
className:
|
|
3593
|
+
return /* @__PURE__ */ jsxs11("div", {
|
|
3594
|
+
className: cn7("flex flex-col gap-3", className),
|
|
801
3595
|
children: [
|
|
802
|
-
/* @__PURE__ */
|
|
3596
|
+
/* @__PURE__ */ jsxs11("div", {
|
|
803
3597
|
className: "flex flex-col gap-1.5",
|
|
804
3598
|
children: [
|
|
805
|
-
/* @__PURE__ */
|
|
3599
|
+
/* @__PURE__ */ jsx11(Label2, {
|
|
806
3600
|
htmlFor: "provider-selection",
|
|
807
3601
|
className: "text-sm font-medium",
|
|
808
3602
|
children: "Provider"
|
|
809
|
-
}
|
|
810
|
-
/* @__PURE__ */
|
|
3603
|
+
}),
|
|
3604
|
+
/* @__PURE__ */ jsx11("div", {
|
|
811
3605
|
className: "flex flex-wrap gap-2",
|
|
812
3606
|
id: "provider-selection",
|
|
813
|
-
children: providers.map((p) => /* @__PURE__ */
|
|
3607
|
+
children: providers.map((p) => /* @__PURE__ */ jsxs11(Button6, {
|
|
814
3608
|
variant: value.provider === p.provider ? "default" : "outline",
|
|
815
3609
|
size: "sm",
|
|
816
3610
|
onPress: () => p.available && handleProviderChange(p.provider),
|
|
817
3611
|
disabled: !p.available,
|
|
818
|
-
className:
|
|
3612
|
+
className: cn7(!p.available && "opacity-50"),
|
|
819
3613
|
children: [
|
|
820
3614
|
PROVIDER_ICONS[p.provider],
|
|
821
|
-
/* @__PURE__ */
|
|
3615
|
+
/* @__PURE__ */ jsx11("span", {
|
|
822
3616
|
children: PROVIDER_NAMES[p.provider]
|
|
823
|
-
}
|
|
824
|
-
/* @__PURE__ */
|
|
3617
|
+
}),
|
|
3618
|
+
/* @__PURE__ */ jsx11(Badge, {
|
|
825
3619
|
variant: MODE_BADGES[p.mode].variant,
|
|
826
3620
|
className: "ml-1",
|
|
827
3621
|
children: MODE_BADGES[p.mode].label
|
|
828
|
-
}
|
|
3622
|
+
})
|
|
829
3623
|
]
|
|
830
|
-
}, p.provider
|
|
831
|
-
}
|
|
3624
|
+
}, p.provider))
|
|
3625
|
+
})
|
|
832
3626
|
]
|
|
833
|
-
}
|
|
834
|
-
/* @__PURE__ */
|
|
3627
|
+
}),
|
|
3628
|
+
/* @__PURE__ */ jsxs11("div", {
|
|
835
3629
|
className: "flex flex-col gap-1.5",
|
|
836
3630
|
children: [
|
|
837
|
-
/* @__PURE__ */
|
|
3631
|
+
/* @__PURE__ */ jsx11(Label2, {
|
|
838
3632
|
htmlFor: "model-picker",
|
|
839
3633
|
className: "text-sm font-medium",
|
|
840
3634
|
children: "Model"
|
|
841
|
-
}
|
|
842
|
-
/* @__PURE__ */
|
|
3635
|
+
}),
|
|
3636
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
843
3637
|
name: "model-picker",
|
|
844
3638
|
value: value.model,
|
|
845
3639
|
onValueChange: handleModelChange,
|
|
846
3640
|
children: [
|
|
847
|
-
/* @__PURE__ */
|
|
848
|
-
children: /* @__PURE__ */
|
|
3641
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
3642
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {
|
|
849
3643
|
placeholder: "Select a model"
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
/* @__PURE__ */
|
|
853
|
-
children: models.map((m) => /* @__PURE__ */
|
|
3644
|
+
})
|
|
3645
|
+
}),
|
|
3646
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3647
|
+
children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
854
3648
|
value: m.id,
|
|
855
|
-
children: /* @__PURE__ */
|
|
3649
|
+
children: /* @__PURE__ */ jsxs11("div", {
|
|
856
3650
|
className: "flex items-center gap-2",
|
|
857
3651
|
children: [
|
|
858
|
-
/* @__PURE__ */
|
|
3652
|
+
/* @__PURE__ */ jsx11("span", {
|
|
859
3653
|
children: m.name
|
|
860
|
-
}
|
|
861
|
-
/* @__PURE__ */
|
|
3654
|
+
}),
|
|
3655
|
+
/* @__PURE__ */ jsxs11("span", {
|
|
862
3656
|
className: "text-muted-foreground text-xs",
|
|
863
3657
|
children: [
|
|
864
3658
|
Math.round(m.contextWindow / 1000),
|
|
865
3659
|
"K"
|
|
866
3660
|
]
|
|
867
|
-
}
|
|
868
|
-
m.capabilities.vision && /* @__PURE__ */
|
|
3661
|
+
}),
|
|
3662
|
+
m.capabilities.vision && /* @__PURE__ */ jsx11(Badge, {
|
|
869
3663
|
variant: "outline",
|
|
870
3664
|
className: "text-xs",
|
|
871
3665
|
children: "Vision"
|
|
872
|
-
}
|
|
873
|
-
m.capabilities.reasoning && /* @__PURE__ */
|
|
3666
|
+
}),
|
|
3667
|
+
m.capabilities.reasoning && /* @__PURE__ */ jsx11(Badge, {
|
|
874
3668
|
variant: "outline",
|
|
875
3669
|
className: "text-xs",
|
|
876
3670
|
children: "Reasoning"
|
|
877
|
-
}
|
|
3671
|
+
})
|
|
878
3672
|
]
|
|
879
|
-
}
|
|
880
|
-
}, m.id
|
|
881
|
-
}
|
|
3673
|
+
})
|
|
3674
|
+
}, m.id))
|
|
3675
|
+
})
|
|
882
3676
|
]
|
|
883
|
-
}
|
|
3677
|
+
})
|
|
884
3678
|
]
|
|
885
|
-
}
|
|
886
|
-
selectedModel && /* @__PURE__ */
|
|
3679
|
+
}),
|
|
3680
|
+
selectedModel && /* @__PURE__ */ jsxs11("div", {
|
|
887
3681
|
className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
|
|
888
3682
|
children: [
|
|
889
|
-
/* @__PURE__ */
|
|
3683
|
+
/* @__PURE__ */ jsxs11("span", {
|
|
890
3684
|
children: [
|
|
891
3685
|
"Context: ",
|
|
892
3686
|
Math.round(selectedModel.contextWindow / 1000),
|
|
893
3687
|
"K tokens"
|
|
894
3688
|
]
|
|
895
|
-
}
|
|
896
|
-
selectedModel.capabilities.vision && /* @__PURE__ */
|
|
3689
|
+
}),
|
|
3690
|
+
selectedModel.capabilities.vision && /* @__PURE__ */ jsx11("span", {
|
|
897
3691
|
children: "• Vision"
|
|
898
|
-
}
|
|
899
|
-
selectedModel.capabilities.tools && /* @__PURE__ */
|
|
3692
|
+
}),
|
|
3693
|
+
selectedModel.capabilities.tools && /* @__PURE__ */ jsx11("span", {
|
|
900
3694
|
children: "• Tools"
|
|
901
|
-
}
|
|
902
|
-
selectedModel.capabilities.reasoning && /* @__PURE__ */
|
|
3695
|
+
}),
|
|
3696
|
+
selectedModel.capabilities.reasoning && /* @__PURE__ */ jsx11("span", {
|
|
903
3697
|
children: "• Reasoning"
|
|
904
|
-
}
|
|
3698
|
+
})
|
|
905
3699
|
]
|
|
906
|
-
}
|
|
3700
|
+
})
|
|
907
3701
|
]
|
|
908
|
-
}
|
|
3702
|
+
});
|
|
909
3703
|
}
|
|
910
3704
|
// src/presentation/components/ContextIndicator.tsx
|
|
911
|
-
import { cn as
|
|
3705
|
+
import { cn as cn8 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
912
3706
|
import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
913
3707
|
import {
|
|
914
3708
|
Tooltip,
|
|
@@ -917,7 +3711,7 @@ import {
|
|
|
917
3711
|
TooltipTrigger
|
|
918
3712
|
} from "@contractspec/lib.ui-kit-web/ui/tooltip";
|
|
919
3713
|
import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
|
|
920
|
-
import {
|
|
3714
|
+
import { jsx as jsx12, jsxs as jsxs12, Fragment as Fragment6 } from "react/jsx-runtime";
|
|
921
3715
|
"use client";
|
|
922
3716
|
function ContextIndicator({
|
|
923
3717
|
summary,
|
|
@@ -926,155 +3720,163 @@ function ContextIndicator({
|
|
|
926
3720
|
showDetails = true
|
|
927
3721
|
}) {
|
|
928
3722
|
if (!summary && !active) {
|
|
929
|
-
return /* @__PURE__ */
|
|
930
|
-
className:
|
|
3723
|
+
return /* @__PURE__ */ jsxs12("div", {
|
|
3724
|
+
className: cn8("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
|
|
931
3725
|
children: [
|
|
932
|
-
/* @__PURE__ */
|
|
3726
|
+
/* @__PURE__ */ jsx12(Info, {
|
|
933
3727
|
className: "h-4 w-4"
|
|
934
|
-
}
|
|
935
|
-
/* @__PURE__ */
|
|
3728
|
+
}),
|
|
3729
|
+
/* @__PURE__ */ jsx12("span", {
|
|
936
3730
|
children: "No workspace context"
|
|
937
|
-
}
|
|
3731
|
+
})
|
|
938
3732
|
]
|
|
939
|
-
}
|
|
3733
|
+
});
|
|
940
3734
|
}
|
|
941
|
-
const content = /* @__PURE__ */
|
|
942
|
-
className:
|
|
3735
|
+
const content = /* @__PURE__ */ jsxs12("div", {
|
|
3736
|
+
className: cn8("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
|
|
943
3737
|
children: [
|
|
944
|
-
/* @__PURE__ */
|
|
3738
|
+
/* @__PURE__ */ jsxs12(Badge2, {
|
|
945
3739
|
variant: active ? "default" : "secondary",
|
|
946
3740
|
className: "flex items-center gap-1",
|
|
947
3741
|
children: [
|
|
948
|
-
/* @__PURE__ */
|
|
3742
|
+
/* @__PURE__ */ jsx12(Zap, {
|
|
949
3743
|
className: "h-3 w-3"
|
|
950
|
-
}
|
|
3744
|
+
}),
|
|
951
3745
|
"Context"
|
|
952
3746
|
]
|
|
953
|
-
}
|
|
954
|
-
summary && showDetails && /* @__PURE__ */
|
|
3747
|
+
}),
|
|
3748
|
+
summary && showDetails && /* @__PURE__ */ jsxs12(Fragment6, {
|
|
955
3749
|
children: [
|
|
956
|
-
/* @__PURE__ */
|
|
3750
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
957
3751
|
className: "flex items-center gap-1 text-xs",
|
|
958
3752
|
children: [
|
|
959
|
-
/* @__PURE__ */
|
|
3753
|
+
/* @__PURE__ */ jsx12(FolderOpen, {
|
|
960
3754
|
className: "h-3.5 w-3.5"
|
|
961
|
-
}
|
|
962
|
-
/* @__PURE__ */
|
|
3755
|
+
}),
|
|
3756
|
+
/* @__PURE__ */ jsx12("span", {
|
|
963
3757
|
children: summary.name
|
|
964
|
-
}
|
|
3758
|
+
})
|
|
965
3759
|
]
|
|
966
|
-
}
|
|
967
|
-
/* @__PURE__ */
|
|
3760
|
+
}),
|
|
3761
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
968
3762
|
className: "flex items-center gap-1 text-xs",
|
|
969
3763
|
children: [
|
|
970
|
-
/* @__PURE__ */
|
|
3764
|
+
/* @__PURE__ */ jsx12(FileCode, {
|
|
971
3765
|
className: "h-3.5 w-3.5"
|
|
972
|
-
}
|
|
973
|
-
/* @__PURE__ */
|
|
3766
|
+
}),
|
|
3767
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
974
3768
|
children: [
|
|
975
3769
|
summary.specs.total,
|
|
976
3770
|
" specs"
|
|
977
3771
|
]
|
|
978
|
-
}
|
|
3772
|
+
})
|
|
979
3773
|
]
|
|
980
|
-
}
|
|
3774
|
+
})
|
|
981
3775
|
]
|
|
982
|
-
}
|
|
3776
|
+
})
|
|
983
3777
|
]
|
|
984
|
-
}
|
|
3778
|
+
});
|
|
985
3779
|
if (!summary) {
|
|
986
3780
|
return content;
|
|
987
3781
|
}
|
|
988
|
-
return /* @__PURE__ */
|
|
989
|
-
children: /* @__PURE__ */
|
|
3782
|
+
return /* @__PURE__ */ jsx12(TooltipProvider, {
|
|
3783
|
+
children: /* @__PURE__ */ jsxs12(Tooltip, {
|
|
990
3784
|
children: [
|
|
991
|
-
/* @__PURE__ */
|
|
3785
|
+
/* @__PURE__ */ jsx12(TooltipTrigger, {
|
|
992
3786
|
asChild: true,
|
|
993
3787
|
children: content
|
|
994
|
-
}
|
|
995
|
-
/* @__PURE__ */
|
|
3788
|
+
}),
|
|
3789
|
+
/* @__PURE__ */ jsx12(TooltipContent, {
|
|
996
3790
|
side: "bottom",
|
|
997
3791
|
className: "max-w-[300px]",
|
|
998
|
-
children: /* @__PURE__ */
|
|
3792
|
+
children: /* @__PURE__ */ jsxs12("div", {
|
|
999
3793
|
className: "flex flex-col gap-2 text-sm",
|
|
1000
3794
|
children: [
|
|
1001
|
-
/* @__PURE__ */
|
|
3795
|
+
/* @__PURE__ */ jsx12("div", {
|
|
1002
3796
|
className: "font-medium",
|
|
1003
3797
|
children: summary.name
|
|
1004
|
-
}
|
|
1005
|
-
/* @__PURE__ */
|
|
3798
|
+
}),
|
|
3799
|
+
/* @__PURE__ */ jsx12("div", {
|
|
1006
3800
|
className: "text-muted-foreground text-xs",
|
|
1007
3801
|
children: summary.path
|
|
1008
|
-
}
|
|
1009
|
-
/* @__PURE__ */
|
|
3802
|
+
}),
|
|
3803
|
+
/* @__PURE__ */ jsx12("div", {
|
|
1010
3804
|
className: "border-t pt-2",
|
|
1011
|
-
children: /* @__PURE__ */
|
|
3805
|
+
children: /* @__PURE__ */ jsxs12("div", {
|
|
1012
3806
|
className: "grid grid-cols-2 gap-1 text-xs",
|
|
1013
3807
|
children: [
|
|
1014
|
-
/* @__PURE__ */
|
|
3808
|
+
/* @__PURE__ */ jsx12("span", {
|
|
1015
3809
|
children: "Commands:"
|
|
1016
|
-
}
|
|
1017
|
-
/* @__PURE__ */
|
|
3810
|
+
}),
|
|
3811
|
+
/* @__PURE__ */ jsx12("span", {
|
|
1018
3812
|
className: "text-right",
|
|
1019
3813
|
children: summary.specs.commands
|
|
1020
|
-
}
|
|
1021
|
-
/* @__PURE__ */
|
|
3814
|
+
}),
|
|
3815
|
+
/* @__PURE__ */ jsx12("span", {
|
|
1022
3816
|
children: "Queries:"
|
|
1023
|
-
}
|
|
1024
|
-
/* @__PURE__ */
|
|
3817
|
+
}),
|
|
3818
|
+
/* @__PURE__ */ jsx12("span", {
|
|
1025
3819
|
className: "text-right",
|
|
1026
3820
|
children: summary.specs.queries
|
|
1027
|
-
}
|
|
1028
|
-
/* @__PURE__ */
|
|
3821
|
+
}),
|
|
3822
|
+
/* @__PURE__ */ jsx12("span", {
|
|
1029
3823
|
children: "Events:"
|
|
1030
|
-
}
|
|
1031
|
-
/* @__PURE__ */
|
|
3824
|
+
}),
|
|
3825
|
+
/* @__PURE__ */ jsx12("span", {
|
|
1032
3826
|
className: "text-right",
|
|
1033
3827
|
children: summary.specs.events
|
|
1034
|
-
}
|
|
1035
|
-
/* @__PURE__ */
|
|
3828
|
+
}),
|
|
3829
|
+
/* @__PURE__ */ jsx12("span", {
|
|
1036
3830
|
children: "Presentations:"
|
|
1037
|
-
}
|
|
1038
|
-
/* @__PURE__ */
|
|
3831
|
+
}),
|
|
3832
|
+
/* @__PURE__ */ jsx12("span", {
|
|
1039
3833
|
className: "text-right",
|
|
1040
3834
|
children: summary.specs.presentations
|
|
1041
|
-
}
|
|
3835
|
+
})
|
|
1042
3836
|
]
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
/* @__PURE__ */
|
|
3837
|
+
})
|
|
3838
|
+
}),
|
|
3839
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
1046
3840
|
className: "border-t pt-2 text-xs",
|
|
1047
3841
|
children: [
|
|
1048
|
-
/* @__PURE__ */
|
|
3842
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
1049
3843
|
children: [
|
|
1050
3844
|
summary.files.total,
|
|
1051
3845
|
" files"
|
|
1052
3846
|
]
|
|
1053
|
-
}
|
|
1054
|
-
/* @__PURE__ */
|
|
3847
|
+
}),
|
|
3848
|
+
/* @__PURE__ */ jsx12("span", {
|
|
1055
3849
|
className: "mx-1",
|
|
1056
3850
|
children: "•"
|
|
1057
|
-
}
|
|
1058
|
-
/* @__PURE__ */
|
|
3851
|
+
}),
|
|
3852
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
1059
3853
|
children: [
|
|
1060
3854
|
summary.files.specFiles,
|
|
1061
3855
|
" spec files"
|
|
1062
3856
|
]
|
|
1063
|
-
}
|
|
3857
|
+
})
|
|
1064
3858
|
]
|
|
1065
|
-
}
|
|
3859
|
+
})
|
|
1066
3860
|
]
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
3861
|
+
})
|
|
3862
|
+
})
|
|
1069
3863
|
]
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
3864
|
+
})
|
|
3865
|
+
});
|
|
1072
3866
|
}
|
|
1073
3867
|
export {
|
|
3868
|
+
isPresentationToolResult,
|
|
3869
|
+
isFormToolResult,
|
|
3870
|
+
ToolResultRenderer,
|
|
3871
|
+
ThinkingLevelPicker,
|
|
1074
3872
|
ModelPicker,
|
|
1075
3873
|
ContextIndicator,
|
|
1076
3874
|
CodePreview,
|
|
3875
|
+
ChatWithSidebar,
|
|
3876
|
+
ChatWithExport,
|
|
3877
|
+
ChatSidebar,
|
|
1077
3878
|
ChatMessage,
|
|
1078
3879
|
ChatInput,
|
|
3880
|
+
ChatExportToolbar,
|
|
1079
3881
|
ChatContainer
|
|
1080
3882
|
};
|