@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
|
@@ -84,7 +84,15 @@ import * as React3 from "react";
|
|
|
84
84
|
import { cn as cn3 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
85
85
|
import { Avatar, AvatarFallback } from "@contractspec/lib.ui-kit-web/ui/avatar";
|
|
86
86
|
import { Skeleton } from "@contractspec/lib.ui-kit-web/ui/skeleton";
|
|
87
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
Bot,
|
|
89
|
+
User,
|
|
90
|
+
AlertCircle,
|
|
91
|
+
Copy as Copy2,
|
|
92
|
+
Check as Check2,
|
|
93
|
+
ExternalLink,
|
|
94
|
+
Wrench
|
|
95
|
+
} from "lucide-react";
|
|
88
96
|
import { Button as Button2 } from "@contractspec/lib.design-system";
|
|
89
97
|
|
|
90
98
|
// src/presentation/components/CodePreview.tsx
|
|
@@ -246,12 +254,40 @@ function extractCodeBlocks(content) {
|
|
|
246
254
|
}
|
|
247
255
|
return blocks;
|
|
248
256
|
}
|
|
257
|
+
function renderInlineMarkdown(text) {
|
|
258
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
259
|
+
const parts = [];
|
|
260
|
+
let lastIndex = 0;
|
|
261
|
+
let match;
|
|
262
|
+
let key = 0;
|
|
263
|
+
while ((match = linkRegex.exec(text)) !== null) {
|
|
264
|
+
if (match.index > lastIndex) {
|
|
265
|
+
parts.push(/* @__PURE__ */ jsxDEV3("span", {
|
|
266
|
+
children: text.slice(lastIndex, match.index)
|
|
267
|
+
}, key++, false, undefined, this));
|
|
268
|
+
}
|
|
269
|
+
parts.push(/* @__PURE__ */ jsxDEV3("a", {
|
|
270
|
+
href: match[2],
|
|
271
|
+
target: "_blank",
|
|
272
|
+
rel: "noopener noreferrer",
|
|
273
|
+
className: "text-primary underline hover:no-underline",
|
|
274
|
+
children: match[1]
|
|
275
|
+
}, key++, false, undefined, this));
|
|
276
|
+
lastIndex = match.index + match[0].length;
|
|
277
|
+
}
|
|
278
|
+
if (lastIndex < text.length) {
|
|
279
|
+
parts.push(/* @__PURE__ */ jsxDEV3("span", {
|
|
280
|
+
children: text.slice(lastIndex)
|
|
281
|
+
}, key++, false, undefined, this));
|
|
282
|
+
}
|
|
283
|
+
return parts.length > 0 ? parts : [text];
|
|
284
|
+
}
|
|
249
285
|
function MessageContent({ content }) {
|
|
250
286
|
const codeBlocks = extractCodeBlocks(content);
|
|
251
287
|
if (codeBlocks.length === 0) {
|
|
252
288
|
return /* @__PURE__ */ jsxDEV3("p", {
|
|
253
289
|
className: "whitespace-pre-wrap",
|
|
254
|
-
children: content
|
|
290
|
+
children: renderInlineMarkdown(content)
|
|
255
291
|
}, undefined, false, undefined, this);
|
|
256
292
|
}
|
|
257
293
|
let remaining = content;
|
|
@@ -262,7 +298,7 @@ function MessageContent({ content }) {
|
|
|
262
298
|
if (before) {
|
|
263
299
|
parts.push(/* @__PURE__ */ jsxDEV3("p", {
|
|
264
300
|
className: "whitespace-pre-wrap",
|
|
265
|
-
children: before.trim()
|
|
301
|
+
children: renderInlineMarkdown(before.trim())
|
|
266
302
|
}, key++, false, undefined, this));
|
|
267
303
|
}
|
|
268
304
|
parts.push(/* @__PURE__ */ jsxDEV3(CodePreview, {
|
|
@@ -275,7 +311,7 @@ function MessageContent({ content }) {
|
|
|
275
311
|
if (remaining.trim()) {
|
|
276
312
|
parts.push(/* @__PURE__ */ jsxDEV3("p", {
|
|
277
313
|
className: "whitespace-pre-wrap",
|
|
278
|
-
children: remaining.trim()
|
|
314
|
+
children: renderInlineMarkdown(remaining.trim())
|
|
279
315
|
}, key++, false, undefined, this));
|
|
280
316
|
}
|
|
281
317
|
return /* @__PURE__ */ jsxDEV3(Fragment, {
|
|
@@ -393,7 +429,77 @@ function ChatMessage({
|
|
|
393
429
|
}, undefined, false, undefined, this)
|
|
394
430
|
}, undefined, false, undefined, this)
|
|
395
431
|
]
|
|
396
|
-
}, undefined, true, undefined, this)
|
|
432
|
+
}, undefined, true, undefined, this),
|
|
433
|
+
message.sources && message.sources.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
434
|
+
className: "mt-2 flex flex-wrap gap-2",
|
|
435
|
+
children: message.sources.map((source) => /* @__PURE__ */ jsxDEV3("a", {
|
|
436
|
+
href: source.url ?? "#",
|
|
437
|
+
target: "_blank",
|
|
438
|
+
rel: "noopener noreferrer",
|
|
439
|
+
className: "text-muted-foreground hover:text-foreground bg-muted inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors",
|
|
440
|
+
children: [
|
|
441
|
+
/* @__PURE__ */ jsxDEV3(ExternalLink, {
|
|
442
|
+
className: "h-3 w-3"
|
|
443
|
+
}, undefined, false, undefined, this),
|
|
444
|
+
source.title || source.url || source.id
|
|
445
|
+
]
|
|
446
|
+
}, source.id, true, undefined, this))
|
|
447
|
+
}, undefined, false, undefined, this),
|
|
448
|
+
message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
449
|
+
className: "mt-2 space-y-2",
|
|
450
|
+
children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxDEV3("details", {
|
|
451
|
+
className: "bg-muted border-border rounded-md border",
|
|
452
|
+
children: [
|
|
453
|
+
/* @__PURE__ */ jsxDEV3("summary", {
|
|
454
|
+
className: "flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium",
|
|
455
|
+
children: [
|
|
456
|
+
/* @__PURE__ */ jsxDEV3(Wrench, {
|
|
457
|
+
className: "text-muted-foreground h-4 w-4"
|
|
458
|
+
}, undefined, false, undefined, this),
|
|
459
|
+
tc.name,
|
|
460
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
461
|
+
className: cn3("ml-auto rounded px-1.5 py-0.5 text-xs", tc.status === "completed" && "bg-green-500/20 text-green-700 dark:text-green-400", tc.status === "error" && "bg-destructive/20 text-destructive", tc.status === "running" && "bg-blue-500/20 text-blue-700 dark:text-blue-400"),
|
|
462
|
+
children: tc.status
|
|
463
|
+
}, undefined, false, undefined, this)
|
|
464
|
+
]
|
|
465
|
+
}, undefined, true, undefined, this),
|
|
466
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
467
|
+
className: "border-border border-t px-3 py-2 text-xs",
|
|
468
|
+
children: [
|
|
469
|
+
Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
470
|
+
className: "mb-2",
|
|
471
|
+
children: [
|
|
472
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
473
|
+
className: "text-muted-foreground font-medium",
|
|
474
|
+
children: "Input:"
|
|
475
|
+
}, undefined, false, undefined, this),
|
|
476
|
+
/* @__PURE__ */ jsxDEV3("pre", {
|
|
477
|
+
className: "bg-background mt-1 overflow-x-auto rounded p-2",
|
|
478
|
+
children: JSON.stringify(tc.args, null, 2)
|
|
479
|
+
}, undefined, false, undefined, this)
|
|
480
|
+
]
|
|
481
|
+
}, undefined, true, undefined, this),
|
|
482
|
+
tc.result !== undefined && /* @__PURE__ */ jsxDEV3("div", {
|
|
483
|
+
children: [
|
|
484
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
485
|
+
className: "text-muted-foreground font-medium",
|
|
486
|
+
children: "Output:"
|
|
487
|
+
}, undefined, false, undefined, this),
|
|
488
|
+
/* @__PURE__ */ jsxDEV3("pre", {
|
|
489
|
+
className: "bg-background mt-1 overflow-x-auto rounded p-2",
|
|
490
|
+
children: typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)
|
|
491
|
+
}, undefined, false, undefined, this)
|
|
492
|
+
]
|
|
493
|
+
}, undefined, true, undefined, this),
|
|
494
|
+
tc.error && /* @__PURE__ */ jsxDEV3("p", {
|
|
495
|
+
className: "text-destructive mt-1",
|
|
496
|
+
children: tc.error
|
|
497
|
+
}, undefined, false, undefined, this)
|
|
498
|
+
]
|
|
499
|
+
}, undefined, true, undefined, this)
|
|
500
|
+
]
|
|
501
|
+
}, tc.id, true, undefined, this))
|
|
502
|
+
}, undefined, false, undefined, this)
|
|
397
503
|
]
|
|
398
504
|
}, undefined, true, undefined, this)
|
|
399
505
|
]
|
|
@@ -8,6 +8,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
|
|
9
9
|
// src/presentation/hooks/useChat.tsx
|
|
10
10
|
import * as React from "react";
|
|
11
|
+
import { tool } from "ai";
|
|
12
|
+
import { z } from "zod";
|
|
11
13
|
|
|
12
14
|
// src/core/chat-service.ts
|
|
13
15
|
import { generateText, streamText } from "ai";
|
|
@@ -142,6 +144,9 @@ class ChatService {
|
|
|
142
144
|
systemPrompt;
|
|
143
145
|
maxHistoryMessages;
|
|
144
146
|
onUsage;
|
|
147
|
+
tools;
|
|
148
|
+
sendReasoning;
|
|
149
|
+
sendSources;
|
|
145
150
|
constructor(config) {
|
|
146
151
|
this.provider = config.provider;
|
|
147
152
|
this.context = config.context;
|
|
@@ -149,6 +154,9 @@ class ChatService {
|
|
|
149
154
|
this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
150
155
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
151
156
|
this.onUsage = config.onUsage;
|
|
157
|
+
this.tools = config.tools;
|
|
158
|
+
this.sendReasoning = config.sendReasoning ?? false;
|
|
159
|
+
this.sendSources = config.sendSources ?? false;
|
|
152
160
|
}
|
|
153
161
|
async send(options) {
|
|
154
162
|
let conversation;
|
|
@@ -173,13 +181,14 @@ class ChatService {
|
|
|
173
181
|
status: "completed",
|
|
174
182
|
attachments: options.attachments
|
|
175
183
|
});
|
|
176
|
-
const
|
|
184
|
+
const messages = this.buildMessages(conversation, options);
|
|
177
185
|
const model = this.provider.getModel();
|
|
178
186
|
try {
|
|
179
187
|
const result = await generateText({
|
|
180
188
|
model,
|
|
181
|
-
|
|
182
|
-
system: this.systemPrompt
|
|
189
|
+
messages,
|
|
190
|
+
system: this.systemPrompt,
|
|
191
|
+
tools: this.tools
|
|
183
192
|
});
|
|
184
193
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
185
194
|
role: "assistant",
|
|
@@ -235,33 +244,106 @@ class ChatService {
|
|
|
235
244
|
content: "",
|
|
236
245
|
status: "streaming"
|
|
237
246
|
});
|
|
238
|
-
const
|
|
247
|
+
const messages = this.buildMessages(conversation, options);
|
|
239
248
|
const model = this.provider.getModel();
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
249
|
+
const systemPrompt = this.systemPrompt;
|
|
250
|
+
const tools = this.tools;
|
|
251
|
+
const store = this.store;
|
|
252
|
+
const onUsage = this.onUsage;
|
|
244
253
|
async function* streamGenerator() {
|
|
245
254
|
let fullContent = "";
|
|
255
|
+
let fullReasoning = "";
|
|
256
|
+
const toolCallsMap = new Map;
|
|
257
|
+
const sources = [];
|
|
246
258
|
try {
|
|
247
259
|
const result = streamText({
|
|
248
260
|
model,
|
|
249
|
-
|
|
250
|
-
system:
|
|
261
|
+
messages,
|
|
262
|
+
system: systemPrompt,
|
|
263
|
+
tools
|
|
251
264
|
});
|
|
252
|
-
for await (const
|
|
253
|
-
|
|
254
|
-
|
|
265
|
+
for await (const part of result.fullStream) {
|
|
266
|
+
if (part.type === "text-delta") {
|
|
267
|
+
const text = part.text ?? "";
|
|
268
|
+
if (text) {
|
|
269
|
+
fullContent += text;
|
|
270
|
+
yield { type: "text", content: text };
|
|
271
|
+
}
|
|
272
|
+
} else if (part.type === "reasoning-delta") {
|
|
273
|
+
const text = part.text ?? "";
|
|
274
|
+
if (text) {
|
|
275
|
+
fullReasoning += text;
|
|
276
|
+
yield { type: "reasoning", content: text };
|
|
277
|
+
}
|
|
278
|
+
} else if (part.type === "source") {
|
|
279
|
+
const src = part;
|
|
280
|
+
const source = {
|
|
281
|
+
id: src.id,
|
|
282
|
+
title: src.title ?? "",
|
|
283
|
+
url: src.url,
|
|
284
|
+
type: "web"
|
|
285
|
+
};
|
|
286
|
+
sources.push(source);
|
|
287
|
+
yield { type: "source", source };
|
|
288
|
+
} else if (part.type === "tool-call") {
|
|
289
|
+
const toolCall = {
|
|
290
|
+
id: part.toolCallId,
|
|
291
|
+
name: part.toolName,
|
|
292
|
+
args: part.input ?? {},
|
|
293
|
+
status: "running"
|
|
294
|
+
};
|
|
295
|
+
toolCallsMap.set(part.toolCallId, toolCall);
|
|
296
|
+
yield { type: "tool_call", toolCall };
|
|
297
|
+
} else if (part.type === "tool-result") {
|
|
298
|
+
const tc = toolCallsMap.get(part.toolCallId);
|
|
299
|
+
if (tc) {
|
|
300
|
+
tc.result = part.output;
|
|
301
|
+
tc.status = "completed";
|
|
302
|
+
}
|
|
303
|
+
yield {
|
|
304
|
+
type: "tool_result",
|
|
305
|
+
toolResult: {
|
|
306
|
+
toolCallId: part.toolCallId,
|
|
307
|
+
toolName: part.toolName,
|
|
308
|
+
result: part.output
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
} else if (part.type === "tool-error") {
|
|
312
|
+
const tc = toolCallsMap.get(part.toolCallId);
|
|
313
|
+
if (tc) {
|
|
314
|
+
tc.status = "error";
|
|
315
|
+
tc.error = part.error ?? "Tool execution failed";
|
|
316
|
+
}
|
|
317
|
+
} else if (part.type === "finish") {
|
|
318
|
+
const usage = part.usage;
|
|
319
|
+
const inputTokens = usage?.inputTokens ?? 0;
|
|
320
|
+
const outputTokens = usage?.completionTokens ?? 0;
|
|
321
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
322
|
+
content: fullContent,
|
|
323
|
+
status: "completed",
|
|
324
|
+
reasoning: fullReasoning || undefined,
|
|
325
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
326
|
+
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined,
|
|
327
|
+
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
328
|
+
});
|
|
329
|
+
onUsage?.({ inputTokens, outputTokens });
|
|
330
|
+
yield {
|
|
331
|
+
type: "done",
|
|
332
|
+
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
333
|
+
};
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
255
336
|
}
|
|
256
|
-
await
|
|
337
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
257
338
|
content: fullContent,
|
|
258
|
-
status: "completed"
|
|
339
|
+
status: "completed",
|
|
340
|
+
reasoning: fullReasoning || undefined,
|
|
341
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
342
|
+
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined
|
|
259
343
|
});
|
|
260
|
-
yield {
|
|
261
|
-
type: "done"
|
|
262
|
-
};
|
|
344
|
+
yield { type: "done" };
|
|
263
345
|
} catch (error) {
|
|
264
|
-
await
|
|
346
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
265
347
|
content: fullContent,
|
|
266
348
|
status: "error",
|
|
267
349
|
error: {
|
|
@@ -296,40 +378,59 @@ class ChatService {
|
|
|
296
378
|
async deleteConversation(conversationId) {
|
|
297
379
|
return this.store.delete(conversationId);
|
|
298
380
|
}
|
|
299
|
-
|
|
300
|
-
let prompt = "";
|
|
381
|
+
buildMessages(conversation, _options) {
|
|
301
382
|
const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
|
|
383
|
+
const messages = [];
|
|
302
384
|
for (let i = historyStart;i < conversation.messages.length; i++) {
|
|
303
385
|
const msg = conversation.messages[i];
|
|
304
386
|
if (!msg)
|
|
305
387
|
continue;
|
|
306
|
-
if (msg.role === "user"
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
let content = options.content;
|
|
313
|
-
if (options.attachments?.length) {
|
|
314
|
-
const attachmentInfo = options.attachments.map((a) => {
|
|
315
|
-
if (a.type === "file" || a.type === "code") {
|
|
316
|
-
return `
|
|
388
|
+
if (msg.role === "user") {
|
|
389
|
+
let content = msg.content;
|
|
390
|
+
if (msg.attachments?.length) {
|
|
391
|
+
const attachmentInfo = msg.attachments.map((a) => {
|
|
392
|
+
if (a.type === "file" || a.type === "code") {
|
|
393
|
+
return `
|
|
317
394
|
|
|
318
395
|
### ${a.name}
|
|
319
396
|
\`\`\`
|
|
320
|
-
${a.content}
|
|
397
|
+
${a.content ?? ""}
|
|
321
398
|
\`\`\``;
|
|
322
|
-
|
|
323
|
-
|
|
399
|
+
}
|
|
400
|
+
return `
|
|
324
401
|
|
|
325
402
|
[Attachment: ${a.name}]`;
|
|
326
|
-
|
|
327
|
-
|
|
403
|
+
}).join("");
|
|
404
|
+
content += attachmentInfo;
|
|
405
|
+
}
|
|
406
|
+
messages.push({ role: "user", content });
|
|
407
|
+
} else if (msg.role === "assistant") {
|
|
408
|
+
if (msg.toolCalls?.length) {
|
|
409
|
+
messages.push({
|
|
410
|
+
role: "assistant",
|
|
411
|
+
content: msg.content || "",
|
|
412
|
+
toolCalls: msg.toolCalls.map((tc) => ({
|
|
413
|
+
type: "tool-call",
|
|
414
|
+
toolCallId: tc.id,
|
|
415
|
+
toolName: tc.name,
|
|
416
|
+
args: tc.args
|
|
417
|
+
}))
|
|
418
|
+
});
|
|
419
|
+
messages.push({
|
|
420
|
+
role: "tool",
|
|
421
|
+
content: msg.toolCalls.map((tc) => ({
|
|
422
|
+
type: "tool-result",
|
|
423
|
+
toolCallId: tc.id,
|
|
424
|
+
toolName: tc.name,
|
|
425
|
+
output: tc.result
|
|
426
|
+
}))
|
|
427
|
+
});
|
|
428
|
+
} else {
|
|
429
|
+
messages.push({ role: "assistant", content: msg.content });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
328
432
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
Assistant:`;
|
|
332
|
-
return prompt;
|
|
433
|
+
return messages;
|
|
333
434
|
}
|
|
334
435
|
}
|
|
335
436
|
function createChatService(config) {
|
|
@@ -341,6 +442,17 @@ import {
|
|
|
341
442
|
createProvider
|
|
342
443
|
} from "@contractspec/lib.ai-providers";
|
|
343
444
|
"use client";
|
|
445
|
+
function toolsToToolSet(defs) {
|
|
446
|
+
const result = {};
|
|
447
|
+
for (const def of defs) {
|
|
448
|
+
result[def.name] = tool({
|
|
449
|
+
description: def.description ?? def.name,
|
|
450
|
+
inputSchema: z.object({}).passthrough(),
|
|
451
|
+
execute: async () => ({})
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
344
456
|
function useChat(options = {}) {
|
|
345
457
|
const {
|
|
346
458
|
provider = "openai",
|
|
@@ -354,7 +466,8 @@ function useChat(options = {}) {
|
|
|
354
466
|
onSend,
|
|
355
467
|
onResponse,
|
|
356
468
|
onError,
|
|
357
|
-
onUsage
|
|
469
|
+
onUsage,
|
|
470
|
+
tools: toolsDefs
|
|
358
471
|
} = options;
|
|
359
472
|
const [messages, setMessages] = React.useState([]);
|
|
360
473
|
const [conversation, setConversation] = React.useState(null);
|
|
@@ -373,9 +486,19 @@ function useChat(options = {}) {
|
|
|
373
486
|
chatServiceRef.current = new ChatService({
|
|
374
487
|
provider: chatProvider,
|
|
375
488
|
systemPrompt,
|
|
376
|
-
onUsage
|
|
489
|
+
onUsage,
|
|
490
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
|
|
377
491
|
});
|
|
378
|
-
}, [
|
|
492
|
+
}, [
|
|
493
|
+
provider,
|
|
494
|
+
mode,
|
|
495
|
+
model,
|
|
496
|
+
apiKey,
|
|
497
|
+
proxyUrl,
|
|
498
|
+
systemPrompt,
|
|
499
|
+
onUsage,
|
|
500
|
+
toolsDefs
|
|
501
|
+
]);
|
|
379
502
|
React.useEffect(() => {
|
|
380
503
|
if (!conversationId || !chatServiceRef.current)
|
|
381
504
|
return;
|
|
@@ -430,13 +553,50 @@ function useChat(options = {}) {
|
|
|
430
553
|
};
|
|
431
554
|
setMessages((prev) => [...prev, assistantMessage]);
|
|
432
555
|
let fullContent = "";
|
|
556
|
+
let fullReasoning = "";
|
|
557
|
+
const toolCallsMap = new Map;
|
|
558
|
+
const sources = [];
|
|
433
559
|
for await (const chunk of result.stream) {
|
|
434
560
|
if (chunk.type === "text" && chunk.content) {
|
|
435
561
|
fullContent += chunk.content;
|
|
436
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
562
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
563
|
+
...m,
|
|
564
|
+
content: fullContent,
|
|
565
|
+
reasoning: fullReasoning || undefined,
|
|
566
|
+
sources: sources.length ? sources : undefined,
|
|
567
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined
|
|
568
|
+
} : m));
|
|
569
|
+
} else if (chunk.type === "reasoning" && chunk.content) {
|
|
570
|
+
fullReasoning += chunk.content;
|
|
571
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, reasoning: fullReasoning } : m));
|
|
572
|
+
} else if (chunk.type === "source" && chunk.source) {
|
|
573
|
+
sources.push(chunk.source);
|
|
574
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, sources: [...sources] } : m));
|
|
575
|
+
} else if (chunk.type === "tool_call" && chunk.toolCall) {
|
|
576
|
+
const tc = chunk.toolCall;
|
|
577
|
+
const chatTc = {
|
|
578
|
+
id: tc.id,
|
|
579
|
+
name: tc.name,
|
|
580
|
+
args: tc.args,
|
|
581
|
+
status: "running"
|
|
582
|
+
};
|
|
583
|
+
toolCallsMap.set(tc.id, chatTc);
|
|
584
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
585
|
+
} else if (chunk.type === "tool_result" && chunk.toolResult) {
|
|
586
|
+
const tr = chunk.toolResult;
|
|
587
|
+
const tc = toolCallsMap.get(tr.toolCallId);
|
|
588
|
+
if (tc) {
|
|
589
|
+
tc.result = tr.result;
|
|
590
|
+
tc.status = "completed";
|
|
591
|
+
}
|
|
592
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
437
593
|
} else if (chunk.type === "done") {
|
|
438
594
|
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
439
595
|
...m,
|
|
596
|
+
content: fullContent,
|
|
597
|
+
reasoning: fullReasoning || undefined,
|
|
598
|
+
sources: sources.length ? sources : undefined,
|
|
599
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
440
600
|
status: "completed",
|
|
441
601
|
usage: chunk.usage,
|
|
442
602
|
updatedAt: new Date
|
|
@@ -498,6 +658,10 @@ function useChat(options = {}) {
|
|
|
498
658
|
abortControllerRef.current?.abort();
|
|
499
659
|
setIsLoading(false);
|
|
500
660
|
}, []);
|
|
661
|
+
const addToolApprovalResponse = React.useCallback((_toolCallId, _result) => {
|
|
662
|
+
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
663
|
+
}, []);
|
|
664
|
+
const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
|
|
501
665
|
return {
|
|
502
666
|
messages,
|
|
503
667
|
conversation,
|
|
@@ -507,7 +671,8 @@ function useChat(options = {}) {
|
|
|
507
671
|
clearConversation,
|
|
508
672
|
setConversationId,
|
|
509
673
|
regenerate,
|
|
510
|
-
stop
|
|
674
|
+
stop,
|
|
675
|
+
...hasApprovalTools && { addToolApprovalResponse }
|
|
511
676
|
};
|
|
512
677
|
}
|
|
513
678
|
// src/presentation/hooks/useProviders.tsx
|
|
@@ -550,7 +715,11 @@ function useProviders() {
|
|
|
550
715
|
refresh: loadProviders
|
|
551
716
|
};
|
|
552
717
|
}
|
|
718
|
+
|
|
719
|
+
// src/presentation/hooks/index.ts
|
|
720
|
+
import { useCompletion } from "@ai-sdk/react";
|
|
553
721
|
export {
|
|
554
722
|
useProviders,
|
|
723
|
+
useCompletion,
|
|
555
724
|
useChat
|
|
556
725
|
};
|