@contractspec/module.ai-chat 3.2.0 → 4.0.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 +70 -0
- package/dist/browser/core/index.js +201 -41
- package/dist/browser/index.js +326 -51
- package/dist/browser/presentation/components/index.js +111 -5
- package/dist/browser/presentation/hooks/index.js +215 -46
- package/dist/browser/presentation/index.js +326 -51
- package/dist/core/chat-service.d.ts +15 -2
- package/dist/core/create-chat-route.d.ts +35 -0
- package/dist/core/create-completion-route.d.ts +16 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +201 -41
- package/dist/core/message-types.d.ts +11 -2
- package/dist/index.js +326 -51
- package/dist/node/core/index.js +201 -41
- package/dist/node/index.js +326 -51
- package/dist/node/presentation/components/index.js +111 -5
- package/dist/node/presentation/hooks/index.js +215 -46
- package/dist/node/presentation/index.js +326 -51
- package/dist/presentation/components/index.js +111 -5
- package/dist/presentation/hooks/index.d.ts +3 -1
- package/dist/presentation/hooks/index.js +215 -46
- package/dist/presentation/hooks/useChat.d.ts +18 -0
- package/dist/presentation/index.js +326 -51
- package/package.json +18 -12
|
@@ -79,7 +79,15 @@ import * as React3 from "react";
|
|
|
79
79
|
import { cn as cn3 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
80
80
|
import { Avatar, AvatarFallback } from "@contractspec/lib.ui-kit-web/ui/avatar";
|
|
81
81
|
import { Skeleton } from "@contractspec/lib.ui-kit-web/ui/skeleton";
|
|
82
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
Bot,
|
|
84
|
+
User,
|
|
85
|
+
AlertCircle,
|
|
86
|
+
Copy as Copy2,
|
|
87
|
+
Check as Check2,
|
|
88
|
+
ExternalLink,
|
|
89
|
+
Wrench
|
|
90
|
+
} from "lucide-react";
|
|
83
91
|
import { Button as Button2 } from "@contractspec/lib.design-system";
|
|
84
92
|
|
|
85
93
|
// src/presentation/components/CodePreview.tsx
|
|
@@ -241,12 +249,40 @@ function extractCodeBlocks(content) {
|
|
|
241
249
|
}
|
|
242
250
|
return blocks;
|
|
243
251
|
}
|
|
252
|
+
function renderInlineMarkdown(text) {
|
|
253
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
254
|
+
const parts = [];
|
|
255
|
+
let lastIndex = 0;
|
|
256
|
+
let match;
|
|
257
|
+
let key = 0;
|
|
258
|
+
while ((match = linkRegex.exec(text)) !== null) {
|
|
259
|
+
if (match.index > lastIndex) {
|
|
260
|
+
parts.push(/* @__PURE__ */ jsxDEV3("span", {
|
|
261
|
+
children: text.slice(lastIndex, match.index)
|
|
262
|
+
}, key++, false, undefined, this));
|
|
263
|
+
}
|
|
264
|
+
parts.push(/* @__PURE__ */ jsxDEV3("a", {
|
|
265
|
+
href: match[2],
|
|
266
|
+
target: "_blank",
|
|
267
|
+
rel: "noopener noreferrer",
|
|
268
|
+
className: "text-primary underline hover:no-underline",
|
|
269
|
+
children: match[1]
|
|
270
|
+
}, key++, false, undefined, this));
|
|
271
|
+
lastIndex = match.index + match[0].length;
|
|
272
|
+
}
|
|
273
|
+
if (lastIndex < text.length) {
|
|
274
|
+
parts.push(/* @__PURE__ */ jsxDEV3("span", {
|
|
275
|
+
children: text.slice(lastIndex)
|
|
276
|
+
}, key++, false, undefined, this));
|
|
277
|
+
}
|
|
278
|
+
return parts.length > 0 ? parts : [text];
|
|
279
|
+
}
|
|
244
280
|
function MessageContent({ content }) {
|
|
245
281
|
const codeBlocks = extractCodeBlocks(content);
|
|
246
282
|
if (codeBlocks.length === 0) {
|
|
247
283
|
return /* @__PURE__ */ jsxDEV3("p", {
|
|
248
284
|
className: "whitespace-pre-wrap",
|
|
249
|
-
children: content
|
|
285
|
+
children: renderInlineMarkdown(content)
|
|
250
286
|
}, undefined, false, undefined, this);
|
|
251
287
|
}
|
|
252
288
|
let remaining = content;
|
|
@@ -257,7 +293,7 @@ function MessageContent({ content }) {
|
|
|
257
293
|
if (before) {
|
|
258
294
|
parts.push(/* @__PURE__ */ jsxDEV3("p", {
|
|
259
295
|
className: "whitespace-pre-wrap",
|
|
260
|
-
children: before.trim()
|
|
296
|
+
children: renderInlineMarkdown(before.trim())
|
|
261
297
|
}, key++, false, undefined, this));
|
|
262
298
|
}
|
|
263
299
|
parts.push(/* @__PURE__ */ jsxDEV3(CodePreview, {
|
|
@@ -270,7 +306,7 @@ function MessageContent({ content }) {
|
|
|
270
306
|
if (remaining.trim()) {
|
|
271
307
|
parts.push(/* @__PURE__ */ jsxDEV3("p", {
|
|
272
308
|
className: "whitespace-pre-wrap",
|
|
273
|
-
children: remaining.trim()
|
|
309
|
+
children: renderInlineMarkdown(remaining.trim())
|
|
274
310
|
}, key++, false, undefined, this));
|
|
275
311
|
}
|
|
276
312
|
return /* @__PURE__ */ jsxDEV3(Fragment, {
|
|
@@ -388,7 +424,77 @@ function ChatMessage({
|
|
|
388
424
|
}, undefined, false, undefined, this)
|
|
389
425
|
}, undefined, false, undefined, this)
|
|
390
426
|
]
|
|
391
|
-
}, undefined, true, undefined, this)
|
|
427
|
+
}, undefined, true, undefined, this),
|
|
428
|
+
message.sources && message.sources.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
429
|
+
className: "mt-2 flex flex-wrap gap-2",
|
|
430
|
+
children: message.sources.map((source) => /* @__PURE__ */ jsxDEV3("a", {
|
|
431
|
+
href: source.url ?? "#",
|
|
432
|
+
target: "_blank",
|
|
433
|
+
rel: "noopener noreferrer",
|
|
434
|
+
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
|
+
children: [
|
|
436
|
+
/* @__PURE__ */ jsxDEV3(ExternalLink, {
|
|
437
|
+
className: "h-3 w-3"
|
|
438
|
+
}, undefined, false, undefined, this),
|
|
439
|
+
source.title || source.url || source.id
|
|
440
|
+
]
|
|
441
|
+
}, source.id, true, undefined, this))
|
|
442
|
+
}, undefined, false, undefined, this),
|
|
443
|
+
message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
444
|
+
className: "mt-2 space-y-2",
|
|
445
|
+
children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxDEV3("details", {
|
|
446
|
+
className: "bg-muted border-border rounded-md border",
|
|
447
|
+
children: [
|
|
448
|
+
/* @__PURE__ */ jsxDEV3("summary", {
|
|
449
|
+
className: "flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium",
|
|
450
|
+
children: [
|
|
451
|
+
/* @__PURE__ */ jsxDEV3(Wrench, {
|
|
452
|
+
className: "text-muted-foreground h-4 w-4"
|
|
453
|
+
}, undefined, false, undefined, this),
|
|
454
|
+
tc.name,
|
|
455
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
456
|
+
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
|
+
children: tc.status
|
|
458
|
+
}, undefined, false, undefined, this)
|
|
459
|
+
]
|
|
460
|
+
}, undefined, true, undefined, this),
|
|
461
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
462
|
+
className: "border-border border-t px-3 py-2 text-xs",
|
|
463
|
+
children: [
|
|
464
|
+
Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
465
|
+
className: "mb-2",
|
|
466
|
+
children: [
|
|
467
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
468
|
+
className: "text-muted-foreground font-medium",
|
|
469
|
+
children: "Input:"
|
|
470
|
+
}, undefined, false, undefined, this),
|
|
471
|
+
/* @__PURE__ */ jsxDEV3("pre", {
|
|
472
|
+
className: "bg-background mt-1 overflow-x-auto rounded p-2",
|
|
473
|
+
children: JSON.stringify(tc.args, null, 2)
|
|
474
|
+
}, undefined, false, undefined, this)
|
|
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)
|
|
487
|
+
]
|
|
488
|
+
}, undefined, true, undefined, this),
|
|
489
|
+
tc.error && /* @__PURE__ */ jsxDEV3("p", {
|
|
490
|
+
className: "text-destructive mt-1",
|
|
491
|
+
children: tc.error
|
|
492
|
+
}, undefined, false, undefined, this)
|
|
493
|
+
]
|
|
494
|
+
}, undefined, true, undefined, this)
|
|
495
|
+
]
|
|
496
|
+
}, tc.id, true, undefined, this))
|
|
497
|
+
}, undefined, false, undefined, this)
|
|
392
498
|
]
|
|
393
499
|
}, undefined, true, undefined, this)
|
|
394
500
|
]
|
|
@@ -961,6 +1067,8 @@ function ContextIndicator({
|
|
|
961
1067
|
}
|
|
962
1068
|
// src/presentation/hooks/useChat.tsx
|
|
963
1069
|
import * as React6 from "react";
|
|
1070
|
+
import { tool } from "ai";
|
|
1071
|
+
import { z } from "zod";
|
|
964
1072
|
|
|
965
1073
|
// src/core/chat-service.ts
|
|
966
1074
|
import { generateText, streamText } from "ai";
|
|
@@ -1095,6 +1203,9 @@ class ChatService {
|
|
|
1095
1203
|
systemPrompt;
|
|
1096
1204
|
maxHistoryMessages;
|
|
1097
1205
|
onUsage;
|
|
1206
|
+
tools;
|
|
1207
|
+
sendReasoning;
|
|
1208
|
+
sendSources;
|
|
1098
1209
|
constructor(config) {
|
|
1099
1210
|
this.provider = config.provider;
|
|
1100
1211
|
this.context = config.context;
|
|
@@ -1102,6 +1213,9 @@ class ChatService {
|
|
|
1102
1213
|
this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
1103
1214
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
1104
1215
|
this.onUsage = config.onUsage;
|
|
1216
|
+
this.tools = config.tools;
|
|
1217
|
+
this.sendReasoning = config.sendReasoning ?? false;
|
|
1218
|
+
this.sendSources = config.sendSources ?? false;
|
|
1105
1219
|
}
|
|
1106
1220
|
async send(options) {
|
|
1107
1221
|
let conversation;
|
|
@@ -1126,13 +1240,14 @@ class ChatService {
|
|
|
1126
1240
|
status: "completed",
|
|
1127
1241
|
attachments: options.attachments
|
|
1128
1242
|
});
|
|
1129
|
-
const
|
|
1243
|
+
const messages = this.buildMessages(conversation, options);
|
|
1130
1244
|
const model = this.provider.getModel();
|
|
1131
1245
|
try {
|
|
1132
1246
|
const result = await generateText({
|
|
1133
1247
|
model,
|
|
1134
|
-
|
|
1135
|
-
system: this.systemPrompt
|
|
1248
|
+
messages,
|
|
1249
|
+
system: this.systemPrompt,
|
|
1250
|
+
tools: this.tools
|
|
1136
1251
|
});
|
|
1137
1252
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
1138
1253
|
role: "assistant",
|
|
@@ -1188,33 +1303,106 @@ class ChatService {
|
|
|
1188
1303
|
content: "",
|
|
1189
1304
|
status: "streaming"
|
|
1190
1305
|
});
|
|
1191
|
-
const
|
|
1306
|
+
const messages = this.buildMessages(conversation, options);
|
|
1192
1307
|
const model = this.provider.getModel();
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1308
|
+
const systemPrompt = this.systemPrompt;
|
|
1309
|
+
const tools = this.tools;
|
|
1310
|
+
const store = this.store;
|
|
1311
|
+
const onUsage = this.onUsage;
|
|
1197
1312
|
async function* streamGenerator() {
|
|
1198
1313
|
let fullContent = "";
|
|
1314
|
+
let fullReasoning = "";
|
|
1315
|
+
const toolCallsMap = new Map;
|
|
1316
|
+
const sources = [];
|
|
1199
1317
|
try {
|
|
1200
1318
|
const result = streamText({
|
|
1201
1319
|
model,
|
|
1202
|
-
|
|
1203
|
-
system:
|
|
1320
|
+
messages,
|
|
1321
|
+
system: systemPrompt,
|
|
1322
|
+
tools
|
|
1204
1323
|
});
|
|
1205
|
-
for await (const
|
|
1206
|
-
|
|
1207
|
-
|
|
1324
|
+
for await (const part of result.fullStream) {
|
|
1325
|
+
if (part.type === "text-delta") {
|
|
1326
|
+
const text = part.text ?? "";
|
|
1327
|
+
if (text) {
|
|
1328
|
+
fullContent += text;
|
|
1329
|
+
yield { type: "text", content: text };
|
|
1330
|
+
}
|
|
1331
|
+
} else if (part.type === "reasoning-delta") {
|
|
1332
|
+
const text = part.text ?? "";
|
|
1333
|
+
if (text) {
|
|
1334
|
+
fullReasoning += text;
|
|
1335
|
+
yield { type: "reasoning", content: text };
|
|
1336
|
+
}
|
|
1337
|
+
} else if (part.type === "source") {
|
|
1338
|
+
const src = part;
|
|
1339
|
+
const source = {
|
|
1340
|
+
id: src.id,
|
|
1341
|
+
title: src.title ?? "",
|
|
1342
|
+
url: src.url,
|
|
1343
|
+
type: "web"
|
|
1344
|
+
};
|
|
1345
|
+
sources.push(source);
|
|
1346
|
+
yield { type: "source", source };
|
|
1347
|
+
} else if (part.type === "tool-call") {
|
|
1348
|
+
const toolCall = {
|
|
1349
|
+
id: part.toolCallId,
|
|
1350
|
+
name: part.toolName,
|
|
1351
|
+
args: part.input ?? {},
|
|
1352
|
+
status: "running"
|
|
1353
|
+
};
|
|
1354
|
+
toolCallsMap.set(part.toolCallId, toolCall);
|
|
1355
|
+
yield { type: "tool_call", toolCall };
|
|
1356
|
+
} else if (part.type === "tool-result") {
|
|
1357
|
+
const tc = toolCallsMap.get(part.toolCallId);
|
|
1358
|
+
if (tc) {
|
|
1359
|
+
tc.result = part.output;
|
|
1360
|
+
tc.status = "completed";
|
|
1361
|
+
}
|
|
1362
|
+
yield {
|
|
1363
|
+
type: "tool_result",
|
|
1364
|
+
toolResult: {
|
|
1365
|
+
toolCallId: part.toolCallId,
|
|
1366
|
+
toolName: part.toolName,
|
|
1367
|
+
result: part.output
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
} else if (part.type === "tool-error") {
|
|
1371
|
+
const tc = toolCallsMap.get(part.toolCallId);
|
|
1372
|
+
if (tc) {
|
|
1373
|
+
tc.status = "error";
|
|
1374
|
+
tc.error = part.error ?? "Tool execution failed";
|
|
1375
|
+
}
|
|
1376
|
+
} else if (part.type === "finish") {
|
|
1377
|
+
const usage = part.usage;
|
|
1378
|
+
const inputTokens = usage?.inputTokens ?? 0;
|
|
1379
|
+
const outputTokens = usage?.completionTokens ?? 0;
|
|
1380
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
1381
|
+
content: fullContent,
|
|
1382
|
+
status: "completed",
|
|
1383
|
+
reasoning: fullReasoning || undefined,
|
|
1384
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
1385
|
+
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined,
|
|
1386
|
+
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
1387
|
+
});
|
|
1388
|
+
onUsage?.({ inputTokens, outputTokens });
|
|
1389
|
+
yield {
|
|
1390
|
+
type: "done",
|
|
1391
|
+
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
1392
|
+
};
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1208
1395
|
}
|
|
1209
|
-
await
|
|
1396
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
1210
1397
|
content: fullContent,
|
|
1211
|
-
status: "completed"
|
|
1398
|
+
status: "completed",
|
|
1399
|
+
reasoning: fullReasoning || undefined,
|
|
1400
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
1401
|
+
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined
|
|
1212
1402
|
});
|
|
1213
|
-
yield {
|
|
1214
|
-
type: "done"
|
|
1215
|
-
};
|
|
1403
|
+
yield { type: "done" };
|
|
1216
1404
|
} catch (error) {
|
|
1217
|
-
await
|
|
1405
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
1218
1406
|
content: fullContent,
|
|
1219
1407
|
status: "error",
|
|
1220
1408
|
error: {
|
|
@@ -1249,40 +1437,59 @@ class ChatService {
|
|
|
1249
1437
|
async deleteConversation(conversationId) {
|
|
1250
1438
|
return this.store.delete(conversationId);
|
|
1251
1439
|
}
|
|
1252
|
-
|
|
1253
|
-
let prompt = "";
|
|
1440
|
+
buildMessages(conversation, _options) {
|
|
1254
1441
|
const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
|
|
1442
|
+
const messages = [];
|
|
1255
1443
|
for (let i = historyStart;i < conversation.messages.length; i++) {
|
|
1256
1444
|
const msg = conversation.messages[i];
|
|
1257
1445
|
if (!msg)
|
|
1258
1446
|
continue;
|
|
1259
|
-
if (msg.role === "user"
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
let content = options.content;
|
|
1266
|
-
if (options.attachments?.length) {
|
|
1267
|
-
const attachmentInfo = options.attachments.map((a) => {
|
|
1268
|
-
if (a.type === "file" || a.type === "code") {
|
|
1269
|
-
return `
|
|
1447
|
+
if (msg.role === "user") {
|
|
1448
|
+
let content = msg.content;
|
|
1449
|
+
if (msg.attachments?.length) {
|
|
1450
|
+
const attachmentInfo = msg.attachments.map((a) => {
|
|
1451
|
+
if (a.type === "file" || a.type === "code") {
|
|
1452
|
+
return `
|
|
1270
1453
|
|
|
1271
1454
|
### ${a.name}
|
|
1272
1455
|
\`\`\`
|
|
1273
|
-
${a.content}
|
|
1456
|
+
${a.content ?? ""}
|
|
1274
1457
|
\`\`\``;
|
|
1275
|
-
|
|
1276
|
-
|
|
1458
|
+
}
|
|
1459
|
+
return `
|
|
1277
1460
|
|
|
1278
1461
|
[Attachment: ${a.name}]`;
|
|
1279
|
-
|
|
1280
|
-
|
|
1462
|
+
}).join("");
|
|
1463
|
+
content += attachmentInfo;
|
|
1464
|
+
}
|
|
1465
|
+
messages.push({ role: "user", content });
|
|
1466
|
+
} else if (msg.role === "assistant") {
|
|
1467
|
+
if (msg.toolCalls?.length) {
|
|
1468
|
+
messages.push({
|
|
1469
|
+
role: "assistant",
|
|
1470
|
+
content: msg.content || "",
|
|
1471
|
+
toolCalls: msg.toolCalls.map((tc) => ({
|
|
1472
|
+
type: "tool-call",
|
|
1473
|
+
toolCallId: tc.id,
|
|
1474
|
+
toolName: tc.name,
|
|
1475
|
+
args: tc.args
|
|
1476
|
+
}))
|
|
1477
|
+
});
|
|
1478
|
+
messages.push({
|
|
1479
|
+
role: "tool",
|
|
1480
|
+
content: msg.toolCalls.map((tc) => ({
|
|
1481
|
+
type: "tool-result",
|
|
1482
|
+
toolCallId: tc.id,
|
|
1483
|
+
toolName: tc.name,
|
|
1484
|
+
output: tc.result
|
|
1485
|
+
}))
|
|
1486
|
+
});
|
|
1487
|
+
} else {
|
|
1488
|
+
messages.push({ role: "assistant", content: msg.content });
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1281
1491
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
Assistant:`;
|
|
1285
|
-
return prompt;
|
|
1492
|
+
return messages;
|
|
1286
1493
|
}
|
|
1287
1494
|
}
|
|
1288
1495
|
function createChatService(config) {
|
|
@@ -1294,6 +1501,17 @@ import {
|
|
|
1294
1501
|
createProvider
|
|
1295
1502
|
} from "@contractspec/lib.ai-providers";
|
|
1296
1503
|
"use client";
|
|
1504
|
+
function toolsToToolSet(defs) {
|
|
1505
|
+
const result = {};
|
|
1506
|
+
for (const def of defs) {
|
|
1507
|
+
result[def.name] = tool({
|
|
1508
|
+
description: def.description ?? def.name,
|
|
1509
|
+
inputSchema: z.object({}).passthrough(),
|
|
1510
|
+
execute: async () => ({})
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
return result;
|
|
1514
|
+
}
|
|
1297
1515
|
function useChat(options = {}) {
|
|
1298
1516
|
const {
|
|
1299
1517
|
provider = "openai",
|
|
@@ -1307,7 +1525,8 @@ function useChat(options = {}) {
|
|
|
1307
1525
|
onSend,
|
|
1308
1526
|
onResponse,
|
|
1309
1527
|
onError,
|
|
1310
|
-
onUsage
|
|
1528
|
+
onUsage,
|
|
1529
|
+
tools: toolsDefs
|
|
1311
1530
|
} = options;
|
|
1312
1531
|
const [messages, setMessages] = React6.useState([]);
|
|
1313
1532
|
const [conversation, setConversation] = React6.useState(null);
|
|
@@ -1326,9 +1545,19 @@ function useChat(options = {}) {
|
|
|
1326
1545
|
chatServiceRef.current = new ChatService({
|
|
1327
1546
|
provider: chatProvider,
|
|
1328
1547
|
systemPrompt,
|
|
1329
|
-
onUsage
|
|
1548
|
+
onUsage,
|
|
1549
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
|
|
1330
1550
|
});
|
|
1331
|
-
}, [
|
|
1551
|
+
}, [
|
|
1552
|
+
provider,
|
|
1553
|
+
mode,
|
|
1554
|
+
model,
|
|
1555
|
+
apiKey,
|
|
1556
|
+
proxyUrl,
|
|
1557
|
+
systemPrompt,
|
|
1558
|
+
onUsage,
|
|
1559
|
+
toolsDefs
|
|
1560
|
+
]);
|
|
1332
1561
|
React6.useEffect(() => {
|
|
1333
1562
|
if (!conversationId || !chatServiceRef.current)
|
|
1334
1563
|
return;
|
|
@@ -1383,13 +1612,50 @@ function useChat(options = {}) {
|
|
|
1383
1612
|
};
|
|
1384
1613
|
setMessages((prev) => [...prev, assistantMessage]);
|
|
1385
1614
|
let fullContent = "";
|
|
1615
|
+
let fullReasoning = "";
|
|
1616
|
+
const toolCallsMap = new Map;
|
|
1617
|
+
const sources = [];
|
|
1386
1618
|
for await (const chunk of result.stream) {
|
|
1387
1619
|
if (chunk.type === "text" && chunk.content) {
|
|
1388
1620
|
fullContent += chunk.content;
|
|
1389
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
1621
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
1622
|
+
...m,
|
|
1623
|
+
content: fullContent,
|
|
1624
|
+
reasoning: fullReasoning || undefined,
|
|
1625
|
+
sources: sources.length ? sources : undefined,
|
|
1626
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined
|
|
1627
|
+
} : m));
|
|
1628
|
+
} else if (chunk.type === "reasoning" && chunk.content) {
|
|
1629
|
+
fullReasoning += chunk.content;
|
|
1630
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, reasoning: fullReasoning } : m));
|
|
1631
|
+
} else if (chunk.type === "source" && chunk.source) {
|
|
1632
|
+
sources.push(chunk.source);
|
|
1633
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, sources: [...sources] } : m));
|
|
1634
|
+
} else if (chunk.type === "tool_call" && chunk.toolCall) {
|
|
1635
|
+
const tc = chunk.toolCall;
|
|
1636
|
+
const chatTc = {
|
|
1637
|
+
id: tc.id,
|
|
1638
|
+
name: tc.name,
|
|
1639
|
+
args: tc.args,
|
|
1640
|
+
status: "running"
|
|
1641
|
+
};
|
|
1642
|
+
toolCallsMap.set(tc.id, chatTc);
|
|
1643
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
1644
|
+
} else if (chunk.type === "tool_result" && chunk.toolResult) {
|
|
1645
|
+
const tr = chunk.toolResult;
|
|
1646
|
+
const tc = toolCallsMap.get(tr.toolCallId);
|
|
1647
|
+
if (tc) {
|
|
1648
|
+
tc.result = tr.result;
|
|
1649
|
+
tc.status = "completed";
|
|
1650
|
+
}
|
|
1651
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
1390
1652
|
} else if (chunk.type === "done") {
|
|
1391
1653
|
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
1392
1654
|
...m,
|
|
1655
|
+
content: fullContent,
|
|
1656
|
+
reasoning: fullReasoning || undefined,
|
|
1657
|
+
sources: sources.length ? sources : undefined,
|
|
1658
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
1393
1659
|
status: "completed",
|
|
1394
1660
|
usage: chunk.usage,
|
|
1395
1661
|
updatedAt: new Date
|
|
@@ -1451,6 +1717,10 @@ function useChat(options = {}) {
|
|
|
1451
1717
|
abortControllerRef.current?.abort();
|
|
1452
1718
|
setIsLoading(false);
|
|
1453
1719
|
}, []);
|
|
1720
|
+
const addToolApprovalResponse = React6.useCallback((_toolCallId, _result) => {
|
|
1721
|
+
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
1722
|
+
}, []);
|
|
1723
|
+
const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
|
|
1454
1724
|
return {
|
|
1455
1725
|
messages,
|
|
1456
1726
|
conversation,
|
|
@@ -1460,7 +1730,8 @@ function useChat(options = {}) {
|
|
|
1460
1730
|
clearConversation,
|
|
1461
1731
|
setConversationId,
|
|
1462
1732
|
regenerate,
|
|
1463
|
-
stop
|
|
1733
|
+
stop,
|
|
1734
|
+
...hasApprovalTools && { addToolApprovalResponse }
|
|
1464
1735
|
};
|
|
1465
1736
|
}
|
|
1466
1737
|
// src/presentation/hooks/useProviders.tsx
|
|
@@ -1503,8 +1774,12 @@ function useProviders() {
|
|
|
1503
1774
|
refresh: loadProviders
|
|
1504
1775
|
};
|
|
1505
1776
|
}
|
|
1777
|
+
|
|
1778
|
+
// src/presentation/hooks/index.ts
|
|
1779
|
+
import { useCompletion } from "@ai-sdk/react";
|
|
1506
1780
|
export {
|
|
1507
1781
|
useProviders,
|
|
1782
|
+
useCompletion,
|
|
1508
1783
|
useChat,
|
|
1509
1784
|
ModelPicker,
|
|
1510
1785
|
ContextIndicator,
|