@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
|
+
})
|
|
240
|
+
]
|
|
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
|
+
})
|
|
232
294
|
]
|
|
233
|
-
}
|
|
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, {
|
|
546
|
+
className: "h-3 w-3"
|
|
547
|
+
}) : /* @__PURE__ */ jsx4(Copy2, {
|
|
405
548
|
className: "h-3 w-3"
|
|
406
|
-
}
|
|
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, {
|
|
407
558
|
className: "h-3 w-3"
|
|
408
|
-
}
|
|
409
|
-
}
|
|
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,402 +1205,487 @@ 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";
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
"
|
|
684
|
-
|
|
685
|
-
ollama: /* @__PURE__ */ jsxDEV5(Cpu, {
|
|
686
|
-
className: "h-4 w-4"
|
|
687
|
-
}, undefined, false, undefined, this),
|
|
688
|
-
openai: /* @__PURE__ */ jsxDEV5(Bot2, {
|
|
689
|
-
className: "h-4 w-4"
|
|
690
|
-
}, undefined, false, undefined, this),
|
|
691
|
-
anthropic: /* @__PURE__ */ jsxDEV5(Sparkles, {
|
|
692
|
-
className: "h-4 w-4"
|
|
693
|
-
}, undefined, false, undefined, this),
|
|
694
|
-
mistral: /* @__PURE__ */ jsxDEV5(Cloud, {
|
|
695
|
-
className: "h-4 w-4"
|
|
696
|
-
}, undefined, false, undefined, this),
|
|
697
|
-
gemini: /* @__PURE__ */ jsxDEV5(Sparkles, {
|
|
698
|
-
className: "h-4 w-4"
|
|
699
|
-
}, undefined, false, undefined, this)
|
|
700
|
-
};
|
|
701
|
-
var PROVIDER_NAMES = {
|
|
702
|
-
ollama: "Ollama (Local)",
|
|
703
|
-
openai: "OpenAI",
|
|
704
|
-
anthropic: "Anthropic",
|
|
705
|
-
mistral: "Mistral",
|
|
706
|
-
gemini: "Google Gemini"
|
|
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"
|
|
707
1216
|
};
|
|
708
|
-
var
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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"
|
|
712
1222
|
};
|
|
713
|
-
function
|
|
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({
|
|
714
1271
|
value,
|
|
715
1272
|
onChange,
|
|
716
|
-
availableProviders,
|
|
717
1273
|
className,
|
|
718
1274
|
compact = false
|
|
719
1275
|
}) {
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
{ provider: "anthropic", available: true, mode: "byok" },
|
|
724
|
-
{ provider: "mistral", available: true, mode: "byok" },
|
|
725
|
-
{ provider: "gemini", available: true, mode: "byok" }
|
|
726
|
-
];
|
|
727
|
-
const models = getModelsForProvider(value.provider);
|
|
728
|
-
const selectedModel = models.find((m) => m.id === value.model);
|
|
729
|
-
const handleProviderChange = React5.useCallback((providerName) => {
|
|
730
|
-
const provider = providerName;
|
|
731
|
-
const providerInfo = providers.find((p) => p.provider === provider);
|
|
732
|
-
const providerModels = getModelsForProvider(provider);
|
|
733
|
-
const defaultModel = providerModels[0]?.id ?? "";
|
|
734
|
-
onChange({
|
|
735
|
-
provider,
|
|
736
|
-
model: defaultModel,
|
|
737
|
-
mode: providerInfo?.mode ?? "byok"
|
|
738
|
-
});
|
|
739
|
-
}, [onChange, providers]);
|
|
740
|
-
const handleModelChange = React5.useCallback((modelId) => {
|
|
741
|
-
onChange({
|
|
742
|
-
...value,
|
|
743
|
-
model: modelId
|
|
744
|
-
});
|
|
745
|
-
}, [onChange, value]);
|
|
1276
|
+
const handleChange = React6.useCallback((v) => {
|
|
1277
|
+
onChange(v);
|
|
1278
|
+
}, [onChange]);
|
|
746
1279
|
if (compact) {
|
|
747
|
-
return /* @__PURE__ */
|
|
748
|
-
|
|
1280
|
+
return /* @__PURE__ */ jsxs7(Select, {
|
|
1281
|
+
value,
|
|
1282
|
+
onValueChange: handleChange,
|
|
749
1283
|
children: [
|
|
750
|
-
/* @__PURE__ */
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
value: p.provider,
|
|
761
|
-
disabled: !p.available,
|
|
762
|
-
children: /* @__PURE__ */ jsxDEV5("div", {
|
|
763
|
-
className: "flex items-center gap-2",
|
|
764
|
-
children: [
|
|
765
|
-
PROVIDER_ICONS[p.provider],
|
|
766
|
-
/* @__PURE__ */ jsxDEV5("span", {
|
|
767
|
-
children: PROVIDER_NAMES[p.provider]
|
|
768
|
-
}, undefined, false, undefined, this)
|
|
769
|
-
]
|
|
770
|
-
}, undefined, true, undefined, this)
|
|
771
|
-
}, p.provider, false, undefined, this))
|
|
772
|
-
}, undefined, false, undefined, this)
|
|
773
|
-
]
|
|
774
|
-
}, undefined, true, undefined, this),
|
|
775
|
-
/* @__PURE__ */ jsxDEV5(Select, {
|
|
776
|
-
value: value.model,
|
|
777
|
-
onValueChange: handleModelChange,
|
|
778
|
-
children: [
|
|
779
|
-
/* @__PURE__ */ jsxDEV5(SelectTrigger, {
|
|
780
|
-
className: "w-[160px]",
|
|
781
|
-
children: /* @__PURE__ */ jsxDEV5(SelectValue, {}, undefined, false, undefined, this)
|
|
782
|
-
}, undefined, false, undefined, this),
|
|
783
|
-
/* @__PURE__ */ jsxDEV5(SelectContent, {
|
|
784
|
-
children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
|
|
785
|
-
value: m.id,
|
|
786
|
-
children: m.name
|
|
787
|
-
}, m.id, false, undefined, this))
|
|
788
|
-
}, undefined, false, undefined, this)
|
|
789
|
-
]
|
|
790
|
-
}, undefined, true, undefined, this)
|
|
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
|
+
})
|
|
791
1294
|
]
|
|
792
|
-
}
|
|
1295
|
+
});
|
|
793
1296
|
}
|
|
794
|
-
return /* @__PURE__ */
|
|
795
|
-
className: cn5("flex flex-col gap-
|
|
1297
|
+
return /* @__PURE__ */ jsxs7("div", {
|
|
1298
|
+
className: cn5("flex flex-col gap-1.5", className),
|
|
796
1299
|
children: [
|
|
797
|
-
/* @__PURE__ */
|
|
798
|
-
|
|
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,
|
|
799
1309
|
children: [
|
|
800
|
-
/* @__PURE__ */
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
/* @__PURE__ */
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
disabled: !p.available,
|
|
813
|
-
className: cn5(!p.available && "opacity-50"),
|
|
814
|
-
children: [
|
|
815
|
-
PROVIDER_ICONS[p.provider],
|
|
816
|
-
/* @__PURE__ */ jsxDEV5("span", {
|
|
817
|
-
children: PROVIDER_NAMES[p.provider]
|
|
818
|
-
}, undefined, false, undefined, this),
|
|
819
|
-
/* @__PURE__ */ jsxDEV5(Badge, {
|
|
820
|
-
variant: MODE_BADGES[p.mode].variant,
|
|
821
|
-
className: "ml-1",
|
|
822
|
-
children: MODE_BADGES[p.mode].label
|
|
823
|
-
}, undefined, false, undefined, this)
|
|
824
|
-
]
|
|
825
|
-
}, p.provider, true, undefined, this))
|
|
826
|
-
}, undefined, false, undefined, this)
|
|
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
|
+
})
|
|
827
1322
|
]
|
|
828
|
-
}
|
|
829
|
-
/* @__PURE__ */ jsxDEV5("div", {
|
|
830
|
-
className: "flex flex-col gap-1.5",
|
|
831
|
-
children: [
|
|
832
|
-
/* @__PURE__ */ jsxDEV5(Label, {
|
|
833
|
-
htmlFor: "model-picker",
|
|
834
|
-
className: "text-sm font-medium",
|
|
835
|
-
children: "Model"
|
|
836
|
-
}, undefined, false, undefined, this),
|
|
837
|
-
/* @__PURE__ */ jsxDEV5(Select, {
|
|
838
|
-
name: "model-picker",
|
|
839
|
-
value: value.model,
|
|
840
|
-
onValueChange: handleModelChange,
|
|
841
|
-
children: [
|
|
842
|
-
/* @__PURE__ */ jsxDEV5(SelectTrigger, {
|
|
843
|
-
children: /* @__PURE__ */ jsxDEV5(SelectValue, {
|
|
844
|
-
placeholder: "Select a model"
|
|
845
|
-
}, undefined, false, undefined, this)
|
|
846
|
-
}, undefined, false, undefined, this),
|
|
847
|
-
/* @__PURE__ */ jsxDEV5(SelectContent, {
|
|
848
|
-
children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
|
|
849
|
-
value: m.id,
|
|
850
|
-
children: /* @__PURE__ */ jsxDEV5("div", {
|
|
851
|
-
className: "flex items-center gap-2",
|
|
852
|
-
children: [
|
|
853
|
-
/* @__PURE__ */ jsxDEV5("span", {
|
|
854
|
-
children: m.name
|
|
855
|
-
}, undefined, false, undefined, this),
|
|
856
|
-
/* @__PURE__ */ jsxDEV5("span", {
|
|
857
|
-
className: "text-muted-foreground text-xs",
|
|
858
|
-
children: [
|
|
859
|
-
Math.round(m.contextWindow / 1000),
|
|
860
|
-
"K"
|
|
861
|
-
]
|
|
862
|
-
}, undefined, true, undefined, this),
|
|
863
|
-
m.capabilities.vision && /* @__PURE__ */ jsxDEV5(Badge, {
|
|
864
|
-
variant: "outline",
|
|
865
|
-
className: "text-xs",
|
|
866
|
-
children: "Vision"
|
|
867
|
-
}, undefined, false, undefined, this),
|
|
868
|
-
m.capabilities.reasoning && /* @__PURE__ */ jsxDEV5(Badge, {
|
|
869
|
-
variant: "outline",
|
|
870
|
-
className: "text-xs",
|
|
871
|
-
children: "Reasoning"
|
|
872
|
-
}, undefined, false, undefined, this)
|
|
873
|
-
]
|
|
874
|
-
}, undefined, true, undefined, this)
|
|
875
|
-
}, m.id, false, undefined, this))
|
|
876
|
-
}, undefined, false, undefined, this)
|
|
877
|
-
]
|
|
878
|
-
}, undefined, true, undefined, this)
|
|
879
|
-
]
|
|
880
|
-
}, undefined, true, undefined, this),
|
|
881
|
-
selectedModel && /* @__PURE__ */ jsxDEV5("div", {
|
|
882
|
-
className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
|
|
883
|
-
children: [
|
|
884
|
-
/* @__PURE__ */ jsxDEV5("span", {
|
|
885
|
-
children: [
|
|
886
|
-
"Context: ",
|
|
887
|
-
Math.round(selectedModel.contextWindow / 1000),
|
|
888
|
-
"K tokens"
|
|
889
|
-
]
|
|
890
|
-
}, undefined, true, undefined, this),
|
|
891
|
-
selectedModel.capabilities.vision && /* @__PURE__ */ jsxDEV5("span", {
|
|
892
|
-
children: "• Vision"
|
|
893
|
-
}, undefined, false, undefined, this),
|
|
894
|
-
selectedModel.capabilities.tools && /* @__PURE__ */ jsxDEV5("span", {
|
|
895
|
-
children: "• Tools"
|
|
896
|
-
}, undefined, false, undefined, this),
|
|
897
|
-
selectedModel.capabilities.reasoning && /* @__PURE__ */ jsxDEV5("span", {
|
|
898
|
-
children: "• Reasoning"
|
|
899
|
-
}, undefined, false, undefined, this)
|
|
900
|
-
]
|
|
901
|
-
}, undefined, true, undefined, this)
|
|
1323
|
+
})
|
|
902
1324
|
]
|
|
903
|
-
}
|
|
1325
|
+
});
|
|
904
1326
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
import
|
|
908
|
-
import {
|
|
909
|
-
Tooltip,
|
|
910
|
-
TooltipContent,
|
|
911
|
-
TooltipProvider,
|
|
912
|
-
TooltipTrigger
|
|
913
|
-
} from "@contractspec/lib.ui-kit-web/ui/tooltip";
|
|
914
|
-
import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
|
|
915
|
-
import { jsxDEV as jsxDEV6, Fragment as Fragment3 } from "react/jsx-dev-runtime";
|
|
1327
|
+
|
|
1328
|
+
// src/presentation/hooks/useMessageSelection.ts
|
|
1329
|
+
import * as React7 from "react";
|
|
916
1330
|
"use client";
|
|
917
|
-
function
|
|
918
|
-
|
|
919
|
-
|
|
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,
|
|
920
1379
|
className,
|
|
921
|
-
|
|
1380
|
+
showExport = true,
|
|
1381
|
+
showMessageSelection = true,
|
|
1382
|
+
showScrollButton = true,
|
|
1383
|
+
onCreateNew,
|
|
1384
|
+
onFork,
|
|
1385
|
+
onEditMessage,
|
|
1386
|
+
thinkingLevel = "thinking",
|
|
1387
|
+
onThinkingLevelChange,
|
|
1388
|
+
presentationRenderer,
|
|
1389
|
+
formRenderer
|
|
922
1390
|
}) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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" });
|
|
935
1489
|
}
|
|
936
|
-
|
|
937
|
-
|
|
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"),
|
|
938
1514
|
children: [
|
|
939
|
-
/* @__PURE__ */
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
className: "h-3 w-3"
|
|
945
|
-
}, undefined, false, undefined, this),
|
|
946
|
-
"Context"
|
|
947
|
-
]
|
|
948
|
-
}, undefined, true, undefined, this),
|
|
949
|
-
summary && showDetails && /* @__PURE__ */ jsxDEV6(Fragment3, {
|
|
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",
|
|
950
1520
|
children: [
|
|
951
|
-
/* @__PURE__ */
|
|
952
|
-
className: "
|
|
953
|
-
children:
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
/* @__PURE__ */ jsxDEV6("span", {
|
|
958
|
-
children: summary.name
|
|
959
|
-
}, undefined, false, undefined, this)
|
|
960
|
-
]
|
|
961
|
-
}, undefined, true, undefined, this),
|
|
962
|
-
/* @__PURE__ */ jsxDEV6("div", {
|
|
963
|
-
className: "flex items-center gap-1 text-xs",
|
|
1521
|
+
/* @__PURE__ */ jsx9("p", {
|
|
1522
|
+
className: "truncate",
|
|
1523
|
+
children: displayTitle
|
|
1524
|
+
}),
|
|
1525
|
+
/* @__PURE__ */ jsxs9("p", {
|
|
1526
|
+
className: "text-muted-foreground text-xs",
|
|
964
1527
|
children: [
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
/* @__PURE__ */ jsxDEV6("span", {
|
|
1528
|
+
formatDate(conversation.updatedAt),
|
|
1529
|
+
conversation.projectName && ` · ${conversation.projectName}`,
|
|
1530
|
+
conversation.tags && conversation.tags.length > 0 && /* @__PURE__ */ jsxs9(Fragment5, {
|
|
969
1531
|
children: [
|
|
970
|
-
|
|
971
|
-
"
|
|
1532
|
+
" · ",
|
|
1533
|
+
conversation.tags.slice(0, 2).join(", ")
|
|
972
1534
|
]
|
|
973
|
-
}
|
|
1535
|
+
})
|
|
974
1536
|
]
|
|
975
|
-
}
|
|
1537
|
+
})
|
|
976
1538
|
]
|
|
977
|
-
},
|
|
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
|
+
})
|
|
978
1553
|
]
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
+
});
|
|
1067
1681
|
}
|
|
1682
|
+
// src/presentation/components/ChatWithSidebar.tsx
|
|
1683
|
+
import * as React12 from "react";
|
|
1684
|
+
|
|
1068
1685
|
// src/presentation/hooks/useChat.tsx
|
|
1069
|
-
import * as
|
|
1070
|
-
import { tool } from "ai";
|
|
1071
|
-
import { z } from "zod";
|
|
1686
|
+
import * as React11 from "react";
|
|
1687
|
+
import { tool as tool4 } from "ai";
|
|
1688
|
+
import { z as z4 } from "zod";
|
|
1072
1689
|
|
|
1073
1690
|
// src/core/chat-service.ts
|
|
1074
1691
|
import { generateText, streamText } from "ai";
|
|
@@ -1150,11 +1767,65 @@ class InMemoryConversationStore {
|
|
|
1150
1767
|
if (options?.status) {
|
|
1151
1768
|
results = results.filter((c) => c.status === options.status);
|
|
1152
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
|
+
}
|
|
1153
1777
|
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
1154
1778
|
const offset = options?.offset ?? 0;
|
|
1155
1779
|
const limit = options?.limit ?? 100;
|
|
1156
1780
|
return results.slice(offset, offset + limit);
|
|
1157
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
|
+
}
|
|
1158
1829
|
async search(query, limit = 20) {
|
|
1159
1830
|
const lowerQuery = query.toLowerCase();
|
|
1160
1831
|
const results = [];
|
|
@@ -1180,43 +1851,571 @@ function createInMemoryConversationStore() {
|
|
|
1180
1851
|
return new InMemoryConversationStore;
|
|
1181
1852
|
}
|
|
1182
1853
|
|
|
1183
|
-
// src/core/
|
|
1184
|
-
|
|
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';
|
|
1185
2017
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
+
}
|
|
1191
2057
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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(`
|
|
1198
2065
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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;
|
|
1211
2338
|
this.context = config.context;
|
|
1212
2339
|
this.store = config.store ?? new InMemoryConversationStore;
|
|
1213
|
-
this.systemPrompt = config
|
|
2340
|
+
this.systemPrompt = this.buildSystemPrompt(config);
|
|
1214
2341
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
1215
2342
|
this.onUsage = config.onUsage;
|
|
1216
|
-
this.tools = config
|
|
1217
|
-
this.
|
|
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");
|
|
1218
2347
|
this.sendSources = config.sendSources ?? false;
|
|
1219
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
|
+
}
|
|
1220
2419
|
async send(options) {
|
|
1221
2420
|
let conversation;
|
|
1222
2421
|
if (options.conversationId) {
|
|
@@ -1234,20 +2433,25 @@ class ChatService {
|
|
|
1234
2433
|
workspacePath: this.context?.workspacePath
|
|
1235
2434
|
});
|
|
1236
2435
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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;
|
|
1243
2445
|
const messages = this.buildMessages(conversation, options);
|
|
1244
|
-
const model = this.
|
|
2446
|
+
const { model, providerName } = await this.resolveModel();
|
|
2447
|
+
const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
1245
2448
|
try {
|
|
1246
2449
|
const result = await generateText({
|
|
1247
2450
|
model,
|
|
1248
2451
|
messages,
|
|
1249
2452
|
system: this.systemPrompt,
|
|
1250
|
-
tools: this.tools
|
|
2453
|
+
tools: this.tools,
|
|
2454
|
+
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
1251
2455
|
});
|
|
1252
2456
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
1253
2457
|
role: "assistant",
|
|
@@ -1292,23 +2496,27 @@ class ChatService {
|
|
|
1292
2496
|
workspacePath: this.context?.workspacePath
|
|
1293
2497
|
});
|
|
1294
2498
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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;
|
|
1301
2508
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
1302
2509
|
role: "assistant",
|
|
1303
2510
|
content: "",
|
|
1304
2511
|
status: "streaming"
|
|
1305
2512
|
});
|
|
1306
2513
|
const messages = this.buildMessages(conversation, options);
|
|
1307
|
-
const model = this.
|
|
2514
|
+
const { model, providerName } = await this.resolveModel();
|
|
1308
2515
|
const systemPrompt = this.systemPrompt;
|
|
1309
2516
|
const tools = this.tools;
|
|
1310
2517
|
const store = this.store;
|
|
1311
2518
|
const onUsage = this.onUsage;
|
|
2519
|
+
const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
1312
2520
|
async function* streamGenerator() {
|
|
1313
2521
|
let fullContent = "";
|
|
1314
2522
|
let fullReasoning = "";
|
|
@@ -1319,7 +2527,8 @@ class ChatService {
|
|
|
1319
2527
|
model,
|
|
1320
2528
|
messages,
|
|
1321
2529
|
system: systemPrompt,
|
|
1322
|
-
tools
|
|
2530
|
+
tools,
|
|
2531
|
+
providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
|
|
1323
2532
|
});
|
|
1324
2533
|
for await (const part of result.fullStream) {
|
|
1325
2534
|
if (part.type === "text-delta") {
|
|
@@ -1434,6 +2643,18 @@ class ChatService {
|
|
|
1434
2643
|
...options
|
|
1435
2644
|
});
|
|
1436
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
|
+
}
|
|
1437
2658
|
async deleteConversation(conversationId) {
|
|
1438
2659
|
return this.store.delete(conversationId);
|
|
1439
2660
|
}
|
|
@@ -1504,9 +2725,9 @@ import {
|
|
|
1504
2725
|
function toolsToToolSet(defs) {
|
|
1505
2726
|
const result = {};
|
|
1506
2727
|
for (const def of defs) {
|
|
1507
|
-
result[def.name] =
|
|
2728
|
+
result[def.name] = tool4({
|
|
1508
2729
|
description: def.description ?? def.name,
|
|
1509
|
-
inputSchema:
|
|
2730
|
+
inputSchema: z4.object({}).passthrough(),
|
|
1510
2731
|
execute: async () => ({})
|
|
1511
2732
|
});
|
|
1512
2733
|
}
|
|
@@ -1520,22 +2741,64 @@ function useChat(options = {}) {
|
|
|
1520
2741
|
apiKey,
|
|
1521
2742
|
proxyUrl,
|
|
1522
2743
|
conversationId: initialConversationId,
|
|
2744
|
+
store,
|
|
1523
2745
|
systemPrompt,
|
|
1524
2746
|
streaming = true,
|
|
1525
2747
|
onSend,
|
|
1526
2748
|
onResponse,
|
|
1527
2749
|
onError,
|
|
1528
2750
|
onUsage,
|
|
1529
|
-
tools: toolsDefs
|
|
2751
|
+
tools: toolsDefs,
|
|
2752
|
+
thinkingLevel,
|
|
2753
|
+
workflowToolsConfig,
|
|
2754
|
+
modelSelector,
|
|
2755
|
+
contractsContext,
|
|
2756
|
+
surfacePlanConfig,
|
|
2757
|
+
mcpServers,
|
|
2758
|
+
agentMode
|
|
1530
2759
|
} = options;
|
|
1531
|
-
const [messages, setMessages] =
|
|
1532
|
-
const [
|
|
1533
|
-
const
|
|
1534
|
-
const [
|
|
1535
|
-
const [
|
|
1536
|
-
const
|
|
1537
|
-
const
|
|
1538
|
-
|
|
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(() => {
|
|
1539
2802
|
const chatProvider = createProvider({
|
|
1540
2803
|
provider,
|
|
1541
2804
|
model,
|
|
@@ -1544,9 +2807,16 @@ function useChat(options = {}) {
|
|
|
1544
2807
|
});
|
|
1545
2808
|
chatServiceRef.current = new ChatService({
|
|
1546
2809
|
provider: chatProvider,
|
|
2810
|
+
store,
|
|
1547
2811
|
systemPrompt,
|
|
1548
2812
|
onUsage,
|
|
1549
|
-
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
|
|
2813
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
|
|
2814
|
+
thinkingLevel,
|
|
2815
|
+
workflowToolsConfig,
|
|
2816
|
+
modelSelector,
|
|
2817
|
+
contractsContext,
|
|
2818
|
+
surfacePlanConfig,
|
|
2819
|
+
mcpTools
|
|
1550
2820
|
});
|
|
1551
2821
|
}, [
|
|
1552
2822
|
provider,
|
|
@@ -1554,11 +2824,18 @@ function useChat(options = {}) {
|
|
|
1554
2824
|
model,
|
|
1555
2825
|
apiKey,
|
|
1556
2826
|
proxyUrl,
|
|
2827
|
+
store,
|
|
1557
2828
|
systemPrompt,
|
|
1558
2829
|
onUsage,
|
|
1559
|
-
toolsDefs
|
|
2830
|
+
toolsDefs,
|
|
2831
|
+
thinkingLevel,
|
|
2832
|
+
workflowToolsConfig,
|
|
2833
|
+
modelSelector,
|
|
2834
|
+
contractsContext,
|
|
2835
|
+
surfacePlanConfig,
|
|
2836
|
+
mcpTools
|
|
1560
2837
|
]);
|
|
1561
|
-
|
|
2838
|
+
React11.useEffect(() => {
|
|
1562
2839
|
if (!conversationId || !chatServiceRef.current)
|
|
1563
2840
|
return;
|
|
1564
2841
|
const loadConversation = async () => {
|
|
@@ -1572,7 +2849,90 @@ function useChat(options = {}) {
|
|
|
1572
2849
|
};
|
|
1573
2850
|
loadConversation().catch(console.error);
|
|
1574
2851
|
}, [conversationId]);
|
|
1575
|
-
const sendMessage =
|
|
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
|
+
}
|
|
1576
2936
|
if (!chatServiceRef.current) {
|
|
1577
2937
|
throw new Error("Chat service not initialized");
|
|
1578
2938
|
}
|
|
@@ -1580,25 +2940,28 @@ function useChat(options = {}) {
|
|
|
1580
2940
|
setError(null);
|
|
1581
2941
|
abortControllerRef.current = new AbortController;
|
|
1582
2942
|
try {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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
|
+
}
|
|
1595
2957
|
if (streaming) {
|
|
1596
2958
|
const result = await chatServiceRef.current.stream({
|
|
1597
2959
|
conversationId: conversationId ?? undefined,
|
|
1598
2960
|
content,
|
|
1599
|
-
attachments
|
|
2961
|
+
attachments,
|
|
2962
|
+
skipUserAppend: opts?.skipUserAppend
|
|
1600
2963
|
});
|
|
1601
|
-
if (!conversationId) {
|
|
2964
|
+
if (!conversationId && !opts?.skipUserAppend) {
|
|
1602
2965
|
setConversationId(result.conversationId);
|
|
1603
2966
|
}
|
|
1604
2967
|
const assistantMessage = {
|
|
@@ -1679,7 +3042,8 @@ function useChat(options = {}) {
|
|
|
1679
3042
|
const result = await chatServiceRef.current.send({
|
|
1680
3043
|
conversationId: conversationId ?? undefined,
|
|
1681
3044
|
content,
|
|
1682
|
-
attachments
|
|
3045
|
+
attachments,
|
|
3046
|
+
skipUserAppend: opts?.skipUserAppend
|
|
1683
3047
|
});
|
|
1684
3048
|
setConversation(result.conversation);
|
|
1685
3049
|
setMessages(result.conversation.messages);
|
|
@@ -1696,14 +3060,24 @@ function useChat(options = {}) {
|
|
|
1696
3060
|
setIsLoading(false);
|
|
1697
3061
|
abortControllerRef.current = null;
|
|
1698
3062
|
}
|
|
1699
|
-
}, [
|
|
1700
|
-
|
|
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(() => {
|
|
1701
3075
|
setMessages([]);
|
|
1702
3076
|
setConversation(null);
|
|
1703
3077
|
setConversationId(null);
|
|
1704
3078
|
setError(null);
|
|
1705
3079
|
}, []);
|
|
1706
|
-
const regenerate =
|
|
3080
|
+
const regenerate = React11.useCallback(async () => {
|
|
1707
3081
|
const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
|
|
1708
3082
|
if (lastUserMessageIndex === -1)
|
|
1709
3083
|
return;
|
|
@@ -1713,11 +3087,51 @@ function useChat(options = {}) {
|
|
|
1713
3087
|
setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
|
|
1714
3088
|
await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
|
|
1715
3089
|
}, [messages, sendMessage]);
|
|
1716
|
-
const stop =
|
|
3090
|
+
const stop = React11.useCallback(() => {
|
|
1717
3091
|
abortControllerRef.current?.abort();
|
|
1718
3092
|
setIsLoading(false);
|
|
1719
3093
|
}, []);
|
|
1720
|
-
const
|
|
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) => {
|
|
1721
3135
|
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
1722
3136
|
}, []);
|
|
1723
3137
|
const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
|
|
@@ -1731,40 +3145,751 @@ function useChat(options = {}) {
|
|
|
1731
3145
|
setConversationId,
|
|
1732
3146
|
regenerate,
|
|
1733
3147
|
stop,
|
|
3148
|
+
createNewConversation,
|
|
3149
|
+
editMessage,
|
|
3150
|
+
forkConversation,
|
|
3151
|
+
updateConversation: updateConversationFn,
|
|
1734
3152
|
...hasApprovalTools && { addToolApprovalResponse }
|
|
1735
3153
|
};
|
|
1736
3154
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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);
|
|
1760
3198
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
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";
|
|
3472
|
+
import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
|
|
3473
|
+
import {
|
|
3474
|
+
getModelsForProvider
|
|
3475
|
+
} from "@contractspec/lib.ai-providers";
|
|
3476
|
+
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3477
|
+
"use client";
|
|
3478
|
+
var PROVIDER_ICONS = {
|
|
3479
|
+
ollama: /* @__PURE__ */ jsx11(Cpu, {
|
|
3480
|
+
className: "h-4 w-4"
|
|
3481
|
+
}),
|
|
3482
|
+
openai: /* @__PURE__ */ jsx11(Bot2, {
|
|
3483
|
+
className: "h-4 w-4"
|
|
3484
|
+
}),
|
|
3485
|
+
anthropic: /* @__PURE__ */ jsx11(Sparkles, {
|
|
3486
|
+
className: "h-4 w-4"
|
|
3487
|
+
}),
|
|
3488
|
+
mistral: /* @__PURE__ */ jsx11(Cloud, {
|
|
3489
|
+
className: "h-4 w-4"
|
|
3490
|
+
}),
|
|
3491
|
+
gemini: /* @__PURE__ */ jsx11(Sparkles, {
|
|
3492
|
+
className: "h-4 w-4"
|
|
3493
|
+
})
|
|
3494
|
+
};
|
|
3495
|
+
var PROVIDER_NAMES = {
|
|
3496
|
+
ollama: "Ollama (Local)",
|
|
3497
|
+
openai: "OpenAI",
|
|
3498
|
+
anthropic: "Anthropic",
|
|
3499
|
+
mistral: "Mistral",
|
|
3500
|
+
gemini: "Google Gemini"
|
|
3501
|
+
};
|
|
3502
|
+
var MODE_BADGES = {
|
|
3503
|
+
local: { label: "Local", variant: "secondary" },
|
|
3504
|
+
byok: { label: "BYOK", variant: "outline" },
|
|
3505
|
+
managed: { label: "Managed", variant: "default" }
|
|
3506
|
+
};
|
|
3507
|
+
function ModelPicker({
|
|
3508
|
+
value,
|
|
3509
|
+
onChange,
|
|
3510
|
+
availableProviders,
|
|
3511
|
+
className,
|
|
3512
|
+
compact = false
|
|
3513
|
+
}) {
|
|
3514
|
+
const providers = availableProviders ?? [
|
|
3515
|
+
{ provider: "ollama", available: true, mode: "local" },
|
|
3516
|
+
{ provider: "openai", available: true, mode: "byok" },
|
|
3517
|
+
{ provider: "anthropic", available: true, mode: "byok" },
|
|
3518
|
+
{ provider: "mistral", available: true, mode: "byok" },
|
|
3519
|
+
{ provider: "gemini", available: true, mode: "byok" }
|
|
3520
|
+
];
|
|
3521
|
+
const models = getModelsForProvider(value.provider);
|
|
3522
|
+
const selectedModel = models.find((m) => m.id === value.model);
|
|
3523
|
+
const handleProviderChange = React13.useCallback((providerName) => {
|
|
3524
|
+
const provider = providerName;
|
|
3525
|
+
const providerInfo = providers.find((p) => p.provider === provider);
|
|
3526
|
+
const providerModels = getModelsForProvider(provider);
|
|
3527
|
+
const defaultModel = providerModels[0]?.id ?? "";
|
|
3528
|
+
onChange({
|
|
3529
|
+
provider,
|
|
3530
|
+
model: defaultModel,
|
|
3531
|
+
mode: providerInfo?.mode ?? "byok"
|
|
3532
|
+
});
|
|
3533
|
+
}, [onChange, providers]);
|
|
3534
|
+
const handleModelChange = React13.useCallback((modelId) => {
|
|
3535
|
+
onChange({
|
|
3536
|
+
...value,
|
|
3537
|
+
model: modelId
|
|
3538
|
+
});
|
|
3539
|
+
}, [onChange, value]);
|
|
3540
|
+
if (compact) {
|
|
3541
|
+
return /* @__PURE__ */ jsxs11("div", {
|
|
3542
|
+
className: cn7("flex items-center gap-2", className),
|
|
3543
|
+
children: [
|
|
3544
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
3545
|
+
value: value.provider,
|
|
3546
|
+
onValueChange: handleProviderChange,
|
|
3547
|
+
children: [
|
|
3548
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
3549
|
+
className: "w-[140px]",
|
|
3550
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {})
|
|
3551
|
+
}),
|
|
3552
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3553
|
+
children: providers.map((p) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
3554
|
+
value: p.provider,
|
|
3555
|
+
disabled: !p.available,
|
|
3556
|
+
children: /* @__PURE__ */ jsxs11("div", {
|
|
3557
|
+
className: "flex items-center gap-2",
|
|
3558
|
+
children: [
|
|
3559
|
+
PROVIDER_ICONS[p.provider],
|
|
3560
|
+
/* @__PURE__ */ jsx11("span", {
|
|
3561
|
+
children: PROVIDER_NAMES[p.provider]
|
|
3562
|
+
})
|
|
3563
|
+
]
|
|
3564
|
+
})
|
|
3565
|
+
}, p.provider))
|
|
3566
|
+
})
|
|
3567
|
+
]
|
|
3568
|
+
}),
|
|
3569
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
3570
|
+
value: value.model,
|
|
3571
|
+
onValueChange: handleModelChange,
|
|
3572
|
+
children: [
|
|
3573
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
3574
|
+
className: "w-[160px]",
|
|
3575
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {})
|
|
3576
|
+
}),
|
|
3577
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3578
|
+
children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
3579
|
+
value: m.id,
|
|
3580
|
+
children: m.name
|
|
3581
|
+
}, m.id))
|
|
3582
|
+
})
|
|
3583
|
+
]
|
|
3584
|
+
})
|
|
3585
|
+
]
|
|
3586
|
+
});
|
|
3587
|
+
}
|
|
3588
|
+
return /* @__PURE__ */ jsxs11("div", {
|
|
3589
|
+
className: cn7("flex flex-col gap-3", className),
|
|
3590
|
+
children: [
|
|
3591
|
+
/* @__PURE__ */ jsxs11("div", {
|
|
3592
|
+
className: "flex flex-col gap-1.5",
|
|
3593
|
+
children: [
|
|
3594
|
+
/* @__PURE__ */ jsx11(Label2, {
|
|
3595
|
+
htmlFor: "provider-selection",
|
|
3596
|
+
className: "text-sm font-medium",
|
|
3597
|
+
children: "Provider"
|
|
3598
|
+
}),
|
|
3599
|
+
/* @__PURE__ */ jsx11("div", {
|
|
3600
|
+
className: "flex flex-wrap gap-2",
|
|
3601
|
+
id: "provider-selection",
|
|
3602
|
+
children: providers.map((p) => /* @__PURE__ */ jsxs11(Button6, {
|
|
3603
|
+
variant: value.provider === p.provider ? "default" : "outline",
|
|
3604
|
+
size: "sm",
|
|
3605
|
+
onPress: () => p.available && handleProviderChange(p.provider),
|
|
3606
|
+
disabled: !p.available,
|
|
3607
|
+
className: cn7(!p.available && "opacity-50"),
|
|
3608
|
+
children: [
|
|
3609
|
+
PROVIDER_ICONS[p.provider],
|
|
3610
|
+
/* @__PURE__ */ jsx11("span", {
|
|
3611
|
+
children: PROVIDER_NAMES[p.provider]
|
|
3612
|
+
}),
|
|
3613
|
+
/* @__PURE__ */ jsx11(Badge, {
|
|
3614
|
+
variant: MODE_BADGES[p.mode].variant,
|
|
3615
|
+
className: "ml-1",
|
|
3616
|
+
children: MODE_BADGES[p.mode].label
|
|
3617
|
+
})
|
|
3618
|
+
]
|
|
3619
|
+
}, p.provider))
|
|
3620
|
+
})
|
|
3621
|
+
]
|
|
3622
|
+
}),
|
|
3623
|
+
/* @__PURE__ */ jsxs11("div", {
|
|
3624
|
+
className: "flex flex-col gap-1.5",
|
|
3625
|
+
children: [
|
|
3626
|
+
/* @__PURE__ */ jsx11(Label2, {
|
|
3627
|
+
htmlFor: "model-picker",
|
|
3628
|
+
className: "text-sm font-medium",
|
|
3629
|
+
children: "Model"
|
|
3630
|
+
}),
|
|
3631
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
3632
|
+
name: "model-picker",
|
|
3633
|
+
value: value.model,
|
|
3634
|
+
onValueChange: handleModelChange,
|
|
3635
|
+
children: [
|
|
3636
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
3637
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {
|
|
3638
|
+
placeholder: "Select a model"
|
|
3639
|
+
})
|
|
3640
|
+
}),
|
|
3641
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3642
|
+
children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
3643
|
+
value: m.id,
|
|
3644
|
+
children: /* @__PURE__ */ jsxs11("div", {
|
|
3645
|
+
className: "flex items-center gap-2",
|
|
3646
|
+
children: [
|
|
3647
|
+
/* @__PURE__ */ jsx11("span", {
|
|
3648
|
+
children: m.name
|
|
3649
|
+
}),
|
|
3650
|
+
/* @__PURE__ */ jsxs11("span", {
|
|
3651
|
+
className: "text-muted-foreground text-xs",
|
|
3652
|
+
children: [
|
|
3653
|
+
Math.round(m.contextWindow / 1000),
|
|
3654
|
+
"K"
|
|
3655
|
+
]
|
|
3656
|
+
}),
|
|
3657
|
+
m.capabilities.vision && /* @__PURE__ */ jsx11(Badge, {
|
|
3658
|
+
variant: "outline",
|
|
3659
|
+
className: "text-xs",
|
|
3660
|
+
children: "Vision"
|
|
3661
|
+
}),
|
|
3662
|
+
m.capabilities.reasoning && /* @__PURE__ */ jsx11(Badge, {
|
|
3663
|
+
variant: "outline",
|
|
3664
|
+
className: "text-xs",
|
|
3665
|
+
children: "Reasoning"
|
|
3666
|
+
})
|
|
3667
|
+
]
|
|
3668
|
+
})
|
|
3669
|
+
}, m.id))
|
|
3670
|
+
})
|
|
3671
|
+
]
|
|
3672
|
+
})
|
|
3673
|
+
]
|
|
3674
|
+
}),
|
|
3675
|
+
selectedModel && /* @__PURE__ */ jsxs11("div", {
|
|
3676
|
+
className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
|
|
3677
|
+
children: [
|
|
3678
|
+
/* @__PURE__ */ jsxs11("span", {
|
|
3679
|
+
children: [
|
|
3680
|
+
"Context: ",
|
|
3681
|
+
Math.round(selectedModel.contextWindow / 1000),
|
|
3682
|
+
"K tokens"
|
|
3683
|
+
]
|
|
3684
|
+
}),
|
|
3685
|
+
selectedModel.capabilities.vision && /* @__PURE__ */ jsx11("span", {
|
|
3686
|
+
children: "• Vision"
|
|
3687
|
+
}),
|
|
3688
|
+
selectedModel.capabilities.tools && /* @__PURE__ */ jsx11("span", {
|
|
3689
|
+
children: "• Tools"
|
|
3690
|
+
}),
|
|
3691
|
+
selectedModel.capabilities.reasoning && /* @__PURE__ */ jsx11("span", {
|
|
3692
|
+
children: "• Reasoning"
|
|
3693
|
+
})
|
|
3694
|
+
]
|
|
3695
|
+
})
|
|
3696
|
+
]
|
|
3697
|
+
});
|
|
3698
|
+
}
|
|
3699
|
+
// src/presentation/components/ContextIndicator.tsx
|
|
3700
|
+
import { cn as cn8 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
3701
|
+
import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
3702
|
+
import {
|
|
3703
|
+
Tooltip,
|
|
3704
|
+
TooltipContent,
|
|
3705
|
+
TooltipProvider,
|
|
3706
|
+
TooltipTrigger
|
|
3707
|
+
} from "@contractspec/lib.ui-kit-web/ui/tooltip";
|
|
3708
|
+
import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
|
|
3709
|
+
import { jsx as jsx12, jsxs as jsxs12, Fragment as Fragment6 } from "react/jsx-runtime";
|
|
3710
|
+
"use client";
|
|
3711
|
+
function ContextIndicator({
|
|
3712
|
+
summary,
|
|
3713
|
+
active = false,
|
|
3714
|
+
className,
|
|
3715
|
+
showDetails = true
|
|
3716
|
+
}) {
|
|
3717
|
+
if (!summary && !active) {
|
|
3718
|
+
return /* @__PURE__ */ jsxs12("div", {
|
|
3719
|
+
className: cn8("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
|
|
3720
|
+
children: [
|
|
3721
|
+
/* @__PURE__ */ jsx12(Info, {
|
|
3722
|
+
className: "h-4 w-4"
|
|
3723
|
+
}),
|
|
3724
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3725
|
+
children: "No workspace context"
|
|
3726
|
+
})
|
|
3727
|
+
]
|
|
3728
|
+
});
|
|
3729
|
+
}
|
|
3730
|
+
const content = /* @__PURE__ */ jsxs12("div", {
|
|
3731
|
+
className: cn8("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
|
|
3732
|
+
children: [
|
|
3733
|
+
/* @__PURE__ */ jsxs12(Badge2, {
|
|
3734
|
+
variant: active ? "default" : "secondary",
|
|
3735
|
+
className: "flex items-center gap-1",
|
|
3736
|
+
children: [
|
|
3737
|
+
/* @__PURE__ */ jsx12(Zap, {
|
|
3738
|
+
className: "h-3 w-3"
|
|
3739
|
+
}),
|
|
3740
|
+
"Context"
|
|
3741
|
+
]
|
|
3742
|
+
}),
|
|
3743
|
+
summary && showDetails && /* @__PURE__ */ jsxs12(Fragment6, {
|
|
3744
|
+
children: [
|
|
3745
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
3746
|
+
className: "flex items-center gap-1 text-xs",
|
|
3747
|
+
children: [
|
|
3748
|
+
/* @__PURE__ */ jsx12(FolderOpen, {
|
|
3749
|
+
className: "h-3.5 w-3.5"
|
|
3750
|
+
}),
|
|
3751
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3752
|
+
children: summary.name
|
|
3753
|
+
})
|
|
3754
|
+
]
|
|
3755
|
+
}),
|
|
3756
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
3757
|
+
className: "flex items-center gap-1 text-xs",
|
|
3758
|
+
children: [
|
|
3759
|
+
/* @__PURE__ */ jsx12(FileCode, {
|
|
3760
|
+
className: "h-3.5 w-3.5"
|
|
3761
|
+
}),
|
|
3762
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
3763
|
+
children: [
|
|
3764
|
+
summary.specs.total,
|
|
3765
|
+
" specs"
|
|
3766
|
+
]
|
|
3767
|
+
})
|
|
3768
|
+
]
|
|
3769
|
+
})
|
|
3770
|
+
]
|
|
3771
|
+
})
|
|
3772
|
+
]
|
|
3773
|
+
});
|
|
3774
|
+
if (!summary) {
|
|
3775
|
+
return content;
|
|
3776
|
+
}
|
|
3777
|
+
return /* @__PURE__ */ jsx12(TooltipProvider, {
|
|
3778
|
+
children: /* @__PURE__ */ jsxs12(Tooltip, {
|
|
3779
|
+
children: [
|
|
3780
|
+
/* @__PURE__ */ jsx12(TooltipTrigger, {
|
|
3781
|
+
asChild: true,
|
|
3782
|
+
children: content
|
|
3783
|
+
}),
|
|
3784
|
+
/* @__PURE__ */ jsx12(TooltipContent, {
|
|
3785
|
+
side: "bottom",
|
|
3786
|
+
className: "max-w-[300px]",
|
|
3787
|
+
children: /* @__PURE__ */ jsxs12("div", {
|
|
3788
|
+
className: "flex flex-col gap-2 text-sm",
|
|
3789
|
+
children: [
|
|
3790
|
+
/* @__PURE__ */ jsx12("div", {
|
|
3791
|
+
className: "font-medium",
|
|
3792
|
+
children: summary.name
|
|
3793
|
+
}),
|
|
3794
|
+
/* @__PURE__ */ jsx12("div", {
|
|
3795
|
+
className: "text-muted-foreground text-xs",
|
|
3796
|
+
children: summary.path
|
|
3797
|
+
}),
|
|
3798
|
+
/* @__PURE__ */ jsx12("div", {
|
|
3799
|
+
className: "border-t pt-2",
|
|
3800
|
+
children: /* @__PURE__ */ jsxs12("div", {
|
|
3801
|
+
className: "grid grid-cols-2 gap-1 text-xs",
|
|
3802
|
+
children: [
|
|
3803
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3804
|
+
children: "Commands:"
|
|
3805
|
+
}),
|
|
3806
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3807
|
+
className: "text-right",
|
|
3808
|
+
children: summary.specs.commands
|
|
3809
|
+
}),
|
|
3810
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3811
|
+
children: "Queries:"
|
|
3812
|
+
}),
|
|
3813
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3814
|
+
className: "text-right",
|
|
3815
|
+
children: summary.specs.queries
|
|
3816
|
+
}),
|
|
3817
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3818
|
+
children: "Events:"
|
|
3819
|
+
}),
|
|
3820
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3821
|
+
className: "text-right",
|
|
3822
|
+
children: summary.specs.events
|
|
3823
|
+
}),
|
|
3824
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3825
|
+
children: "Presentations:"
|
|
3826
|
+
}),
|
|
3827
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3828
|
+
className: "text-right",
|
|
3829
|
+
children: summary.specs.presentations
|
|
3830
|
+
})
|
|
3831
|
+
]
|
|
3832
|
+
})
|
|
3833
|
+
}),
|
|
3834
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
3835
|
+
className: "border-t pt-2 text-xs",
|
|
3836
|
+
children: [
|
|
3837
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
3838
|
+
children: [
|
|
3839
|
+
summary.files.total,
|
|
3840
|
+
" files"
|
|
3841
|
+
]
|
|
3842
|
+
}),
|
|
3843
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3844
|
+
className: "mx-1",
|
|
3845
|
+
children: "•"
|
|
3846
|
+
}),
|
|
3847
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
3848
|
+
children: [
|
|
3849
|
+
summary.files.specFiles,
|
|
3850
|
+
" spec files"
|
|
3851
|
+
]
|
|
3852
|
+
})
|
|
3853
|
+
]
|
|
3854
|
+
})
|
|
3855
|
+
]
|
|
3856
|
+
})
|
|
3857
|
+
})
|
|
3858
|
+
]
|
|
3859
|
+
})
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
// src/presentation/hooks/useProviders.tsx
|
|
3863
|
+
import * as React14 from "react";
|
|
3864
|
+
import {
|
|
3865
|
+
getAvailableProviders,
|
|
3866
|
+
getModelsForProvider as getModelsForProvider2
|
|
3867
|
+
} from "@contractspec/lib.ai-providers";
|
|
3868
|
+
"use client";
|
|
3869
|
+
function useProviders() {
|
|
3870
|
+
const [providers, setProviders] = React14.useState([]);
|
|
3871
|
+
const [isLoading, setIsLoading] = React14.useState(true);
|
|
3872
|
+
const loadProviders = React14.useCallback(async () => {
|
|
3873
|
+
setIsLoading(true);
|
|
3874
|
+
try {
|
|
3875
|
+
const available = getAvailableProviders();
|
|
3876
|
+
const providersWithModels = available.map((p) => ({
|
|
3877
|
+
...p,
|
|
3878
|
+
models: getModelsForProvider2(p.provider)
|
|
3879
|
+
}));
|
|
3880
|
+
setProviders(providersWithModels);
|
|
3881
|
+
} catch (error) {
|
|
3882
|
+
console.error("Failed to load providers:", error);
|
|
3883
|
+
} finally {
|
|
3884
|
+
setIsLoading(false);
|
|
3885
|
+
}
|
|
3886
|
+
}, []);
|
|
3887
|
+
React14.useEffect(() => {
|
|
1763
3888
|
loadProviders();
|
|
1764
3889
|
}, [loadProviders]);
|
|
1765
|
-
const availableProviders =
|
|
1766
|
-
const isAvailable =
|
|
1767
|
-
const getModelsCallback =
|
|
3890
|
+
const availableProviders = React14.useMemo(() => providers.filter((p) => p.available), [providers]);
|
|
3891
|
+
const isAvailable = React14.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
|
|
3892
|
+
const getModelsCallback = React14.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
|
|
1768
3893
|
return {
|
|
1769
3894
|
providers,
|
|
1770
3895
|
availableProviders,
|
|
@@ -1779,12 +3904,22 @@ function useProviders() {
|
|
|
1779
3904
|
import { useCompletion } from "@ai-sdk/react";
|
|
1780
3905
|
export {
|
|
1781
3906
|
useProviders,
|
|
3907
|
+
useMessageSelection,
|
|
3908
|
+
useConversations,
|
|
1782
3909
|
useCompletion,
|
|
1783
3910
|
useChat,
|
|
3911
|
+
isPresentationToolResult,
|
|
3912
|
+
isFormToolResult,
|
|
3913
|
+
ToolResultRenderer,
|
|
3914
|
+
ThinkingLevelPicker,
|
|
1784
3915
|
ModelPicker,
|
|
1785
3916
|
ContextIndicator,
|
|
1786
3917
|
CodePreview,
|
|
3918
|
+
ChatWithSidebar,
|
|
3919
|
+
ChatWithExport,
|
|
3920
|
+
ChatSidebar,
|
|
1787
3921
|
ChatMessage,
|
|
1788
3922
|
ChatInput,
|
|
3923
|
+
ChatExportToolbar,
|
|
1789
3924
|
ChatContainer
|
|
1790
3925
|
};
|