@arcote.tech/arc-chat 0.5.2 → 0.5.5
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/package.json +6 -6
- package/src/aggregates/message.ts +89 -72
- package/src/chat-builder.ts +35 -2
- package/src/index.ts +1 -1
- package/src/listeners/ai-generation-listener.ts +353 -150
- package/src/react/chat-component.tsx +227 -95
- package/src/tools/ask-questions.tsx +38 -19
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect, type ComponentType, createElement, type ReactNode } from "react";
|
|
2
2
|
import type { ChatStreamEvent, ArcToolAny } from "@arcote.tech/arc-ai";
|
|
3
|
-
import type { ChatMessageData, SendMessageOptions } from "@arcote.tech/arc-ds";
|
|
4
|
-
import { Chat, ChatMessage, ChatInputProvider, ChatToolLog } from "@arcote.tech/arc-ds";
|
|
3
|
+
import type { ChatLabels, ChatMessageData, SendMessageOptions } from "@arcote.tech/arc-ds";
|
|
4
|
+
import { Chat, ChatMessage, ChatInputProvider, ChatLabelsProvider, ChatToolLog, useChatLabels } from "@arcote.tech/arc-ds";
|
|
5
5
|
|
|
6
6
|
interface ChatComponentConfig {
|
|
7
7
|
chatName: string;
|
|
8
8
|
tools: ArcToolAny[];
|
|
9
9
|
messageElementName: string;
|
|
10
|
+
/** Show the model selector dropdown in ChatInput. Default true. */
|
|
11
|
+
showModelSelector?: boolean;
|
|
12
|
+
/** Show the web search toggle in ChatInput. Default true. */
|
|
13
|
+
showWebSearch?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Render slot for ChatInput's send button. Receives `onClick` and
|
|
16
|
+
* `disabled` — caller renders its own button (e.g. branded with a logo).
|
|
17
|
+
*/
|
|
18
|
+
renderSendButton?: (props: {
|
|
19
|
+
onClick: () => void;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
}) => ReactNode;
|
|
22
|
+
/** Partial overrides for chat i18n labels. Falls back to English defaults. */
|
|
23
|
+
labels?: Partial<ChatLabels>;
|
|
10
24
|
}
|
|
11
25
|
|
|
12
26
|
type TimelineItem =
|
|
@@ -16,10 +30,19 @@ type TimelineItem =
|
|
|
16
30
|
export function createChatComponent(
|
|
17
31
|
config: ChatComponentConfig,
|
|
18
32
|
): ComponentType<{ scope: any; identifyBy: string }> {
|
|
19
|
-
const {
|
|
33
|
+
const {
|
|
34
|
+
chatName,
|
|
35
|
+
tools,
|
|
36
|
+
messageElementName,
|
|
37
|
+
showModelSelector = true,
|
|
38
|
+
showWebSearch = true,
|
|
39
|
+
renderSendButton,
|
|
40
|
+
labels,
|
|
41
|
+
} = config;
|
|
20
42
|
const toolsMap = new Map(tools.map((t) => [t.name, t]));
|
|
21
43
|
|
|
22
|
-
|
|
44
|
+
function ChatComponentInner({ scope, identifyBy }: { scope: any; identifyBy: string }) {
|
|
45
|
+
const chatLabels = useChatLabels();
|
|
23
46
|
const [timeline, setTimeline] = useState<TimelineItem[]>([]);
|
|
24
47
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
25
48
|
const sessionIdRef = useRef<string | null>(null);
|
|
@@ -57,32 +80,75 @@ export function createChatComponent(
|
|
|
57
80
|
let hasActiveGeneration = false;
|
|
58
81
|
|
|
59
82
|
for (const msg of historyData) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
// System messages are developer-injected priming prompts. They go
|
|
84
|
+
// to the LLM via buildHistory() but must not appear in the user's
|
|
85
|
+
// chat timeline.
|
|
86
|
+
if (msg.role === "system") continue;
|
|
87
|
+
|
|
88
|
+
if (msg.role === "user") {
|
|
66
89
|
items.push({
|
|
67
90
|
type: "message",
|
|
68
91
|
id: msg._id,
|
|
69
|
-
role:
|
|
70
|
-
content: msg.content,
|
|
71
|
-
isStreaming: msg.isGenerating === true,
|
|
92
|
+
role: "user",
|
|
93
|
+
content: msg.content ?? "",
|
|
72
94
|
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (msg.role === "assistant") {
|
|
99
|
+
// Placeholder row created at the start of generation. We track its
|
|
100
|
+
// session for SSE reconnect, but don't render it yet — the loop
|
|
101
|
+
// will save the real assistant row with `blocks` once streaming
|
|
102
|
+
// completes.
|
|
103
|
+
const blocksStr = msg.blocks ?? "";
|
|
104
|
+
if (msg.isGenerating && !blocksStr) {
|
|
105
|
+
if (msg.sessionId) sessionIdRef.current = msg.sessionId;
|
|
106
|
+
hasActiveGeneration = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Walk the assistant's blocks in order — each TextBlock becomes a
|
|
111
|
+
// message item, each ToolCallBlock becomes a tool item paired with
|
|
112
|
+
// its result row.
|
|
113
|
+
const blocks =
|
|
114
|
+
(tryParseJson(blocksStr) as Array<
|
|
115
|
+
| { type: "text"; text: string }
|
|
116
|
+
| {
|
|
117
|
+
type: "tool_call";
|
|
118
|
+
id: string;
|
|
119
|
+
name: string;
|
|
120
|
+
arguments: Record<string, unknown>;
|
|
121
|
+
}
|
|
122
|
+
>) ?? [];
|
|
123
|
+
|
|
124
|
+
let blockIdx = 0;
|
|
125
|
+
for (const block of blocks) {
|
|
126
|
+
if (block.type === "text") {
|
|
127
|
+
if (block.text) {
|
|
128
|
+
items.push({
|
|
129
|
+
type: "message",
|
|
130
|
+
id: `${msg._id}_b${blockIdx}`,
|
|
131
|
+
role: "assistant",
|
|
132
|
+
content: block.text,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
const result = resultMap.get(block.id);
|
|
137
|
+
items.push({
|
|
138
|
+
type: "tool",
|
|
139
|
+
id: `${msg._id}_b${blockIdx}`,
|
|
140
|
+
toolCallId: block.id,
|
|
141
|
+
toolName: block.name,
|
|
142
|
+
params: block.arguments,
|
|
143
|
+
result: result ? tryParseJson(result.content) : undefined,
|
|
144
|
+
calling: !resultIds.has(block.id),
|
|
145
|
+
error: result?.isError ? result.content : undefined,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
blockIdx++;
|
|
149
|
+
}
|
|
150
|
+
|
|
73
151
|
if (msg.isGenerating === true) hasActiveGeneration = true;
|
|
74
|
-
} else if (msg.role === "tool_call" && msg.toolCallId) {
|
|
75
|
-
const result = resultMap.get(msg.toolCallId);
|
|
76
|
-
items.push({
|
|
77
|
-
type: "tool",
|
|
78
|
-
id: msg._id,
|
|
79
|
-
toolCallId: msg.toolCallId,
|
|
80
|
-
toolName: msg.toolName ?? "",
|
|
81
|
-
params: tryParseJson(msg.content) as Record<string, unknown>,
|
|
82
|
-
result: result ? tryParseJson(result.content) : undefined,
|
|
83
|
-
calling: !resultIds.has(msg.toolCallId),
|
|
84
|
-
error: result?.isError ? result.content : undefined,
|
|
85
|
-
});
|
|
86
152
|
}
|
|
87
153
|
}
|
|
88
154
|
|
|
@@ -95,34 +161,63 @@ export function createChatComponent(
|
|
|
95
161
|
// ─── SSE event processing ───────────────────────────────────
|
|
96
162
|
const processEvent = useCallback(
|
|
97
163
|
async (event: ChatStreamEvent) => {
|
|
98
|
-
const assistantId = currentAssistantIdRef.current;
|
|
99
|
-
|
|
100
164
|
switch (event.type) {
|
|
101
165
|
case "content_delta":
|
|
102
|
-
if (event.content
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
166
|
+
if (!event.content) break;
|
|
167
|
+
// Append to the trailing assistant text bubble. If there isn't
|
|
168
|
+
// one (e.g. just finished a tool, or no streaming bubble yet),
|
|
169
|
+
// create a new one. This produces correctly interleaved
|
|
170
|
+
// text/tool/text/tool ordering during streaming.
|
|
171
|
+
setTimeline((prev) => {
|
|
172
|
+
const last = prev[prev.length - 1];
|
|
173
|
+
if (
|
|
174
|
+
last &&
|
|
175
|
+
last.type === "message" &&
|
|
176
|
+
last.role === "assistant" &&
|
|
177
|
+
last.isStreaming
|
|
178
|
+
) {
|
|
179
|
+
return prev.map((item, i) =>
|
|
180
|
+
i === prev.length - 1 && item.type === "message"
|
|
106
181
|
? { ...item, content: item.content + event.content }
|
|
107
182
|
: item,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const newId = `assistant_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
186
|
+
currentAssistantIdRef.current = newId;
|
|
187
|
+
return [
|
|
188
|
+
...prev,
|
|
189
|
+
{
|
|
190
|
+
type: "message",
|
|
191
|
+
id: newId,
|
|
192
|
+
role: "assistant",
|
|
193
|
+
content: event.content!,
|
|
194
|
+
isStreaming: true,
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
});
|
|
111
198
|
break;
|
|
112
199
|
|
|
113
200
|
case "server_tool_start":
|
|
114
201
|
if (event.toolCall) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
202
|
+
// Finalize any streaming text bubble before the tool — next
|
|
203
|
+
// content_delta will start a fresh bubble after the tool.
|
|
204
|
+
setTimeline((prev) => {
|
|
205
|
+
const next = prev.map((item) =>
|
|
206
|
+
item.type === "message" && item.isStreaming
|
|
207
|
+
? { ...item, isStreaming: false }
|
|
208
|
+
: item,
|
|
209
|
+
);
|
|
210
|
+
next.push({
|
|
118
211
|
type: "tool",
|
|
119
212
|
id: `tc_${event.toolCall!.id}`,
|
|
120
213
|
toolCallId: event.toolCall!.id,
|
|
121
214
|
toolName: event.toolCall!.name,
|
|
122
215
|
params: event.toolCall!.arguments ?? {},
|
|
123
216
|
calling: true,
|
|
124
|
-
}
|
|
125
|
-
|
|
217
|
+
});
|
|
218
|
+
return next;
|
|
219
|
+
});
|
|
220
|
+
currentAssistantIdRef.current = null;
|
|
126
221
|
}
|
|
127
222
|
break;
|
|
128
223
|
|
|
@@ -145,48 +240,78 @@ export function createChatComponent(
|
|
|
145
240
|
|
|
146
241
|
case "interactive_tool_request":
|
|
147
242
|
if (event.toolCalls) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
243
|
+
// Finalize current text bubble and append the interactive tool
|
|
244
|
+
// items. After this, the loop pauses until userResponded.
|
|
245
|
+
setTimeline((prev) => {
|
|
246
|
+
const next = prev.map((item) =>
|
|
247
|
+
item.type === "message" && item.isStreaming
|
|
248
|
+
? { ...item, isStreaming: false }
|
|
249
|
+
: item,
|
|
250
|
+
);
|
|
251
|
+
for (const tc of event.toolCalls!) {
|
|
252
|
+
next.push({
|
|
253
|
+
type: "tool",
|
|
254
|
+
id: `tc_${tc.id}`,
|
|
255
|
+
toolCallId: tc.id,
|
|
256
|
+
toolName: tc.name,
|
|
257
|
+
params: tc.arguments ?? {},
|
|
258
|
+
calling: true,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return next;
|
|
262
|
+
});
|
|
263
|
+
currentAssistantIdRef.current = null;
|
|
159
264
|
}
|
|
160
265
|
break;
|
|
161
266
|
|
|
162
267
|
case "done":
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
268
|
+
// Mark any trailing streaming bubble as done.
|
|
269
|
+
setTimeline((prev) =>
|
|
270
|
+
prev.map((item) =>
|
|
271
|
+
item.type === "message" && item.isStreaming
|
|
272
|
+
? { ...item, isStreaming: false }
|
|
273
|
+
: item,
|
|
274
|
+
),
|
|
275
|
+
);
|
|
172
276
|
setIsStreaming(false);
|
|
277
|
+
currentAssistantIdRef.current = null;
|
|
173
278
|
break;
|
|
174
279
|
|
|
175
280
|
case "error":
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
281
|
+
setTimeline((prev) => {
|
|
282
|
+
const last = prev[prev.length - 1];
|
|
283
|
+
if (
|
|
284
|
+
last &&
|
|
285
|
+
last.type === "message" &&
|
|
286
|
+
last.role === "assistant" &&
|
|
287
|
+
last.isStreaming
|
|
288
|
+
) {
|
|
289
|
+
return prev.map((item, i) =>
|
|
290
|
+
i === prev.length - 1 && item.type === "message"
|
|
291
|
+
? {
|
|
292
|
+
...item,
|
|
293
|
+
content: item.content || event.error || chatLabels.errorLabel,
|
|
294
|
+
isStreaming: false,
|
|
295
|
+
}
|
|
181
296
|
: item,
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return [
|
|
300
|
+
...prev,
|
|
301
|
+
{
|
|
302
|
+
type: "message",
|
|
303
|
+
id: `error_${Date.now()}`,
|
|
304
|
+
role: "assistant",
|
|
305
|
+
content: event.error || chatLabels.errorLabel,
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
});
|
|
185
309
|
setIsStreaming(false);
|
|
310
|
+
currentAssistantIdRef.current = null;
|
|
186
311
|
break;
|
|
187
312
|
}
|
|
188
313
|
},
|
|
189
|
-
[],
|
|
314
|
+
[chatLabels],
|
|
190
315
|
);
|
|
191
316
|
|
|
192
317
|
// ─── Send message ───────────────────────────────────────────
|
|
@@ -220,12 +345,11 @@ export function createChatComponent(
|
|
|
220
345
|
|
|
221
346
|
if (!response.ok) throw new Error(`Stream failed: ${response.status}`);
|
|
222
347
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
]);
|
|
348
|
+
// Don't pre-create an assistant bubble here — the SSE handler will
|
|
349
|
+
// create one lazily on the first content_delta. This way, if the
|
|
350
|
+
// model jumps straight to a tool call (no text), we don't render an
|
|
351
|
+
// empty bubble.
|
|
352
|
+
currentAssistantIdRef.current = null;
|
|
229
353
|
|
|
230
354
|
const reader = response.body!.getReader();
|
|
231
355
|
const decoder = new TextDecoder();
|
|
@@ -260,17 +384,17 @@ export function createChatComponent(
|
|
|
260
384
|
sessionIdRef.current = null;
|
|
261
385
|
currentAssistantIdRef.current = null;
|
|
262
386
|
} catch (err) {
|
|
263
|
-
const errorMsg = err instanceof Error ? err.message :
|
|
387
|
+
const errorMsg = err instanceof Error ? err.message : chatLabels.errorLabel;
|
|
264
388
|
setTimeline((prev) => [
|
|
265
389
|
...prev,
|
|
266
|
-
{ type: "message", id: `error_${Date.now()}`, role: "assistant", content:
|
|
390
|
+
{ type: "message", id: `error_${Date.now()}`, role: "assistant", content: `${chatLabels.errorLabel}: ${errorMsg}` },
|
|
267
391
|
]);
|
|
268
392
|
setIsStreaming(false);
|
|
269
393
|
sessionIdRef.current = null;
|
|
270
394
|
currentAssistantIdRef.current = null;
|
|
271
395
|
}
|
|
272
396
|
},
|
|
273
|
-
[isStreaming, scopeId, messageMutations, processEvent],
|
|
397
|
+
[isStreaming, scopeId, messageMutations, processEvent, chatLabels],
|
|
274
398
|
);
|
|
275
399
|
|
|
276
400
|
// ─── Build messages for Chat DS (only user/assistant) ───────
|
|
@@ -292,7 +416,7 @@ export function createChatComponent(
|
|
|
292
416
|
return createElement(ChatToolLog, {
|
|
293
417
|
calling: item.calling,
|
|
294
418
|
label: item.toolName,
|
|
295
|
-
}, item.calling ?
|
|
419
|
+
}, item.calling ? chatLabels.toolCallingLabel : item.error ?? chatLabels.toolDoneLabel);
|
|
296
420
|
}
|
|
297
421
|
|
|
298
422
|
if (tool.isServerTool) {
|
|
@@ -328,16 +452,12 @@ export function createChatComponent(
|
|
|
328
452
|
const { sessionId: newSessionId } = respondResult as { sessionId: string };
|
|
329
453
|
if (!newSessionId) return;
|
|
330
454
|
|
|
331
|
-
// Connect to new SSE stream for resumed generation
|
|
455
|
+
// Connect to new SSE stream for resumed generation. Don't pre-create
|
|
456
|
+
// an assistant bubble — the SSE handler creates one lazily on the
|
|
457
|
+
// first content_delta (same pattern as handleSend).
|
|
332
458
|
sessionIdRef.current = newSessionId;
|
|
333
459
|
setIsStreaming(true);
|
|
334
|
-
|
|
335
|
-
const assistantMsgId = `assistant_${Date.now()}`;
|
|
336
|
-
currentAssistantIdRef.current = assistantMsgId;
|
|
337
|
-
setTimeline((prev) => [
|
|
338
|
-
...prev,
|
|
339
|
-
{ type: "message", id: assistantMsgId, role: "assistant", content: "", isStreaming: true },
|
|
340
|
-
]);
|
|
460
|
+
currentAssistantIdRef.current = null;
|
|
341
461
|
|
|
342
462
|
try {
|
|
343
463
|
const streamUrl = `/route/chat/${chatName}/stream/${newSessionId}`;
|
|
@@ -374,13 +494,15 @@ export function createChatComponent(
|
|
|
374
494
|
} catch {}
|
|
375
495
|
}
|
|
376
496
|
} catch (err) {
|
|
377
|
-
setTimeline((prev) =>
|
|
378
|
-
prev
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
497
|
+
setTimeline((prev) => [
|
|
498
|
+
...prev,
|
|
499
|
+
{
|
|
500
|
+
type: "message",
|
|
501
|
+
id: `error_${Date.now()}`,
|
|
502
|
+
role: "assistant",
|
|
503
|
+
content: `${chatLabels.errorLabel}: ${err instanceof Error ? err.message : chatLabels.errorLabel}`,
|
|
504
|
+
},
|
|
505
|
+
]);
|
|
384
506
|
} finally {
|
|
385
507
|
setIsStreaming(false);
|
|
386
508
|
sessionIdRef.current = null;
|
|
@@ -438,13 +560,23 @@ export function createChatComponent(
|
|
|
438
560
|
),
|
|
439
561
|
createElement(Chat, {
|
|
440
562
|
messages: [],
|
|
441
|
-
models: [{ value: "gpt-5.4-
|
|
442
|
-
defaultModel: "gpt-5.4-
|
|
563
|
+
models: [{ value: "gpt-5.4-mini", label: "GPT-5.4 Mini" }],
|
|
564
|
+
defaultModel: "gpt-5.4-mini",
|
|
443
565
|
onSend: handleSend,
|
|
566
|
+
showModelSelector,
|
|
567
|
+
showWebSearch,
|
|
568
|
+
renderSendButton,
|
|
444
569
|
disabled: isStreaming || hasWaitingInteractive,
|
|
445
570
|
}),
|
|
446
571
|
),
|
|
447
572
|
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return function ChatComponent(props: { scope: any; identifyBy: string }) {
|
|
576
|
+
return createElement(
|
|
577
|
+
ChatLabelsProvider,
|
|
578
|
+
{ labels, children: createElement(ChatComponentInner, props) },
|
|
579
|
+
);
|
|
448
580
|
};
|
|
449
581
|
}
|
|
450
582
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import { tool } from "@arcote.tech/arc-ai";
|
|
3
3
|
import { string, array, object } from "@arcote.tech/arc";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ChatToolQuestion,
|
|
6
|
+
QuestionTabs,
|
|
7
|
+
useChatInput,
|
|
8
|
+
useChatLabels,
|
|
9
|
+
} from "@arcote.tech/arc-ds";
|
|
5
10
|
import type { Question, QuestionAnswers } from "@arcote.tech/arc-ds";
|
|
6
11
|
|
|
7
12
|
type AskQuestionsParams = {
|
|
8
|
-
readonly comment: string;
|
|
9
13
|
readonly questions: readonly {
|
|
10
14
|
readonly id: string;
|
|
11
15
|
readonly label: string;
|
|
@@ -14,6 +18,13 @@ type AskQuestionsParams = {
|
|
|
14
18
|
}[];
|
|
15
19
|
};
|
|
16
20
|
|
|
21
|
+
/** Result shape returned by respond() — includes a discuss flag. */
|
|
22
|
+
export interface AskQuestionsResult {
|
|
23
|
+
answers: QuestionAnswers;
|
|
24
|
+
/** User clicked "Continue discussion" instead of submitting answers. */
|
|
25
|
+
wantsToDiscuss: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
function AskQuestionsView({
|
|
18
29
|
params,
|
|
19
30
|
respond,
|
|
@@ -21,11 +32,12 @@ function AskQuestionsView({
|
|
|
21
32
|
result,
|
|
22
33
|
}: {
|
|
23
34
|
params: AskQuestionsParams;
|
|
24
|
-
respond: (result:
|
|
35
|
+
respond: (result: AskQuestionsResult) => void;
|
|
25
36
|
calling: boolean;
|
|
26
|
-
result?:
|
|
37
|
+
result?: AskQuestionsResult;
|
|
27
38
|
}) {
|
|
28
39
|
const { registerInputOverride, clearInputOverride } = useChatInput();
|
|
40
|
+
const { answerBelowLabel } = useChatLabels();
|
|
29
41
|
|
|
30
42
|
useEffect(() => {
|
|
31
43
|
if (calling) {
|
|
@@ -39,8 +51,8 @@ function AskQuestionsView({
|
|
|
39
51
|
registerInputOverride(
|
|
40
52
|
<QuestionTabs
|
|
41
53
|
questions={questions}
|
|
42
|
-
onSubmit={(answers) => {
|
|
43
|
-
respond(answers);
|
|
54
|
+
onSubmit={(answers, { wantsToDiscuss }) => {
|
|
55
|
+
respond({ answers, wantsToDiscuss });
|
|
44
56
|
clearInputOverride();
|
|
45
57
|
}}
|
|
46
58
|
/>,
|
|
@@ -52,14 +64,14 @@ function AskQuestionsView({
|
|
|
52
64
|
|
|
53
65
|
// Answered — show summary
|
|
54
66
|
if (!calling && result) {
|
|
55
|
-
const answers = result
|
|
67
|
+
const { answers } = result;
|
|
68
|
+
const entries = Object.entries(answers) as Array<
|
|
69
|
+
[string, { selected?: string[]; text?: string }]
|
|
70
|
+
>;
|
|
56
71
|
return (
|
|
57
72
|
<ChatToolQuestion calling={false}>
|
|
58
|
-
{params.comment && (
|
|
59
|
-
<p className="text-sm mb-2">{params.comment}</p>
|
|
60
|
-
)}
|
|
61
73
|
<div className="space-y-1.5">
|
|
62
|
-
{
|
|
74
|
+
{entries.map(([questionId, answer]) => {
|
|
63
75
|
const question = params.questions.find((q) => q.id === questionId);
|
|
64
76
|
const selected = answer?.selected?.length ? answer.selected.join(", ") : "";
|
|
65
77
|
const text = answer?.text || "";
|
|
@@ -79,22 +91,29 @@ function AskQuestionsView({
|
|
|
79
91
|
// Waiting for response
|
|
80
92
|
return (
|
|
81
93
|
<ChatToolQuestion calling={true}>
|
|
82
|
-
|
|
83
|
-
<p className="text-sm mb-1">{params.comment}</p>
|
|
84
|
-
)}
|
|
85
|
-
<p className="text-xs text-muted-foreground">
|
|
86
|
-
Odpowiedz na pytania poniżej
|
|
87
|
-
</p>
|
|
94
|
+
<p className="text-xs text-muted-foreground">{answerBelowLabel}</p>
|
|
88
95
|
</ChatToolQuestion>
|
|
89
96
|
);
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
export const askQuestions = tool("askQuestions")
|
|
93
100
|
.description(
|
|
94
|
-
"Zadaj użytkownikowi
|
|
101
|
+
"Zadaj użytkownikowi **zamknięte** pytanie z predefiniowanymi opcjami — " +
|
|
102
|
+
"gdy wiesz jakich konkretnie odpowiedzi się spodziewasz i możesz je " +
|
|
103
|
+
"wymienić jako 3–6 opcji (np. preferowany ton, zakres tematyczny, " +
|
|
104
|
+
"formalność). Każde pytanie ma id, label, description i tablicę opcji. " +
|
|
105
|
+
"Komentarz/insight napisz jako zwykły tekst PRZED wywołaniem toola — " +
|
|
106
|
+
"nie wkładaj komentarzy do parametrów.\n\n" +
|
|
107
|
+
"NIE używaj do otwartych pytań narracyjnych (np. 'opowiedz o swoim " +
|
|
108
|
+
"zespole', 'opisz przełomowy moment') — na te pytaj zwykłym tekstem " +
|
|
109
|
+
"w jednym zdaniu, użytkownik odpowie własnymi słowami.\n\n" +
|
|
110
|
+
"Tool result: `{ answers, wantsToDiscuss }`. Gdy `wantsToDiscuss === true` " +
|
|
111
|
+
"— user kliknął 'Kontynuuj rozmowę' zamiast odpowiadać. W TEJ turze NIE " +
|
|
112
|
+
"wolno wołać żadnego toola (ani askQuestions ani innego). Zwróć zwykły " +
|
|
113
|
+
"tekst: krótkie podsumowanie (2-3 zdania) + jedno open-ended pytanie " +
|
|
114
|
+
"w prozie.",
|
|
95
115
|
)
|
|
96
116
|
.withParams({
|
|
97
|
-
comment: string(),
|
|
98
117
|
questions: array(
|
|
99
118
|
object({
|
|
100
119
|
id: string(),
|