@contractspec/module.ai-chat 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +1138 -21
- package/dist/browser/index.js +2816 -651
- package/dist/browser/presentation/components/index.js +3143 -358
- package/dist/browser/presentation/hooks/index.js +961 -43
- package/dist/browser/presentation/index.js +2784 -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 +1138 -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 +2816 -651
- package/dist/node/core/index.js +1138 -21
- package/dist/node/index.js +2816 -651
- package/dist/node/presentation/components/index.js +3143 -358
- package/dist/node/presentation/hooks/index.js +961 -43
- package/dist/node/presentation/index.js +2787 -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 +3143 -358
- package/dist/presentation/hooks/index.d.ts +2 -0
- package/dist/presentation/hooks/index.js +961 -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 +2787 -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: "mt-2 rounded-md border border-border bg-background/50 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: "mt-2 rounded-md border border-border bg-background/50 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,566 @@ 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 { validatePatchProposal } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
|
|
2151
|
+
import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
|
|
2152
|
+
var VALID_OPS = [
|
|
2153
|
+
"insert-node",
|
|
2154
|
+
"replace-node",
|
|
2155
|
+
"remove-node",
|
|
2156
|
+
"move-node",
|
|
2157
|
+
"resize-panel",
|
|
2158
|
+
"set-layout",
|
|
2159
|
+
"reveal-field",
|
|
2160
|
+
"hide-field",
|
|
2161
|
+
"promote-action",
|
|
2162
|
+
"set-focus"
|
|
2163
|
+
];
|
|
2164
|
+
var DEFAULT_NODE_KINDS = [
|
|
2165
|
+
"entity-section",
|
|
2166
|
+
"entity-card",
|
|
2167
|
+
"data-view",
|
|
2168
|
+
"assistant-panel",
|
|
2169
|
+
"chat-thread",
|
|
2170
|
+
"action-bar",
|
|
2171
|
+
"timeline",
|
|
2172
|
+
"table",
|
|
2173
|
+
"rich-doc",
|
|
2174
|
+
"form",
|
|
2175
|
+
"chart",
|
|
2176
|
+
"custom-widget"
|
|
2177
|
+
];
|
|
2178
|
+
function collectSlotIdsFromRegion(node) {
|
|
2179
|
+
const ids = [];
|
|
2180
|
+
if (node.type === "slot") {
|
|
2181
|
+
ids.push(node.slotId);
|
|
2182
|
+
}
|
|
2183
|
+
if (node.type === "panel-group" || node.type === "stack") {
|
|
2184
|
+
for (const child of node.children) {
|
|
2185
|
+
ids.push(...collectSlotIdsFromRegion(child));
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
if (node.type === "tabs") {
|
|
2189
|
+
for (const tab of node.tabs) {
|
|
2190
|
+
ids.push(...collectSlotIdsFromRegion(tab.child));
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
if (node.type === "floating") {
|
|
2194
|
+
ids.push(node.anchorSlotId);
|
|
2195
|
+
ids.push(...collectSlotIdsFromRegion(node.child));
|
|
2196
|
+
}
|
|
2197
|
+
return ids;
|
|
2198
|
+
}
|
|
2199
|
+
function deriveConstraints(plan) {
|
|
2200
|
+
const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
|
|
2201
|
+
const uniqueSlots = [...new Set(slotIds)];
|
|
2202
|
+
return {
|
|
2203
|
+
allowedOps: VALID_OPS,
|
|
2204
|
+
allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
|
|
2205
|
+
allowedNodeKinds: DEFAULT_NODE_KINDS
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
var ProposePatchInputSchema = z3.object({
|
|
2209
|
+
proposalId: z3.string().describe("Unique proposal identifier"),
|
|
2210
|
+
ops: z3.array(z3.object({
|
|
2211
|
+
op: z3.enum([
|
|
2212
|
+
"insert-node",
|
|
2213
|
+
"replace-node",
|
|
2214
|
+
"remove-node",
|
|
2215
|
+
"move-node",
|
|
2216
|
+
"resize-panel",
|
|
2217
|
+
"set-layout",
|
|
2218
|
+
"reveal-field",
|
|
2219
|
+
"hide-field",
|
|
2220
|
+
"promote-action",
|
|
2221
|
+
"set-focus"
|
|
2222
|
+
]),
|
|
2223
|
+
slotId: z3.string().optional(),
|
|
2224
|
+
nodeId: z3.string().optional(),
|
|
2225
|
+
toSlotId: z3.string().optional(),
|
|
2226
|
+
index: z3.number().optional(),
|
|
2227
|
+
node: z3.object({
|
|
2228
|
+
nodeId: z3.string(),
|
|
2229
|
+
kind: z3.string(),
|
|
2230
|
+
title: z3.string().optional(),
|
|
2231
|
+
props: z3.record(z3.string(), z3.unknown()).optional(),
|
|
2232
|
+
children: z3.array(z3.unknown()).optional()
|
|
2233
|
+
}).optional(),
|
|
2234
|
+
persistKey: z3.string().optional(),
|
|
2235
|
+
sizes: z3.array(z3.number()).optional(),
|
|
2236
|
+
layoutId: z3.string().optional(),
|
|
2237
|
+
fieldId: z3.string().optional(),
|
|
2238
|
+
actionId: z3.string().optional(),
|
|
2239
|
+
placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
|
|
2240
|
+
targetId: z3.string().optional()
|
|
2241
|
+
}))
|
|
2242
|
+
});
|
|
2243
|
+
function createSurfacePlannerTools(config) {
|
|
2244
|
+
const { plan, constraints, onPatchProposal } = config;
|
|
2245
|
+
const resolvedConstraints = constraints ?? deriveConstraints(plan);
|
|
2246
|
+
const proposePatchTool = tool3({
|
|
2247
|
+
description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
|
|
2248
|
+
inputSchema: ProposePatchInputSchema,
|
|
2249
|
+
execute: async (input) => {
|
|
2250
|
+
const ops = input.ops;
|
|
2251
|
+
try {
|
|
2252
|
+
validatePatchProposal(ops, resolvedConstraints);
|
|
2253
|
+
const proposal = buildSurfacePatchProposal(input.proposalId, ops);
|
|
2254
|
+
onPatchProposal?.(proposal);
|
|
2255
|
+
return {
|
|
2256
|
+
success: true,
|
|
2257
|
+
proposalId: proposal.proposalId,
|
|
2258
|
+
opsCount: proposal.ops.length,
|
|
2259
|
+
message: "Patch proposal validated; awaiting user approval"
|
|
2260
|
+
};
|
|
2261
|
+
} catch (err) {
|
|
2262
|
+
return {
|
|
2263
|
+
success: false,
|
|
2264
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2265
|
+
proposalId: input.proposalId
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
return {
|
|
2271
|
+
"propose-patch": proposePatchTool
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
function buildPlannerPromptInput(plan) {
|
|
2275
|
+
const constraints = deriveConstraints(plan);
|
|
2276
|
+
return {
|
|
2277
|
+
bundleMeta: {
|
|
2278
|
+
key: plan.bundleKey,
|
|
2279
|
+
version: "0.0.0",
|
|
2280
|
+
title: plan.bundleKey
|
|
2281
|
+
},
|
|
2282
|
+
surfaceId: plan.surfaceId,
|
|
2283
|
+
allowedPatchOps: constraints.allowedOps,
|
|
2284
|
+
allowedSlots: [...constraints.allowedSlots],
|
|
2285
|
+
allowedNodeKinds: [...constraints.allowedNodeKinds],
|
|
2286
|
+
actions: plan.actions.map((a) => ({ actionId: a.actionId, title: a.title })),
|
|
2287
|
+
preferences: {
|
|
2288
|
+
guidance: "hints",
|
|
2289
|
+
density: "standard",
|
|
2290
|
+
dataDepth: "detailed",
|
|
2291
|
+
control: "standard",
|
|
2292
|
+
media: "text",
|
|
2293
|
+
pace: "balanced",
|
|
2294
|
+
narrative: "top-down"
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// src/core/chat-service.ts
|
|
2300
|
+
import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
|
|
2301
|
+
var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
|
|
2302
|
+
|
|
2303
|
+
Your capabilities:
|
|
2304
|
+
- Help users create, modify, and understand ContractSpec specifications
|
|
2305
|
+
- Generate code that follows ContractSpec patterns and best practices
|
|
2306
|
+
- Explain concepts from the ContractSpec documentation
|
|
2307
|
+
- Suggest improvements and identify issues in specs and implementations
|
|
2308
|
+
|
|
2309
|
+
Guidelines:
|
|
2310
|
+
- Be concise but thorough
|
|
2311
|
+
- Provide code examples when helpful
|
|
2312
|
+
- Reference relevant ContractSpec concepts and patterns
|
|
2313
|
+
- Ask clarifying questions when the user's intent is unclear
|
|
2314
|
+
- When suggesting code changes, explain the rationale`;
|
|
2315
|
+
var WORKFLOW_TOOLS_PROMPT = `
|
|
2316
|
+
|
|
2317
|
+
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.`;
|
|
2318
|
+
|
|
2319
|
+
class ChatService {
|
|
2320
|
+
provider;
|
|
2321
|
+
context;
|
|
2322
|
+
store;
|
|
2323
|
+
systemPrompt;
|
|
2324
|
+
maxHistoryMessages;
|
|
2325
|
+
onUsage;
|
|
2326
|
+
tools;
|
|
2327
|
+
thinkingLevel;
|
|
2328
|
+
sendReasoning;
|
|
2329
|
+
sendSources;
|
|
2330
|
+
modelSelector;
|
|
2331
|
+
constructor(config) {
|
|
2332
|
+
this.provider = config.provider;
|
|
1211
2333
|
this.context = config.context;
|
|
1212
2334
|
this.store = config.store ?? new InMemoryConversationStore;
|
|
1213
|
-
this.systemPrompt = config
|
|
2335
|
+
this.systemPrompt = this.buildSystemPrompt(config);
|
|
1214
2336
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
1215
2337
|
this.onUsage = config.onUsage;
|
|
1216
|
-
this.tools = config
|
|
1217
|
-
this.
|
|
2338
|
+
this.tools = this.mergeTools(config);
|
|
2339
|
+
this.thinkingLevel = config.thinkingLevel;
|
|
2340
|
+
this.modelSelector = config.modelSelector;
|
|
2341
|
+
this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
|
|
1218
2342
|
this.sendSources = config.sendSources ?? false;
|
|
1219
2343
|
}
|
|
2344
|
+
buildSystemPrompt(config) {
|
|
2345
|
+
let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
2346
|
+
if (config.workflowToolsConfig?.baseWorkflows?.length) {
|
|
2347
|
+
base += WORKFLOW_TOOLS_PROMPT;
|
|
2348
|
+
}
|
|
2349
|
+
const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
|
|
2350
|
+
if (contractsPrompt) {
|
|
2351
|
+
base += contractsPrompt;
|
|
2352
|
+
}
|
|
2353
|
+
if (config.surfacePlanConfig?.plan) {
|
|
2354
|
+
const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
|
|
2355
|
+
base += `
|
|
2356
|
+
|
|
2357
|
+
` + compilePlannerPrompt(plannerInput);
|
|
2358
|
+
}
|
|
2359
|
+
return base;
|
|
2360
|
+
}
|
|
2361
|
+
mergeTools(config) {
|
|
2362
|
+
let merged = config.tools ?? {};
|
|
2363
|
+
const wfConfig = config.workflowToolsConfig;
|
|
2364
|
+
if (wfConfig?.baseWorkflows?.length) {
|
|
2365
|
+
const workflowTools = createWorkflowTools({
|
|
2366
|
+
baseWorkflows: wfConfig.baseWorkflows,
|
|
2367
|
+
composer: wfConfig.composer
|
|
2368
|
+
});
|
|
2369
|
+
merged = { ...merged, ...workflowTools };
|
|
2370
|
+
}
|
|
2371
|
+
const contractsCtx = config.contractsContext;
|
|
2372
|
+
if (contractsCtx?.agentSpecs?.length) {
|
|
2373
|
+
const allTools = [];
|
|
2374
|
+
for (const agent of contractsCtx.agentSpecs) {
|
|
2375
|
+
if (agent.tools?.length)
|
|
2376
|
+
allTools.push(...agent.tools);
|
|
2377
|
+
}
|
|
2378
|
+
if (allTools.length > 0) {
|
|
2379
|
+
const agentTools = agentToolConfigsToToolSet(allTools);
|
|
2380
|
+
merged = { ...merged, ...agentTools };
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
const surfaceConfig = config.surfacePlanConfig;
|
|
2384
|
+
if (surfaceConfig?.plan) {
|
|
2385
|
+
const plannerTools = createSurfacePlannerTools({
|
|
2386
|
+
plan: surfaceConfig.plan,
|
|
2387
|
+
onPatchProposal: surfaceConfig.onPatchProposal
|
|
2388
|
+
});
|
|
2389
|
+
merged = { ...merged, ...plannerTools };
|
|
2390
|
+
}
|
|
2391
|
+
if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
|
|
2392
|
+
merged = { ...merged, ...config.mcpTools };
|
|
2393
|
+
}
|
|
2394
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
2395
|
+
}
|
|
2396
|
+
async resolveModel() {
|
|
2397
|
+
if (this.modelSelector) {
|
|
2398
|
+
const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
|
|
2399
|
+
const { model, selection } = await this.modelSelector.selectAndCreate({
|
|
2400
|
+
taskDimension: dimension
|
|
2401
|
+
});
|
|
2402
|
+
return { model, providerName: selection.providerKey };
|
|
2403
|
+
}
|
|
2404
|
+
return {
|
|
2405
|
+
model: this.provider.getModel(),
|
|
2406
|
+
providerName: this.provider.name
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
thinkingLevelToDimension(level) {
|
|
2410
|
+
if (!level || level === "instant")
|
|
2411
|
+
return "latency";
|
|
2412
|
+
return "reasoning";
|
|
2413
|
+
}
|
|
1220
2414
|
async send(options) {
|
|
1221
2415
|
let conversation;
|
|
1222
2416
|
if (options.conversationId) {
|
|
@@ -1234,20 +2428,25 @@ class ChatService {
|
|
|
1234
2428
|
workspacePath: this.context?.workspacePath
|
|
1235
2429
|
});
|
|
1236
2430
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
2431
|
+
if (!options.skipUserAppend) {
|
|
2432
|
+
await this.store.appendMessage(conversation.id, {
|
|
2433
|
+
role: "user",
|
|
2434
|
+
content: options.content,
|
|
2435
|
+
status: "completed",
|
|
2436
|
+
attachments: options.attachments
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
1243
2440
|
const messages = this.buildMessages(conversation, options);
|
|
1244
|
-
const model = this.
|
|
2441
|
+
const { model, providerName } = await this.resolveModel();
|
|
2442
|
+
const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
1245
2443
|
try {
|
|
1246
2444
|
const result = await generateText({
|
|
1247
2445
|
model,
|
|
1248
2446
|
messages,
|
|
1249
2447
|
system: this.systemPrompt,
|
|
1250
|
-
tools: this.tools
|
|
2448
|
+
tools: this.tools,
|
|
2449
|
+
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
1251
2450
|
});
|
|
1252
2451
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
1253
2452
|
role: "assistant",
|
|
@@ -1292,23 +2491,27 @@ class ChatService {
|
|
|
1292
2491
|
workspacePath: this.context?.workspacePath
|
|
1293
2492
|
});
|
|
1294
2493
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
2494
|
+
if (!options.skipUserAppend) {
|
|
2495
|
+
await this.store.appendMessage(conversation.id, {
|
|
2496
|
+
role: "user",
|
|
2497
|
+
content: options.content,
|
|
2498
|
+
status: "completed",
|
|
2499
|
+
attachments: options.attachments
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
1301
2503
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
1302
2504
|
role: "assistant",
|
|
1303
2505
|
content: "",
|
|
1304
2506
|
status: "streaming"
|
|
1305
2507
|
});
|
|
1306
2508
|
const messages = this.buildMessages(conversation, options);
|
|
1307
|
-
const model = this.
|
|
2509
|
+
const { model, providerName } = await this.resolveModel();
|
|
1308
2510
|
const systemPrompt = this.systemPrompt;
|
|
1309
2511
|
const tools = this.tools;
|
|
1310
2512
|
const store = this.store;
|
|
1311
2513
|
const onUsage = this.onUsage;
|
|
2514
|
+
const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
1312
2515
|
async function* streamGenerator() {
|
|
1313
2516
|
let fullContent = "";
|
|
1314
2517
|
let fullReasoning = "";
|
|
@@ -1319,7 +2522,8 @@ class ChatService {
|
|
|
1319
2522
|
model,
|
|
1320
2523
|
messages,
|
|
1321
2524
|
system: systemPrompt,
|
|
1322
|
-
tools
|
|
2525
|
+
tools,
|
|
2526
|
+
providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
|
|
1323
2527
|
});
|
|
1324
2528
|
for await (const part of result.fullStream) {
|
|
1325
2529
|
if (part.type === "text-delta") {
|
|
@@ -1434,6 +2638,18 @@ class ChatService {
|
|
|
1434
2638
|
...options
|
|
1435
2639
|
});
|
|
1436
2640
|
}
|
|
2641
|
+
async updateConversation(conversationId, updates) {
|
|
2642
|
+
return this.store.update(conversationId, updates);
|
|
2643
|
+
}
|
|
2644
|
+
async forkConversation(conversationId, upToMessageId) {
|
|
2645
|
+
return this.store.fork(conversationId, upToMessageId);
|
|
2646
|
+
}
|
|
2647
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
2648
|
+
return this.store.updateMessage(conversationId, messageId, updates);
|
|
2649
|
+
}
|
|
2650
|
+
async truncateAfter(conversationId, messageId) {
|
|
2651
|
+
return this.store.truncateAfter(conversationId, messageId);
|
|
2652
|
+
}
|
|
1437
2653
|
async deleteConversation(conversationId) {
|
|
1438
2654
|
return this.store.delete(conversationId);
|
|
1439
2655
|
}
|
|
@@ -1504,9 +2720,9 @@ import {
|
|
|
1504
2720
|
function toolsToToolSet(defs) {
|
|
1505
2721
|
const result = {};
|
|
1506
2722
|
for (const def of defs) {
|
|
1507
|
-
result[def.name] =
|
|
2723
|
+
result[def.name] = tool4({
|
|
1508
2724
|
description: def.description ?? def.name,
|
|
1509
|
-
inputSchema:
|
|
2725
|
+
inputSchema: z4.object({}).passthrough(),
|
|
1510
2726
|
execute: async () => ({})
|
|
1511
2727
|
});
|
|
1512
2728
|
}
|
|
@@ -1520,22 +2736,64 @@ function useChat(options = {}) {
|
|
|
1520
2736
|
apiKey,
|
|
1521
2737
|
proxyUrl,
|
|
1522
2738
|
conversationId: initialConversationId,
|
|
2739
|
+
store,
|
|
1523
2740
|
systemPrompt,
|
|
1524
2741
|
streaming = true,
|
|
1525
2742
|
onSend,
|
|
1526
2743
|
onResponse,
|
|
1527
2744
|
onError,
|
|
1528
2745
|
onUsage,
|
|
1529
|
-
tools: toolsDefs
|
|
2746
|
+
tools: toolsDefs,
|
|
2747
|
+
thinkingLevel,
|
|
2748
|
+
workflowToolsConfig,
|
|
2749
|
+
modelSelector,
|
|
2750
|
+
contractsContext,
|
|
2751
|
+
surfacePlanConfig,
|
|
2752
|
+
mcpServers,
|
|
2753
|
+
agentMode
|
|
1530
2754
|
} = options;
|
|
1531
|
-
const [messages, setMessages] =
|
|
1532
|
-
const [
|
|
1533
|
-
const
|
|
1534
|
-
const [
|
|
1535
|
-
const [
|
|
1536
|
-
const
|
|
1537
|
-
const
|
|
1538
|
-
|
|
2755
|
+
const [messages, setMessages] = React11.useState([]);
|
|
2756
|
+
const [mcpTools, setMcpTools] = React11.useState(null);
|
|
2757
|
+
const mcpCleanupRef = React11.useRef(null);
|
|
2758
|
+
const [conversation, setConversation] = React11.useState(null);
|
|
2759
|
+
const [isLoading, setIsLoading] = React11.useState(false);
|
|
2760
|
+
const [error, setError] = React11.useState(null);
|
|
2761
|
+
const [conversationId, setConversationId] = React11.useState(initialConversationId ?? null);
|
|
2762
|
+
const abortControllerRef = React11.useRef(null);
|
|
2763
|
+
const chatServiceRef = React11.useRef(null);
|
|
2764
|
+
React11.useEffect(() => {
|
|
2765
|
+
if (!mcpServers?.length) {
|
|
2766
|
+
setMcpTools(null);
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
let cancelled = false;
|
|
2770
|
+
import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
|
|
2771
|
+
createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
|
|
2772
|
+
if (!cancelled) {
|
|
2773
|
+
setMcpTools(tools);
|
|
2774
|
+
mcpCleanupRef.current = cleanup;
|
|
2775
|
+
} else {
|
|
2776
|
+
cleanup().catch(() => {
|
|
2777
|
+
return;
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2780
|
+
}).catch(() => {
|
|
2781
|
+
if (!cancelled)
|
|
2782
|
+
setMcpTools(null);
|
|
2783
|
+
});
|
|
2784
|
+
});
|
|
2785
|
+
return () => {
|
|
2786
|
+
cancelled = true;
|
|
2787
|
+
const cleanup = mcpCleanupRef.current;
|
|
2788
|
+
mcpCleanupRef.current = null;
|
|
2789
|
+
if (cleanup)
|
|
2790
|
+
cleanup().catch(() => {
|
|
2791
|
+
return;
|
|
2792
|
+
});
|
|
2793
|
+
setMcpTools(null);
|
|
2794
|
+
};
|
|
2795
|
+
}, [mcpServers]);
|
|
2796
|
+
React11.useEffect(() => {
|
|
1539
2797
|
const chatProvider = createProvider({
|
|
1540
2798
|
provider,
|
|
1541
2799
|
model,
|
|
@@ -1544,9 +2802,16 @@ function useChat(options = {}) {
|
|
|
1544
2802
|
});
|
|
1545
2803
|
chatServiceRef.current = new ChatService({
|
|
1546
2804
|
provider: chatProvider,
|
|
2805
|
+
store,
|
|
1547
2806
|
systemPrompt,
|
|
1548
2807
|
onUsage,
|
|
1549
|
-
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
|
|
2808
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
|
|
2809
|
+
thinkingLevel,
|
|
2810
|
+
workflowToolsConfig,
|
|
2811
|
+
modelSelector,
|
|
2812
|
+
contractsContext,
|
|
2813
|
+
surfacePlanConfig,
|
|
2814
|
+
mcpTools
|
|
1550
2815
|
});
|
|
1551
2816
|
}, [
|
|
1552
2817
|
provider,
|
|
@@ -1554,11 +2819,18 @@ function useChat(options = {}) {
|
|
|
1554
2819
|
model,
|
|
1555
2820
|
apiKey,
|
|
1556
2821
|
proxyUrl,
|
|
2822
|
+
store,
|
|
1557
2823
|
systemPrompt,
|
|
1558
2824
|
onUsage,
|
|
1559
|
-
toolsDefs
|
|
2825
|
+
toolsDefs,
|
|
2826
|
+
thinkingLevel,
|
|
2827
|
+
workflowToolsConfig,
|
|
2828
|
+
modelSelector,
|
|
2829
|
+
contractsContext,
|
|
2830
|
+
surfacePlanConfig,
|
|
2831
|
+
mcpTools
|
|
1560
2832
|
]);
|
|
1561
|
-
|
|
2833
|
+
React11.useEffect(() => {
|
|
1562
2834
|
if (!conversationId || !chatServiceRef.current)
|
|
1563
2835
|
return;
|
|
1564
2836
|
const loadConversation = async () => {
|
|
@@ -1572,7 +2844,90 @@ function useChat(options = {}) {
|
|
|
1572
2844
|
};
|
|
1573
2845
|
loadConversation().catch(console.error);
|
|
1574
2846
|
}, [conversationId]);
|
|
1575
|
-
const sendMessage =
|
|
2847
|
+
const sendMessage = React11.useCallback(async (content, attachments, opts) => {
|
|
2848
|
+
if (agentMode?.agent) {
|
|
2849
|
+
setIsLoading(true);
|
|
2850
|
+
setError(null);
|
|
2851
|
+
abortControllerRef.current = new AbortController;
|
|
2852
|
+
try {
|
|
2853
|
+
if (!opts?.skipUserAppend) {
|
|
2854
|
+
const userMessage = {
|
|
2855
|
+
id: `msg_${Date.now()}`,
|
|
2856
|
+
conversationId: conversationId ?? "",
|
|
2857
|
+
role: "user",
|
|
2858
|
+
content,
|
|
2859
|
+
status: "completed",
|
|
2860
|
+
createdAt: new Date,
|
|
2861
|
+
updatedAt: new Date,
|
|
2862
|
+
attachments
|
|
2863
|
+
};
|
|
2864
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
2865
|
+
onSend?.(userMessage);
|
|
2866
|
+
}
|
|
2867
|
+
const result = await agentMode.agent.generate({
|
|
2868
|
+
prompt: content,
|
|
2869
|
+
signal: abortControllerRef.current.signal
|
|
2870
|
+
});
|
|
2871
|
+
const toolCallsMap = new Map;
|
|
2872
|
+
for (const tc of result.toolCalls ?? []) {
|
|
2873
|
+
const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
|
|
2874
|
+
toolCallsMap.set(tc.toolCallId, {
|
|
2875
|
+
id: tc.toolCallId,
|
|
2876
|
+
name: tc.toolName,
|
|
2877
|
+
args: tc.args ?? {},
|
|
2878
|
+
result: tr?.output,
|
|
2879
|
+
status: "completed"
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
const assistantMessage = {
|
|
2883
|
+
id: `msg_${Date.now()}_a`,
|
|
2884
|
+
conversationId: conversationId ?? "",
|
|
2885
|
+
role: "assistant",
|
|
2886
|
+
content: result.text,
|
|
2887
|
+
status: "completed",
|
|
2888
|
+
createdAt: new Date,
|
|
2889
|
+
updatedAt: new Date,
|
|
2890
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
2891
|
+
usage: result.usage
|
|
2892
|
+
};
|
|
2893
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
2894
|
+
onResponse?.(assistantMessage);
|
|
2895
|
+
onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
|
|
2896
|
+
if (store && !conversationId) {
|
|
2897
|
+
const conv = await store.create({
|
|
2898
|
+
status: "active",
|
|
2899
|
+
provider: "agent",
|
|
2900
|
+
model: "agent",
|
|
2901
|
+
messages: []
|
|
2902
|
+
});
|
|
2903
|
+
if (!opts?.skipUserAppend) {
|
|
2904
|
+
await store.appendMessage(conv.id, {
|
|
2905
|
+
role: "user",
|
|
2906
|
+
content,
|
|
2907
|
+
status: "completed",
|
|
2908
|
+
attachments
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
await store.appendMessage(conv.id, {
|
|
2912
|
+
role: "assistant",
|
|
2913
|
+
content: result.text,
|
|
2914
|
+
status: "completed",
|
|
2915
|
+
toolCalls: assistantMessage.toolCalls,
|
|
2916
|
+
usage: result.usage
|
|
2917
|
+
});
|
|
2918
|
+
const updated = await store.get(conv.id);
|
|
2919
|
+
if (updated)
|
|
2920
|
+
setConversation(updated);
|
|
2921
|
+
setConversationId(conv.id);
|
|
2922
|
+
}
|
|
2923
|
+
} catch (err) {
|
|
2924
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2925
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
2926
|
+
} finally {
|
|
2927
|
+
setIsLoading(false);
|
|
2928
|
+
}
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
1576
2931
|
if (!chatServiceRef.current) {
|
|
1577
2932
|
throw new Error("Chat service not initialized");
|
|
1578
2933
|
}
|
|
@@ -1580,25 +2935,28 @@ function useChat(options = {}) {
|
|
|
1580
2935
|
setError(null);
|
|
1581
2936
|
abortControllerRef.current = new AbortController;
|
|
1582
2937
|
try {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
2938
|
+
if (!opts?.skipUserAppend) {
|
|
2939
|
+
const userMessage = {
|
|
2940
|
+
id: `msg_${Date.now()}`,
|
|
2941
|
+
conversationId: conversationId ?? "",
|
|
2942
|
+
role: "user",
|
|
2943
|
+
content,
|
|
2944
|
+
status: "completed",
|
|
2945
|
+
createdAt: new Date,
|
|
2946
|
+
updatedAt: new Date,
|
|
2947
|
+
attachments
|
|
2948
|
+
};
|
|
2949
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
2950
|
+
onSend?.(userMessage);
|
|
2951
|
+
}
|
|
1595
2952
|
if (streaming) {
|
|
1596
2953
|
const result = await chatServiceRef.current.stream({
|
|
1597
2954
|
conversationId: conversationId ?? undefined,
|
|
1598
2955
|
content,
|
|
1599
|
-
attachments
|
|
2956
|
+
attachments,
|
|
2957
|
+
skipUserAppend: opts?.skipUserAppend
|
|
1600
2958
|
});
|
|
1601
|
-
if (!conversationId) {
|
|
2959
|
+
if (!conversationId && !opts?.skipUserAppend) {
|
|
1602
2960
|
setConversationId(result.conversationId);
|
|
1603
2961
|
}
|
|
1604
2962
|
const assistantMessage = {
|
|
@@ -1679,7 +3037,8 @@ function useChat(options = {}) {
|
|
|
1679
3037
|
const result = await chatServiceRef.current.send({
|
|
1680
3038
|
conversationId: conversationId ?? undefined,
|
|
1681
3039
|
content,
|
|
1682
|
-
attachments
|
|
3040
|
+
attachments,
|
|
3041
|
+
skipUserAppend: opts?.skipUserAppend
|
|
1683
3042
|
});
|
|
1684
3043
|
setConversation(result.conversation);
|
|
1685
3044
|
setMessages(result.conversation.messages);
|
|
@@ -1696,14 +3055,14 @@ function useChat(options = {}) {
|
|
|
1696
3055
|
setIsLoading(false);
|
|
1697
3056
|
abortControllerRef.current = null;
|
|
1698
3057
|
}
|
|
1699
|
-
}, [conversationId, streaming, onSend, onResponse, onError, messages]);
|
|
1700
|
-
const clearConversation =
|
|
3058
|
+
}, [conversationId, streaming, onSend, onResponse, onError, onUsage, messages, agentMode, store]);
|
|
3059
|
+
const clearConversation = React11.useCallback(() => {
|
|
1701
3060
|
setMessages([]);
|
|
1702
3061
|
setConversation(null);
|
|
1703
3062
|
setConversationId(null);
|
|
1704
3063
|
setError(null);
|
|
1705
3064
|
}, []);
|
|
1706
|
-
const regenerate =
|
|
3065
|
+
const regenerate = React11.useCallback(async () => {
|
|
1707
3066
|
const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
|
|
1708
3067
|
if (lastUserMessageIndex === -1)
|
|
1709
3068
|
return;
|
|
@@ -1713,11 +3072,49 @@ function useChat(options = {}) {
|
|
|
1713
3072
|
setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
|
|
1714
3073
|
await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
|
|
1715
3074
|
}, [messages, sendMessage]);
|
|
1716
|
-
const stop =
|
|
3075
|
+
const stop = React11.useCallback(() => {
|
|
1717
3076
|
abortControllerRef.current?.abort();
|
|
1718
3077
|
setIsLoading(false);
|
|
1719
3078
|
}, []);
|
|
1720
|
-
const
|
|
3079
|
+
const createNewConversation = clearConversation;
|
|
3080
|
+
const editMessage = React11.useCallback(async (messageId, newContent) => {
|
|
3081
|
+
if (!chatServiceRef.current || !conversationId)
|
|
3082
|
+
return;
|
|
3083
|
+
const msg = messages.find((m) => m.id === messageId);
|
|
3084
|
+
if (!msg || msg.role !== "user")
|
|
3085
|
+
return;
|
|
3086
|
+
await chatServiceRef.current.updateMessage(conversationId, messageId, { content: newContent });
|
|
3087
|
+
const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
|
|
3088
|
+
if (truncated) {
|
|
3089
|
+
setMessages(truncated.messages);
|
|
3090
|
+
}
|
|
3091
|
+
await sendMessage(newContent, undefined, { skipUserAppend: true });
|
|
3092
|
+
}, [conversationId, messages, sendMessage]);
|
|
3093
|
+
const forkConversation = React11.useCallback(async (upToMessageId) => {
|
|
3094
|
+
if (!chatServiceRef.current)
|
|
3095
|
+
return null;
|
|
3096
|
+
const idToFork = conversationId ?? conversation?.id;
|
|
3097
|
+
if (!idToFork)
|
|
3098
|
+
return null;
|
|
3099
|
+
try {
|
|
3100
|
+
const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
|
|
3101
|
+
setConversationId(forked.id);
|
|
3102
|
+
setConversation(forked);
|
|
3103
|
+
setMessages(forked.messages);
|
|
3104
|
+
return forked.id;
|
|
3105
|
+
} catch {
|
|
3106
|
+
return null;
|
|
3107
|
+
}
|
|
3108
|
+
}, [conversationId, conversation]);
|
|
3109
|
+
const updateConversationFn = React11.useCallback(async (updates) => {
|
|
3110
|
+
if (!chatServiceRef.current || !conversationId)
|
|
3111
|
+
return null;
|
|
3112
|
+
const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
|
|
3113
|
+
if (updated)
|
|
3114
|
+
setConversation(updated);
|
|
3115
|
+
return updated;
|
|
3116
|
+
}, [conversationId]);
|
|
3117
|
+
const addToolApprovalResponse = React11.useCallback((_toolCallId, _result) => {
|
|
1721
3118
|
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
1722
3119
|
}, []);
|
|
1723
3120
|
const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
|
|
@@ -1731,40 +3128,751 @@ function useChat(options = {}) {
|
|
|
1731
3128
|
setConversationId,
|
|
1732
3129
|
regenerate,
|
|
1733
3130
|
stop,
|
|
3131
|
+
createNewConversation,
|
|
3132
|
+
editMessage,
|
|
3133
|
+
forkConversation,
|
|
3134
|
+
updateConversation: updateConversationFn,
|
|
1734
3135
|
...hasApprovalTools && { addToolApprovalResponse }
|
|
1735
3136
|
};
|
|
1736
3137
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
3138
|
+
|
|
3139
|
+
// src/core/local-storage-conversation-store.ts
|
|
3140
|
+
var DEFAULT_KEY = "contractspec:ai-chat:conversations";
|
|
3141
|
+
function generateId2(prefix) {
|
|
3142
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
3143
|
+
}
|
|
3144
|
+
function toSerializable(conv) {
|
|
3145
|
+
return {
|
|
3146
|
+
...conv,
|
|
3147
|
+
createdAt: conv.createdAt.toISOString(),
|
|
3148
|
+
updatedAt: conv.updatedAt.toISOString(),
|
|
3149
|
+
messages: conv.messages.map((m) => ({
|
|
3150
|
+
...m,
|
|
3151
|
+
createdAt: m.createdAt.toISOString(),
|
|
3152
|
+
updatedAt: m.updatedAt.toISOString()
|
|
3153
|
+
}))
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
function fromSerializable(raw) {
|
|
3157
|
+
const messages = raw.messages?.map((m) => ({
|
|
3158
|
+
...m,
|
|
3159
|
+
createdAt: new Date(m.createdAt),
|
|
3160
|
+
updatedAt: new Date(m.updatedAt)
|
|
3161
|
+
})) ?? [];
|
|
3162
|
+
return {
|
|
3163
|
+
...raw,
|
|
3164
|
+
createdAt: new Date(raw.createdAt),
|
|
3165
|
+
updatedAt: new Date(raw.updatedAt),
|
|
3166
|
+
messages
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
function loadAll(key) {
|
|
3170
|
+
if (typeof window === "undefined")
|
|
3171
|
+
return new Map;
|
|
3172
|
+
try {
|
|
3173
|
+
const raw = window.localStorage.getItem(key);
|
|
3174
|
+
if (!raw)
|
|
3175
|
+
return new Map;
|
|
3176
|
+
const arr = JSON.parse(raw);
|
|
3177
|
+
const map = new Map;
|
|
3178
|
+
for (const item of arr) {
|
|
3179
|
+
const conv = fromSerializable(item);
|
|
3180
|
+
map.set(conv.id, conv);
|
|
1760
3181
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
3182
|
+
return map;
|
|
3183
|
+
} catch {
|
|
3184
|
+
return new Map;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
function saveAll(key, map) {
|
|
3188
|
+
if (typeof window === "undefined")
|
|
3189
|
+
return;
|
|
3190
|
+
try {
|
|
3191
|
+
const arr = Array.from(map.values()).map(toSerializable);
|
|
3192
|
+
window.localStorage.setItem(key, JSON.stringify(arr));
|
|
3193
|
+
} catch {}
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
class LocalStorageConversationStore {
|
|
3197
|
+
key;
|
|
3198
|
+
cache = null;
|
|
3199
|
+
constructor(storageKey = DEFAULT_KEY) {
|
|
3200
|
+
this.key = storageKey;
|
|
3201
|
+
}
|
|
3202
|
+
getMap() {
|
|
3203
|
+
if (!this.cache) {
|
|
3204
|
+
this.cache = loadAll(this.key);
|
|
3205
|
+
}
|
|
3206
|
+
return this.cache;
|
|
3207
|
+
}
|
|
3208
|
+
persist() {
|
|
3209
|
+
saveAll(this.key, this.getMap());
|
|
3210
|
+
}
|
|
3211
|
+
async get(conversationId) {
|
|
3212
|
+
return this.getMap().get(conversationId) ?? null;
|
|
3213
|
+
}
|
|
3214
|
+
async create(conversation) {
|
|
3215
|
+
const now = new Date;
|
|
3216
|
+
const full = {
|
|
3217
|
+
...conversation,
|
|
3218
|
+
id: generateId2("conv"),
|
|
3219
|
+
createdAt: now,
|
|
3220
|
+
updatedAt: now
|
|
3221
|
+
};
|
|
3222
|
+
this.getMap().set(full.id, full);
|
|
3223
|
+
this.persist();
|
|
3224
|
+
return full;
|
|
3225
|
+
}
|
|
3226
|
+
async update(conversationId, updates) {
|
|
3227
|
+
const conv = this.getMap().get(conversationId);
|
|
3228
|
+
if (!conv)
|
|
3229
|
+
return null;
|
|
3230
|
+
const updated = {
|
|
3231
|
+
...conv,
|
|
3232
|
+
...updates,
|
|
3233
|
+
updatedAt: new Date
|
|
3234
|
+
};
|
|
3235
|
+
this.getMap().set(conversationId, updated);
|
|
3236
|
+
this.persist();
|
|
3237
|
+
return updated;
|
|
3238
|
+
}
|
|
3239
|
+
async appendMessage(conversationId, message) {
|
|
3240
|
+
const conv = this.getMap().get(conversationId);
|
|
3241
|
+
if (!conv)
|
|
3242
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
3243
|
+
const now = new Date;
|
|
3244
|
+
const fullMessage = {
|
|
3245
|
+
...message,
|
|
3246
|
+
id: generateId2("msg"),
|
|
3247
|
+
conversationId,
|
|
3248
|
+
createdAt: now,
|
|
3249
|
+
updatedAt: now
|
|
3250
|
+
};
|
|
3251
|
+
conv.messages.push(fullMessage);
|
|
3252
|
+
conv.updatedAt = now;
|
|
3253
|
+
this.persist();
|
|
3254
|
+
return fullMessage;
|
|
3255
|
+
}
|
|
3256
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
3257
|
+
const conv = this.getMap().get(conversationId);
|
|
3258
|
+
if (!conv)
|
|
3259
|
+
return null;
|
|
3260
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
3261
|
+
if (idx === -1)
|
|
3262
|
+
return null;
|
|
3263
|
+
const msg = conv.messages[idx];
|
|
3264
|
+
if (!msg)
|
|
3265
|
+
return null;
|
|
3266
|
+
const updated = {
|
|
3267
|
+
...msg,
|
|
3268
|
+
...updates,
|
|
3269
|
+
updatedAt: new Date
|
|
3270
|
+
};
|
|
3271
|
+
conv.messages[idx] = updated;
|
|
3272
|
+
conv.updatedAt = new Date;
|
|
3273
|
+
this.persist();
|
|
3274
|
+
return updated;
|
|
3275
|
+
}
|
|
3276
|
+
async delete(conversationId) {
|
|
3277
|
+
const deleted = this.getMap().delete(conversationId);
|
|
3278
|
+
if (deleted)
|
|
3279
|
+
this.persist();
|
|
3280
|
+
return deleted;
|
|
3281
|
+
}
|
|
3282
|
+
async list(options) {
|
|
3283
|
+
let results = Array.from(this.getMap().values());
|
|
3284
|
+
if (options?.status) {
|
|
3285
|
+
results = results.filter((c) => c.status === options.status);
|
|
3286
|
+
}
|
|
3287
|
+
if (options?.projectId) {
|
|
3288
|
+
results = results.filter((c) => c.projectId === options.projectId);
|
|
3289
|
+
}
|
|
3290
|
+
if (options?.tags && options.tags.length > 0) {
|
|
3291
|
+
const tagSet = new Set(options.tags);
|
|
3292
|
+
results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
|
|
3293
|
+
}
|
|
3294
|
+
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
3295
|
+
const offset = options?.offset ?? 0;
|
|
3296
|
+
const limit = options?.limit ?? 100;
|
|
3297
|
+
return results.slice(offset, offset + limit);
|
|
3298
|
+
}
|
|
3299
|
+
async fork(conversationId, upToMessageId) {
|
|
3300
|
+
const source = this.getMap().get(conversationId);
|
|
3301
|
+
if (!source)
|
|
3302
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
3303
|
+
let messagesToCopy = source.messages;
|
|
3304
|
+
if (upToMessageId) {
|
|
3305
|
+
const idx = source.messages.findIndex((m) => m.id === upToMessageId);
|
|
3306
|
+
if (idx === -1)
|
|
3307
|
+
throw new Error(`Message ${upToMessageId} not found`);
|
|
3308
|
+
messagesToCopy = source.messages.slice(0, idx + 1);
|
|
3309
|
+
}
|
|
3310
|
+
const now = new Date;
|
|
3311
|
+
const forkedMessages = messagesToCopy.map((m) => ({
|
|
3312
|
+
...m,
|
|
3313
|
+
id: generateId2("msg"),
|
|
3314
|
+
conversationId: "",
|
|
3315
|
+
createdAt: new Date(m.createdAt),
|
|
3316
|
+
updatedAt: new Date(m.updatedAt)
|
|
3317
|
+
}));
|
|
3318
|
+
const forked = {
|
|
3319
|
+
...source,
|
|
3320
|
+
id: generateId2("conv"),
|
|
3321
|
+
title: source.title ? `${source.title} (fork)` : undefined,
|
|
3322
|
+
forkedFromId: source.id,
|
|
3323
|
+
createdAt: now,
|
|
3324
|
+
updatedAt: now,
|
|
3325
|
+
messages: forkedMessages
|
|
3326
|
+
};
|
|
3327
|
+
for (const m of forked.messages) {
|
|
3328
|
+
m.conversationId = forked.id;
|
|
3329
|
+
}
|
|
3330
|
+
this.getMap().set(forked.id, forked);
|
|
3331
|
+
this.persist();
|
|
3332
|
+
return forked;
|
|
3333
|
+
}
|
|
3334
|
+
async truncateAfter(conversationId, messageId) {
|
|
3335
|
+
const conv = this.getMap().get(conversationId);
|
|
3336
|
+
if (!conv)
|
|
3337
|
+
return null;
|
|
3338
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
3339
|
+
if (idx === -1)
|
|
3340
|
+
return null;
|
|
3341
|
+
conv.messages = conv.messages.slice(0, idx + 1);
|
|
3342
|
+
conv.updatedAt = new Date;
|
|
3343
|
+
this.persist();
|
|
3344
|
+
return conv;
|
|
3345
|
+
}
|
|
3346
|
+
async search(query, limit = 20) {
|
|
3347
|
+
const lowerQuery = query.toLowerCase();
|
|
3348
|
+
const results = [];
|
|
3349
|
+
for (const conv of this.getMap().values()) {
|
|
3350
|
+
if (conv.title?.toLowerCase().includes(lowerQuery)) {
|
|
3351
|
+
results.push(conv);
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
if (conv.messages.some((m) => m.content.toLowerCase().includes(lowerQuery))) {
|
|
3355
|
+
results.push(conv);
|
|
3356
|
+
}
|
|
3357
|
+
if (results.length >= limit)
|
|
3358
|
+
break;
|
|
3359
|
+
}
|
|
3360
|
+
return results;
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
function createLocalStorageConversationStore(storageKey) {
|
|
3364
|
+
return new LocalStorageConversationStore(storageKey);
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
// src/presentation/components/ChatWithSidebar.tsx
|
|
3368
|
+
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3369
|
+
"use client";
|
|
3370
|
+
var defaultStore = createLocalStorageConversationStore();
|
|
3371
|
+
function ChatWithSidebar({
|
|
3372
|
+
store = defaultStore,
|
|
3373
|
+
projectId,
|
|
3374
|
+
tags,
|
|
3375
|
+
className,
|
|
3376
|
+
thinkingLevel: initialThinkingLevel = "thinking",
|
|
3377
|
+
presentationRenderer,
|
|
3378
|
+
formRenderer,
|
|
3379
|
+
...useChatOptions
|
|
3380
|
+
}) {
|
|
3381
|
+
const effectiveStore = store;
|
|
3382
|
+
const [thinkingLevel, setThinkingLevel] = React12.useState(initialThinkingLevel);
|
|
3383
|
+
const chat = useChat({
|
|
3384
|
+
...useChatOptions,
|
|
3385
|
+
store: effectiveStore,
|
|
3386
|
+
thinkingLevel
|
|
3387
|
+
});
|
|
3388
|
+
const {
|
|
3389
|
+
messages,
|
|
3390
|
+
conversation,
|
|
3391
|
+
sendMessage,
|
|
3392
|
+
isLoading,
|
|
3393
|
+
setConversationId,
|
|
3394
|
+
createNewConversation,
|
|
3395
|
+
editMessage,
|
|
3396
|
+
forkConversation,
|
|
3397
|
+
updateConversation
|
|
3398
|
+
} = chat;
|
|
3399
|
+
const selectedConversationId = conversation?.id ?? null;
|
|
3400
|
+
const handleSelectConversation = React12.useCallback((id) => {
|
|
3401
|
+
setConversationId(id);
|
|
3402
|
+
}, [setConversationId]);
|
|
3403
|
+
return /* @__PURE__ */ jsxs10("div", {
|
|
3404
|
+
className: className ?? "flex h-full w-full",
|
|
3405
|
+
children: [
|
|
3406
|
+
/* @__PURE__ */ jsx10(ChatSidebar, {
|
|
3407
|
+
store: effectiveStore,
|
|
3408
|
+
selectedConversationId,
|
|
3409
|
+
onSelectConversation: handleSelectConversation,
|
|
3410
|
+
onCreateNew: createNewConversation,
|
|
3411
|
+
projectId,
|
|
3412
|
+
tags,
|
|
3413
|
+
selectedConversation: conversation,
|
|
3414
|
+
onUpdateConversation: updateConversation ? async (id, updates) => {
|
|
3415
|
+
if (id === selectedConversationId) {
|
|
3416
|
+
await updateConversation(updates);
|
|
3417
|
+
}
|
|
3418
|
+
} : undefined
|
|
3419
|
+
}),
|
|
3420
|
+
/* @__PURE__ */ jsx10("div", {
|
|
3421
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
3422
|
+
children: /* @__PURE__ */ jsx10(ChatWithExport, {
|
|
3423
|
+
messages,
|
|
3424
|
+
conversation,
|
|
3425
|
+
onCreateNew: createNewConversation,
|
|
3426
|
+
onFork: forkConversation,
|
|
3427
|
+
onEditMessage: editMessage,
|
|
3428
|
+
thinkingLevel,
|
|
3429
|
+
onThinkingLevelChange: setThinkingLevel,
|
|
3430
|
+
presentationRenderer,
|
|
3431
|
+
formRenderer,
|
|
3432
|
+
children: /* @__PURE__ */ jsx10(ChatInput, {
|
|
3433
|
+
onSend: (content, att) => sendMessage(content, att),
|
|
3434
|
+
disabled: isLoading,
|
|
3435
|
+
isLoading
|
|
3436
|
+
})
|
|
3437
|
+
})
|
|
3438
|
+
})
|
|
3439
|
+
]
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
// src/presentation/components/ModelPicker.tsx
|
|
3443
|
+
import * as React13 from "react";
|
|
3444
|
+
import { cn as cn7 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
3445
|
+
import { Button as Button6 } from "@contractspec/lib.design-system";
|
|
3446
|
+
import {
|
|
3447
|
+
Select as Select2,
|
|
3448
|
+
SelectContent as SelectContent2,
|
|
3449
|
+
SelectItem as SelectItem2,
|
|
3450
|
+
SelectTrigger as SelectTrigger2,
|
|
3451
|
+
SelectValue as SelectValue2
|
|
3452
|
+
} from "@contractspec/lib.ui-kit-web/ui/select";
|
|
3453
|
+
import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
3454
|
+
import { Label as Label2 } from "@contractspec/lib.ui-kit-web/ui/label";
|
|
3455
|
+
import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
|
|
3456
|
+
import {
|
|
3457
|
+
getModelsForProvider
|
|
3458
|
+
} from "@contractspec/lib.ai-providers";
|
|
3459
|
+
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3460
|
+
"use client";
|
|
3461
|
+
var PROVIDER_ICONS = {
|
|
3462
|
+
ollama: /* @__PURE__ */ jsx11(Cpu, {
|
|
3463
|
+
className: "h-4 w-4"
|
|
3464
|
+
}),
|
|
3465
|
+
openai: /* @__PURE__ */ jsx11(Bot2, {
|
|
3466
|
+
className: "h-4 w-4"
|
|
3467
|
+
}),
|
|
3468
|
+
anthropic: /* @__PURE__ */ jsx11(Sparkles, {
|
|
3469
|
+
className: "h-4 w-4"
|
|
3470
|
+
}),
|
|
3471
|
+
mistral: /* @__PURE__ */ jsx11(Cloud, {
|
|
3472
|
+
className: "h-4 w-4"
|
|
3473
|
+
}),
|
|
3474
|
+
gemini: /* @__PURE__ */ jsx11(Sparkles, {
|
|
3475
|
+
className: "h-4 w-4"
|
|
3476
|
+
})
|
|
3477
|
+
};
|
|
3478
|
+
var PROVIDER_NAMES = {
|
|
3479
|
+
ollama: "Ollama (Local)",
|
|
3480
|
+
openai: "OpenAI",
|
|
3481
|
+
anthropic: "Anthropic",
|
|
3482
|
+
mistral: "Mistral",
|
|
3483
|
+
gemini: "Google Gemini"
|
|
3484
|
+
};
|
|
3485
|
+
var MODE_BADGES = {
|
|
3486
|
+
local: { label: "Local", variant: "secondary" },
|
|
3487
|
+
byok: { label: "BYOK", variant: "outline" },
|
|
3488
|
+
managed: { label: "Managed", variant: "default" }
|
|
3489
|
+
};
|
|
3490
|
+
function ModelPicker({
|
|
3491
|
+
value,
|
|
3492
|
+
onChange,
|
|
3493
|
+
availableProviders,
|
|
3494
|
+
className,
|
|
3495
|
+
compact = false
|
|
3496
|
+
}) {
|
|
3497
|
+
const providers = availableProviders ?? [
|
|
3498
|
+
{ provider: "ollama", available: true, mode: "local" },
|
|
3499
|
+
{ provider: "openai", available: true, mode: "byok" },
|
|
3500
|
+
{ provider: "anthropic", available: true, mode: "byok" },
|
|
3501
|
+
{ provider: "mistral", available: true, mode: "byok" },
|
|
3502
|
+
{ provider: "gemini", available: true, mode: "byok" }
|
|
3503
|
+
];
|
|
3504
|
+
const models = getModelsForProvider(value.provider);
|
|
3505
|
+
const selectedModel = models.find((m) => m.id === value.model);
|
|
3506
|
+
const handleProviderChange = React13.useCallback((providerName) => {
|
|
3507
|
+
const provider = providerName;
|
|
3508
|
+
const providerInfo = providers.find((p) => p.provider === provider);
|
|
3509
|
+
const providerModels = getModelsForProvider(provider);
|
|
3510
|
+
const defaultModel = providerModels[0]?.id ?? "";
|
|
3511
|
+
onChange({
|
|
3512
|
+
provider,
|
|
3513
|
+
model: defaultModel,
|
|
3514
|
+
mode: providerInfo?.mode ?? "byok"
|
|
3515
|
+
});
|
|
3516
|
+
}, [onChange, providers]);
|
|
3517
|
+
const handleModelChange = React13.useCallback((modelId) => {
|
|
3518
|
+
onChange({
|
|
3519
|
+
...value,
|
|
3520
|
+
model: modelId
|
|
3521
|
+
});
|
|
3522
|
+
}, [onChange, value]);
|
|
3523
|
+
if (compact) {
|
|
3524
|
+
return /* @__PURE__ */ jsxs11("div", {
|
|
3525
|
+
className: cn7("flex items-center gap-2", className),
|
|
3526
|
+
children: [
|
|
3527
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
3528
|
+
value: value.provider,
|
|
3529
|
+
onValueChange: handleProviderChange,
|
|
3530
|
+
children: [
|
|
3531
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
3532
|
+
className: "w-[140px]",
|
|
3533
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {})
|
|
3534
|
+
}),
|
|
3535
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3536
|
+
children: providers.map((p) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
3537
|
+
value: p.provider,
|
|
3538
|
+
disabled: !p.available,
|
|
3539
|
+
children: /* @__PURE__ */ jsxs11("div", {
|
|
3540
|
+
className: "flex items-center gap-2",
|
|
3541
|
+
children: [
|
|
3542
|
+
PROVIDER_ICONS[p.provider],
|
|
3543
|
+
/* @__PURE__ */ jsx11("span", {
|
|
3544
|
+
children: PROVIDER_NAMES[p.provider]
|
|
3545
|
+
})
|
|
3546
|
+
]
|
|
3547
|
+
})
|
|
3548
|
+
}, p.provider))
|
|
3549
|
+
})
|
|
3550
|
+
]
|
|
3551
|
+
}),
|
|
3552
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
3553
|
+
value: value.model,
|
|
3554
|
+
onValueChange: handleModelChange,
|
|
3555
|
+
children: [
|
|
3556
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
3557
|
+
className: "w-[160px]",
|
|
3558
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {})
|
|
3559
|
+
}),
|
|
3560
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3561
|
+
children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
3562
|
+
value: m.id,
|
|
3563
|
+
children: m.name
|
|
3564
|
+
}, m.id))
|
|
3565
|
+
})
|
|
3566
|
+
]
|
|
3567
|
+
})
|
|
3568
|
+
]
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
3571
|
+
return /* @__PURE__ */ jsxs11("div", {
|
|
3572
|
+
className: cn7("flex flex-col gap-3", className),
|
|
3573
|
+
children: [
|
|
3574
|
+
/* @__PURE__ */ jsxs11("div", {
|
|
3575
|
+
className: "flex flex-col gap-1.5",
|
|
3576
|
+
children: [
|
|
3577
|
+
/* @__PURE__ */ jsx11(Label2, {
|
|
3578
|
+
htmlFor: "provider-selection",
|
|
3579
|
+
className: "text-sm font-medium",
|
|
3580
|
+
children: "Provider"
|
|
3581
|
+
}),
|
|
3582
|
+
/* @__PURE__ */ jsx11("div", {
|
|
3583
|
+
className: "flex flex-wrap gap-2",
|
|
3584
|
+
id: "provider-selection",
|
|
3585
|
+
children: providers.map((p) => /* @__PURE__ */ jsxs11(Button6, {
|
|
3586
|
+
variant: value.provider === p.provider ? "default" : "outline",
|
|
3587
|
+
size: "sm",
|
|
3588
|
+
onPress: () => p.available && handleProviderChange(p.provider),
|
|
3589
|
+
disabled: !p.available,
|
|
3590
|
+
className: cn7(!p.available && "opacity-50"),
|
|
3591
|
+
children: [
|
|
3592
|
+
PROVIDER_ICONS[p.provider],
|
|
3593
|
+
/* @__PURE__ */ jsx11("span", {
|
|
3594
|
+
children: PROVIDER_NAMES[p.provider]
|
|
3595
|
+
}),
|
|
3596
|
+
/* @__PURE__ */ jsx11(Badge, {
|
|
3597
|
+
variant: MODE_BADGES[p.mode].variant,
|
|
3598
|
+
className: "ml-1",
|
|
3599
|
+
children: MODE_BADGES[p.mode].label
|
|
3600
|
+
})
|
|
3601
|
+
]
|
|
3602
|
+
}, p.provider))
|
|
3603
|
+
})
|
|
3604
|
+
]
|
|
3605
|
+
}),
|
|
3606
|
+
/* @__PURE__ */ jsxs11("div", {
|
|
3607
|
+
className: "flex flex-col gap-1.5",
|
|
3608
|
+
children: [
|
|
3609
|
+
/* @__PURE__ */ jsx11(Label2, {
|
|
3610
|
+
htmlFor: "model-picker",
|
|
3611
|
+
className: "text-sm font-medium",
|
|
3612
|
+
children: "Model"
|
|
3613
|
+
}),
|
|
3614
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
3615
|
+
name: "model-picker",
|
|
3616
|
+
value: value.model,
|
|
3617
|
+
onValueChange: handleModelChange,
|
|
3618
|
+
children: [
|
|
3619
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
3620
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {
|
|
3621
|
+
placeholder: "Select a model"
|
|
3622
|
+
})
|
|
3623
|
+
}),
|
|
3624
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3625
|
+
children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
3626
|
+
value: m.id,
|
|
3627
|
+
children: /* @__PURE__ */ jsxs11("div", {
|
|
3628
|
+
className: "flex items-center gap-2",
|
|
3629
|
+
children: [
|
|
3630
|
+
/* @__PURE__ */ jsx11("span", {
|
|
3631
|
+
children: m.name
|
|
3632
|
+
}),
|
|
3633
|
+
/* @__PURE__ */ jsxs11("span", {
|
|
3634
|
+
className: "text-muted-foreground text-xs",
|
|
3635
|
+
children: [
|
|
3636
|
+
Math.round(m.contextWindow / 1000),
|
|
3637
|
+
"K"
|
|
3638
|
+
]
|
|
3639
|
+
}),
|
|
3640
|
+
m.capabilities.vision && /* @__PURE__ */ jsx11(Badge, {
|
|
3641
|
+
variant: "outline",
|
|
3642
|
+
className: "text-xs",
|
|
3643
|
+
children: "Vision"
|
|
3644
|
+
}),
|
|
3645
|
+
m.capabilities.reasoning && /* @__PURE__ */ jsx11(Badge, {
|
|
3646
|
+
variant: "outline",
|
|
3647
|
+
className: "text-xs",
|
|
3648
|
+
children: "Reasoning"
|
|
3649
|
+
})
|
|
3650
|
+
]
|
|
3651
|
+
})
|
|
3652
|
+
}, m.id))
|
|
3653
|
+
})
|
|
3654
|
+
]
|
|
3655
|
+
})
|
|
3656
|
+
]
|
|
3657
|
+
}),
|
|
3658
|
+
selectedModel && /* @__PURE__ */ jsxs11("div", {
|
|
3659
|
+
className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
|
|
3660
|
+
children: [
|
|
3661
|
+
/* @__PURE__ */ jsxs11("span", {
|
|
3662
|
+
children: [
|
|
3663
|
+
"Context: ",
|
|
3664
|
+
Math.round(selectedModel.contextWindow / 1000),
|
|
3665
|
+
"K tokens"
|
|
3666
|
+
]
|
|
3667
|
+
}),
|
|
3668
|
+
selectedModel.capabilities.vision && /* @__PURE__ */ jsx11("span", {
|
|
3669
|
+
children: "• Vision"
|
|
3670
|
+
}),
|
|
3671
|
+
selectedModel.capabilities.tools && /* @__PURE__ */ jsx11("span", {
|
|
3672
|
+
children: "• Tools"
|
|
3673
|
+
}),
|
|
3674
|
+
selectedModel.capabilities.reasoning && /* @__PURE__ */ jsx11("span", {
|
|
3675
|
+
children: "• Reasoning"
|
|
3676
|
+
})
|
|
3677
|
+
]
|
|
3678
|
+
})
|
|
3679
|
+
]
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
// src/presentation/components/ContextIndicator.tsx
|
|
3683
|
+
import { cn as cn8 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
3684
|
+
import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
3685
|
+
import {
|
|
3686
|
+
Tooltip,
|
|
3687
|
+
TooltipContent,
|
|
3688
|
+
TooltipProvider,
|
|
3689
|
+
TooltipTrigger
|
|
3690
|
+
} from "@contractspec/lib.ui-kit-web/ui/tooltip";
|
|
3691
|
+
import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
|
|
3692
|
+
import { jsx as jsx12, jsxs as jsxs12, Fragment as Fragment6 } from "react/jsx-runtime";
|
|
3693
|
+
"use client";
|
|
3694
|
+
function ContextIndicator({
|
|
3695
|
+
summary,
|
|
3696
|
+
active = false,
|
|
3697
|
+
className,
|
|
3698
|
+
showDetails = true
|
|
3699
|
+
}) {
|
|
3700
|
+
if (!summary && !active) {
|
|
3701
|
+
return /* @__PURE__ */ jsxs12("div", {
|
|
3702
|
+
className: cn8("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
|
|
3703
|
+
children: [
|
|
3704
|
+
/* @__PURE__ */ jsx12(Info, {
|
|
3705
|
+
className: "h-4 w-4"
|
|
3706
|
+
}),
|
|
3707
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3708
|
+
children: "No workspace context"
|
|
3709
|
+
})
|
|
3710
|
+
]
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3713
|
+
const content = /* @__PURE__ */ jsxs12("div", {
|
|
3714
|
+
className: cn8("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
|
|
3715
|
+
children: [
|
|
3716
|
+
/* @__PURE__ */ jsxs12(Badge2, {
|
|
3717
|
+
variant: active ? "default" : "secondary",
|
|
3718
|
+
className: "flex items-center gap-1",
|
|
3719
|
+
children: [
|
|
3720
|
+
/* @__PURE__ */ jsx12(Zap, {
|
|
3721
|
+
className: "h-3 w-3"
|
|
3722
|
+
}),
|
|
3723
|
+
"Context"
|
|
3724
|
+
]
|
|
3725
|
+
}),
|
|
3726
|
+
summary && showDetails && /* @__PURE__ */ jsxs12(Fragment6, {
|
|
3727
|
+
children: [
|
|
3728
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
3729
|
+
className: "flex items-center gap-1 text-xs",
|
|
3730
|
+
children: [
|
|
3731
|
+
/* @__PURE__ */ jsx12(FolderOpen, {
|
|
3732
|
+
className: "h-3.5 w-3.5"
|
|
3733
|
+
}),
|
|
3734
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3735
|
+
children: summary.name
|
|
3736
|
+
})
|
|
3737
|
+
]
|
|
3738
|
+
}),
|
|
3739
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
3740
|
+
className: "flex items-center gap-1 text-xs",
|
|
3741
|
+
children: [
|
|
3742
|
+
/* @__PURE__ */ jsx12(FileCode, {
|
|
3743
|
+
className: "h-3.5 w-3.5"
|
|
3744
|
+
}),
|
|
3745
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
3746
|
+
children: [
|
|
3747
|
+
summary.specs.total,
|
|
3748
|
+
" specs"
|
|
3749
|
+
]
|
|
3750
|
+
})
|
|
3751
|
+
]
|
|
3752
|
+
})
|
|
3753
|
+
]
|
|
3754
|
+
})
|
|
3755
|
+
]
|
|
3756
|
+
});
|
|
3757
|
+
if (!summary) {
|
|
3758
|
+
return content;
|
|
3759
|
+
}
|
|
3760
|
+
return /* @__PURE__ */ jsx12(TooltipProvider, {
|
|
3761
|
+
children: /* @__PURE__ */ jsxs12(Tooltip, {
|
|
3762
|
+
children: [
|
|
3763
|
+
/* @__PURE__ */ jsx12(TooltipTrigger, {
|
|
3764
|
+
asChild: true,
|
|
3765
|
+
children: content
|
|
3766
|
+
}),
|
|
3767
|
+
/* @__PURE__ */ jsx12(TooltipContent, {
|
|
3768
|
+
side: "bottom",
|
|
3769
|
+
className: "max-w-[300px]",
|
|
3770
|
+
children: /* @__PURE__ */ jsxs12("div", {
|
|
3771
|
+
className: "flex flex-col gap-2 text-sm",
|
|
3772
|
+
children: [
|
|
3773
|
+
/* @__PURE__ */ jsx12("div", {
|
|
3774
|
+
className: "font-medium",
|
|
3775
|
+
children: summary.name
|
|
3776
|
+
}),
|
|
3777
|
+
/* @__PURE__ */ jsx12("div", {
|
|
3778
|
+
className: "text-muted-foreground text-xs",
|
|
3779
|
+
children: summary.path
|
|
3780
|
+
}),
|
|
3781
|
+
/* @__PURE__ */ jsx12("div", {
|
|
3782
|
+
className: "border-t pt-2",
|
|
3783
|
+
children: /* @__PURE__ */ jsxs12("div", {
|
|
3784
|
+
className: "grid grid-cols-2 gap-1 text-xs",
|
|
3785
|
+
children: [
|
|
3786
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3787
|
+
children: "Commands:"
|
|
3788
|
+
}),
|
|
3789
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3790
|
+
className: "text-right",
|
|
3791
|
+
children: summary.specs.commands
|
|
3792
|
+
}),
|
|
3793
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3794
|
+
children: "Queries:"
|
|
3795
|
+
}),
|
|
3796
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3797
|
+
className: "text-right",
|
|
3798
|
+
children: summary.specs.queries
|
|
3799
|
+
}),
|
|
3800
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3801
|
+
children: "Events:"
|
|
3802
|
+
}),
|
|
3803
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3804
|
+
className: "text-right",
|
|
3805
|
+
children: summary.specs.events
|
|
3806
|
+
}),
|
|
3807
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3808
|
+
children: "Presentations:"
|
|
3809
|
+
}),
|
|
3810
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3811
|
+
className: "text-right",
|
|
3812
|
+
children: summary.specs.presentations
|
|
3813
|
+
})
|
|
3814
|
+
]
|
|
3815
|
+
})
|
|
3816
|
+
}),
|
|
3817
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
3818
|
+
className: "border-t pt-2 text-xs",
|
|
3819
|
+
children: [
|
|
3820
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
3821
|
+
children: [
|
|
3822
|
+
summary.files.total,
|
|
3823
|
+
" files"
|
|
3824
|
+
]
|
|
3825
|
+
}),
|
|
3826
|
+
/* @__PURE__ */ jsx12("span", {
|
|
3827
|
+
className: "mx-1",
|
|
3828
|
+
children: "•"
|
|
3829
|
+
}),
|
|
3830
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
3831
|
+
children: [
|
|
3832
|
+
summary.files.specFiles,
|
|
3833
|
+
" spec files"
|
|
3834
|
+
]
|
|
3835
|
+
})
|
|
3836
|
+
]
|
|
3837
|
+
})
|
|
3838
|
+
]
|
|
3839
|
+
})
|
|
3840
|
+
})
|
|
3841
|
+
]
|
|
3842
|
+
})
|
|
3843
|
+
});
|
|
3844
|
+
}
|
|
3845
|
+
// src/presentation/hooks/useProviders.tsx
|
|
3846
|
+
import * as React14 from "react";
|
|
3847
|
+
import {
|
|
3848
|
+
getAvailableProviders,
|
|
3849
|
+
getModelsForProvider as getModelsForProvider2
|
|
3850
|
+
} from "@contractspec/lib.ai-providers";
|
|
3851
|
+
"use client";
|
|
3852
|
+
function useProviders() {
|
|
3853
|
+
const [providers, setProviders] = React14.useState([]);
|
|
3854
|
+
const [isLoading, setIsLoading] = React14.useState(true);
|
|
3855
|
+
const loadProviders = React14.useCallback(async () => {
|
|
3856
|
+
setIsLoading(true);
|
|
3857
|
+
try {
|
|
3858
|
+
const available = getAvailableProviders();
|
|
3859
|
+
const providersWithModels = available.map((p) => ({
|
|
3860
|
+
...p,
|
|
3861
|
+
models: getModelsForProvider2(p.provider)
|
|
3862
|
+
}));
|
|
3863
|
+
setProviders(providersWithModels);
|
|
3864
|
+
} catch (error) {
|
|
3865
|
+
console.error("Failed to load providers:", error);
|
|
3866
|
+
} finally {
|
|
3867
|
+
setIsLoading(false);
|
|
3868
|
+
}
|
|
3869
|
+
}, []);
|
|
3870
|
+
React14.useEffect(() => {
|
|
1763
3871
|
loadProviders();
|
|
1764
3872
|
}, [loadProviders]);
|
|
1765
|
-
const availableProviders =
|
|
1766
|
-
const isAvailable =
|
|
1767
|
-
const getModelsCallback =
|
|
3873
|
+
const availableProviders = React14.useMemo(() => providers.filter((p) => p.available), [providers]);
|
|
3874
|
+
const isAvailable = React14.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
|
|
3875
|
+
const getModelsCallback = React14.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
|
|
1768
3876
|
return {
|
|
1769
3877
|
providers,
|
|
1770
3878
|
availableProviders,
|
|
@@ -1779,12 +3887,22 @@ function useProviders() {
|
|
|
1779
3887
|
import { useCompletion } from "@ai-sdk/react";
|
|
1780
3888
|
export {
|
|
1781
3889
|
useProviders,
|
|
3890
|
+
useMessageSelection,
|
|
3891
|
+
useConversations,
|
|
1782
3892
|
useCompletion,
|
|
1783
3893
|
useChat,
|
|
3894
|
+
isPresentationToolResult,
|
|
3895
|
+
isFormToolResult,
|
|
3896
|
+
ToolResultRenderer,
|
|
3897
|
+
ThinkingLevelPicker,
|
|
1784
3898
|
ModelPicker,
|
|
1785
3899
|
ContextIndicator,
|
|
1786
3900
|
CodePreview,
|
|
3901
|
+
ChatWithSidebar,
|
|
3902
|
+
ChatWithExport,
|
|
3903
|
+
ChatSidebar,
|
|
1787
3904
|
ChatMessage,
|
|
1788
3905
|
ChatInput,
|
|
3906
|
+
ChatExportToolbar,
|
|
1789
3907
|
ChatContainer
|
|
1790
3908
|
};
|