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