@arcote.tech/arc-chat 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/aggregates/message.ts +272 -43
- package/src/chat-builder.ts +243 -83
- package/src/index.ts +4 -22
- package/src/listeners/ai-generation-listener.ts +322 -249
- package/src/react/chat-component.tsx +457 -0
- package/src/react/index.ts +2 -3
- package/src/react/use-chat.ts +1 -260
- package/src/routes/chat-stream-route.ts +4 -10
- package/src/streaming/stream-registry.ts +92 -124
- package/src/tools/ask-questions.tsx +107 -0
- package/src/routes/tool-results-route.ts +0 -49
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect, type ComponentType, createElement, type ReactNode } from "react";
|
|
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";
|
|
5
|
+
|
|
6
|
+
interface ChatComponentConfig {
|
|
7
|
+
chatName: string;
|
|
8
|
+
tools: ArcToolAny[];
|
|
9
|
+
messageElementName: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type TimelineItem =
|
|
13
|
+
| { type: "message"; id: string; role: "user" | "assistant"; content: string; isStreaming?: boolean }
|
|
14
|
+
| { type: "tool"; id: string; toolCallId: string; toolName: string; params: Record<string, unknown>; result?: unknown; calling: boolean; error?: string };
|
|
15
|
+
|
|
16
|
+
export function createChatComponent(
|
|
17
|
+
config: ChatComponentConfig,
|
|
18
|
+
): ComponentType<{ scope: any; identifyBy: string }> {
|
|
19
|
+
const { chatName, tools, messageElementName } = config;
|
|
20
|
+
const toolsMap = new Map(tools.map((t) => [t.name, t]));
|
|
21
|
+
|
|
22
|
+
return function ChatComponent({ scope, identifyBy }: { scope: any; identifyBy: string }) {
|
|
23
|
+
const [timeline, setTimeline] = useState<TimelineItem[]>([]);
|
|
24
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
25
|
+
const sessionIdRef = useRef<string | null>(null);
|
|
26
|
+
const currentAssistantIdRef = useRef<string | null>(null);
|
|
27
|
+
const lastHistoryLenRef = useRef(0);
|
|
28
|
+
|
|
29
|
+
const queries = scope.useQuery();
|
|
30
|
+
const mutations = scope.useMutation();
|
|
31
|
+
const messageQueries = queries[messageElementName];
|
|
32
|
+
const messageMutations = mutations[messageElementName];
|
|
33
|
+
|
|
34
|
+
const scopeId = identifyBy;
|
|
35
|
+
const historyResult = scopeId && messageQueries?.getByScope
|
|
36
|
+
? messageQueries.getByScope({ scopeId })
|
|
37
|
+
: [undefined, false];
|
|
38
|
+
const historyData = historyResult?.[0];
|
|
39
|
+
const historyLen = historyData?.length ?? 0;
|
|
40
|
+
|
|
41
|
+
// ─── Restore timeline from DB history ───────────────────────
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (isStreaming || !historyData || historyLen === 0) return;
|
|
44
|
+
if (historyLen === lastHistoryLenRef.current) return;
|
|
45
|
+
lastHistoryLenRef.current = historyLen;
|
|
46
|
+
|
|
47
|
+
const resultIds = new Set<string>();
|
|
48
|
+
const resultMap = new Map<string, { content: string; isError?: boolean }>();
|
|
49
|
+
for (const msg of historyData) {
|
|
50
|
+
if (msg.role === "tool_result" && msg.toolCallId) {
|
|
51
|
+
resultIds.add(msg.toolCallId);
|
|
52
|
+
resultMap.set(msg.toolCallId, { content: msg.content, isError: msg.isError });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const items: TimelineItem[] = [];
|
|
57
|
+
let hasActiveGeneration = false;
|
|
58
|
+
|
|
59
|
+
for (const msg of historyData) {
|
|
60
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
61
|
+
if (msg.isGenerating && !msg.content) {
|
|
62
|
+
if (msg.sessionId) sessionIdRef.current = msg.sessionId;
|
|
63
|
+
hasActiveGeneration = msg.isGenerating === true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
items.push({
|
|
67
|
+
type: "message",
|
|
68
|
+
id: msg._id,
|
|
69
|
+
role: msg.role,
|
|
70
|
+
content: msg.content,
|
|
71
|
+
isStreaming: msg.isGenerating === true,
|
|
72
|
+
});
|
|
73
|
+
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
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setTimeline(items);
|
|
90
|
+
if (!isStreaming && hasActiveGeneration) {
|
|
91
|
+
setIsStreaming(true);
|
|
92
|
+
}
|
|
93
|
+
}, [historyLen, isStreaming]);
|
|
94
|
+
|
|
95
|
+
// ─── SSE event processing ───────────────────────────────────
|
|
96
|
+
const processEvent = useCallback(
|
|
97
|
+
async (event: ChatStreamEvent) => {
|
|
98
|
+
const assistantId = currentAssistantIdRef.current;
|
|
99
|
+
|
|
100
|
+
switch (event.type) {
|
|
101
|
+
case "content_delta":
|
|
102
|
+
if (event.content && assistantId) {
|
|
103
|
+
setTimeline((prev) =>
|
|
104
|
+
prev.map((item) =>
|
|
105
|
+
item.type === "message" && item.id === assistantId
|
|
106
|
+
? { ...item, content: item.content + event.content }
|
|
107
|
+
: item,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case "server_tool_start":
|
|
114
|
+
if (event.toolCall) {
|
|
115
|
+
setTimeline((prev) => [
|
|
116
|
+
...prev,
|
|
117
|
+
{
|
|
118
|
+
type: "tool",
|
|
119
|
+
id: `tc_${event.toolCall!.id}`,
|
|
120
|
+
toolCallId: event.toolCall!.id,
|
|
121
|
+
toolName: event.toolCall!.name,
|
|
122
|
+
params: event.toolCall!.arguments ?? {},
|
|
123
|
+
calling: true,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case "server_tool_result":
|
|
130
|
+
if (event.toolResult) {
|
|
131
|
+
setTimeline((prev) =>
|
|
132
|
+
prev.map((item) =>
|
|
133
|
+
item.type === "tool" && item.toolCallId === event.toolResult!.toolCallId
|
|
134
|
+
? {
|
|
135
|
+
...item,
|
|
136
|
+
result: tryParseJson(event.toolResult!.content),
|
|
137
|
+
calling: false,
|
|
138
|
+
error: event.toolResult!.isError ? event.toolResult!.content : undefined,
|
|
139
|
+
}
|
|
140
|
+
: item,
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case "interactive_tool_request":
|
|
147
|
+
if (event.toolCalls) {
|
|
148
|
+
setTimeline((prev) => [
|
|
149
|
+
...prev,
|
|
150
|
+
...event.toolCalls!.map((tc) => ({
|
|
151
|
+
type: "tool" as const,
|
|
152
|
+
id: `tc_${tc.id}`,
|
|
153
|
+
toolCallId: tc.id,
|
|
154
|
+
toolName: tc.name,
|
|
155
|
+
params: tc.arguments ?? {},
|
|
156
|
+
calling: true,
|
|
157
|
+
})),
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case "done":
|
|
163
|
+
if (assistantId) {
|
|
164
|
+
setTimeline((prev) =>
|
|
165
|
+
prev.map((item) =>
|
|
166
|
+
item.type === "message" && item.id === assistantId
|
|
167
|
+
? { ...item, isStreaming: false }
|
|
168
|
+
: item,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
setIsStreaming(false);
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
case "error":
|
|
176
|
+
if (assistantId) {
|
|
177
|
+
setTimeline((prev) =>
|
|
178
|
+
prev.map((item) =>
|
|
179
|
+
item.type === "message" && item.id === assistantId
|
|
180
|
+
? { ...item, content: item.content || event.error || "Error", isStreaming: false }
|
|
181
|
+
: item,
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
setIsStreaming(false);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
[],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// ─── Send message ───────────────────────────────────────────
|
|
193
|
+
const handleSend = useCallback(
|
|
194
|
+
async (content: string, options: SendMessageOptions) => {
|
|
195
|
+
if (isStreaming || !scopeId) return;
|
|
196
|
+
|
|
197
|
+
setIsStreaming(true);
|
|
198
|
+
|
|
199
|
+
const userMsgId = `user_${Date.now()}`;
|
|
200
|
+
setTimeline((prev) => [
|
|
201
|
+
...prev,
|
|
202
|
+
{ type: "message", id: userMsgId, role: "user", content },
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const sendResult = await messageMutations.sendMessage({
|
|
207
|
+
scopeId,
|
|
208
|
+
content,
|
|
209
|
+
model: options.model,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const { sessionId } = sendResult as { sessionId: string; messageId: string };
|
|
213
|
+
sessionIdRef.current = sessionId;
|
|
214
|
+
|
|
215
|
+
const streamUrl = `/route/chat/${chatName}/stream/${sessionId}`;
|
|
216
|
+
const response = await fetch(streamUrl, {
|
|
217
|
+
credentials: "include",
|
|
218
|
+
headers: { Accept: "text/event-stream" },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!response.ok) throw new Error(`Stream failed: ${response.status}`);
|
|
222
|
+
|
|
223
|
+
const assistantMsgId = `assistant_${Date.now()}`;
|
|
224
|
+
currentAssistantIdRef.current = assistantMsgId;
|
|
225
|
+
setTimeline((prev) => [
|
|
226
|
+
...prev,
|
|
227
|
+
{ type: "message", id: assistantMsgId, role: "assistant", content: "", isStreaming: true },
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
const reader = response.body!.getReader();
|
|
231
|
+
const decoder = new TextDecoder();
|
|
232
|
+
let partialLine = "";
|
|
233
|
+
|
|
234
|
+
while (true) {
|
|
235
|
+
const { value, done } = await reader.read();
|
|
236
|
+
if (done) break;
|
|
237
|
+
|
|
238
|
+
const text = partialLine + decoder.decode(value, { stream: true });
|
|
239
|
+
const lines = text.split("\n");
|
|
240
|
+
partialLine = lines.pop() ?? "";
|
|
241
|
+
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
if (line.startsWith("data: ")) {
|
|
244
|
+
try {
|
|
245
|
+
const event = JSON.parse(line.slice(6)) as ChatStreamEvent;
|
|
246
|
+
await processEvent(event);
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (partialLine.startsWith("data: ")) {
|
|
253
|
+
try {
|
|
254
|
+
const event = JSON.parse(partialLine.slice(6)) as ChatStreamEvent;
|
|
255
|
+
await processEvent(event);
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
setIsStreaming(false);
|
|
260
|
+
sessionIdRef.current = null;
|
|
261
|
+
currentAssistantIdRef.current = null;
|
|
262
|
+
} catch (err) {
|
|
263
|
+
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
264
|
+
setTimeline((prev) => [
|
|
265
|
+
...prev,
|
|
266
|
+
{ type: "message", id: `error_${Date.now()}`, role: "assistant", content: `Error: ${errorMsg}` },
|
|
267
|
+
]);
|
|
268
|
+
setIsStreaming(false);
|
|
269
|
+
sessionIdRef.current = null;
|
|
270
|
+
currentAssistantIdRef.current = null;
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
[isStreaming, scopeId, messageMutations, processEvent],
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// ─── Build messages for Chat DS (only user/assistant) ───────
|
|
277
|
+
const chatMessages: ChatMessageData[] = timeline
|
|
278
|
+
.filter((item): item is TimelineItem & { type: "message" } => item.type === "message")
|
|
279
|
+
.map((item) => ({
|
|
280
|
+
id: item.id,
|
|
281
|
+
role: item.role,
|
|
282
|
+
content: item.content,
|
|
283
|
+
isStreaming: item.isStreaming,
|
|
284
|
+
}));
|
|
285
|
+
|
|
286
|
+
// ─── Render tool view ───────────────────────────────────────
|
|
287
|
+
const renderToolItem = (item: TimelineItem & { type: "tool" }) => {
|
|
288
|
+
const tool = toolsMap.get(item.toolName);
|
|
289
|
+
const ViewComponent = tool?.viewComponent;
|
|
290
|
+
|
|
291
|
+
if (!ViewComponent) {
|
|
292
|
+
return createElement(ChatToolLog, {
|
|
293
|
+
calling: item.calling,
|
|
294
|
+
label: item.toolName,
|
|
295
|
+
}, item.calling ? "Wykonuję..." : item.error ?? "Gotowe");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (tool.isServerTool) {
|
|
299
|
+
return createElement(ViewComponent, {
|
|
300
|
+
params: item.params,
|
|
301
|
+
result: item.result,
|
|
302
|
+
calling: item.calling,
|
|
303
|
+
error: item.error,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Interactive tool
|
|
308
|
+
const respond = async (result: unknown) => {
|
|
309
|
+
if (!scopeId) return;
|
|
310
|
+
|
|
311
|
+
// Update timeline immediately
|
|
312
|
+
setTimeline((prev) =>
|
|
313
|
+
prev.map((t) =>
|
|
314
|
+
t.type === "tool" && t.toolCallId === item.toolCallId
|
|
315
|
+
? { ...t, calling: false, result }
|
|
316
|
+
: t,
|
|
317
|
+
),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Send response — returns new sessionId for SSE
|
|
321
|
+
const respondResult = await messageMutations.respondToTool({
|
|
322
|
+
scopeId,
|
|
323
|
+
toolCallId: item.toolCallId,
|
|
324
|
+
toolName: item.toolName,
|
|
325
|
+
result: JSON.stringify(result),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const { sessionId: newSessionId } = respondResult as { sessionId: string };
|
|
329
|
+
if (!newSessionId) return;
|
|
330
|
+
|
|
331
|
+
// Connect to new SSE stream for resumed generation
|
|
332
|
+
sessionIdRef.current = newSessionId;
|
|
333
|
+
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
|
+
]);
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const streamUrl = `/route/chat/${chatName}/stream/${newSessionId}`;
|
|
344
|
+
const response = await fetch(streamUrl, {
|
|
345
|
+
credentials: "include",
|
|
346
|
+
headers: { Accept: "text/event-stream" },
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (!response.ok) throw new Error(`Stream failed: ${response.status}`);
|
|
350
|
+
|
|
351
|
+
const reader = response.body!.getReader();
|
|
352
|
+
const decoder = new TextDecoder();
|
|
353
|
+
let partialLine = "";
|
|
354
|
+
|
|
355
|
+
while (true) {
|
|
356
|
+
const { value, done } = await reader.read();
|
|
357
|
+
if (done) break;
|
|
358
|
+
const text = partialLine + decoder.decode(value, { stream: true });
|
|
359
|
+
const lines = text.split("\n");
|
|
360
|
+
partialLine = lines.pop() ?? "";
|
|
361
|
+
for (const line of lines) {
|
|
362
|
+
if (line.startsWith("data: ")) {
|
|
363
|
+
try {
|
|
364
|
+
const evt = JSON.parse(line.slice(6)) as ChatStreamEvent;
|
|
365
|
+
await processEvent(evt);
|
|
366
|
+
} catch {}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (partialLine.startsWith("data: ")) {
|
|
371
|
+
try {
|
|
372
|
+
const evt = JSON.parse(partialLine.slice(6)) as ChatStreamEvent;
|
|
373
|
+
await processEvent(evt);
|
|
374
|
+
} catch {}
|
|
375
|
+
}
|
|
376
|
+
} catch (err) {
|
|
377
|
+
setTimeline((prev) =>
|
|
378
|
+
prev.map((t) =>
|
|
379
|
+
t.type === "message" && t.id === assistantMsgId
|
|
380
|
+
? { ...t, content: `Error: ${err instanceof Error ? err.message : "Unknown"}`, isStreaming: false }
|
|
381
|
+
: t,
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
} finally {
|
|
385
|
+
setIsStreaming(false);
|
|
386
|
+
sessionIdRef.current = null;
|
|
387
|
+
currentAssistantIdRef.current = null;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
return createElement(ViewComponent, {
|
|
392
|
+
params: item.params,
|
|
393
|
+
respond,
|
|
394
|
+
calling: item.calling,
|
|
395
|
+
result: item.result,
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// ─── Render full timeline ───────────────────────────────────
|
|
400
|
+
// Streaming message renders last (after tools) for correct visual order
|
|
401
|
+
const sortedTimeline = [...timeline].sort((a, b) => {
|
|
402
|
+
const aStreaming = a.type === "message" && a.isStreaming ? 1 : 0;
|
|
403
|
+
const bStreaming = b.type === "message" && b.isStreaming ? 1 : 0;
|
|
404
|
+
return aStreaming - bStreaming;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const timelineElements: ReactNode[] = [];
|
|
408
|
+
for (const item of sortedTimeline) {
|
|
409
|
+
if (item.type === "message") {
|
|
410
|
+
timelineElements.push(
|
|
411
|
+
createElement(ChatMessage, {
|
|
412
|
+
key: item.id,
|
|
413
|
+
message: { id: item.id, role: item.role, content: item.content, isStreaming: item.isStreaming },
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
} else if (item.type === "tool") {
|
|
417
|
+
timelineElements.push(
|
|
418
|
+
createElement("div", { key: item.id }, renderToolItem(item)),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Check if any interactive tool is waiting for response
|
|
424
|
+
const hasWaitingInteractive = timeline.some(
|
|
425
|
+
(item) => item.type === "tool" && item.calling && !toolsMap.get(item.toolName)?.isServerTool,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
return createElement(
|
|
429
|
+
ChatInputProvider,
|
|
430
|
+
null,
|
|
431
|
+
createElement(
|
|
432
|
+
"div",
|
|
433
|
+
{ className: "flex flex-col h-full" },
|
|
434
|
+
createElement(
|
|
435
|
+
"div",
|
|
436
|
+
{ className: "max-w-3xl mx-auto w-full space-y-4 flex-1" },
|
|
437
|
+
...timelineElements,
|
|
438
|
+
),
|
|
439
|
+
createElement(Chat, {
|
|
440
|
+
messages: [],
|
|
441
|
+
models: [{ value: "gpt-5.4-nano", label: "GPT-5.4 Nano" }],
|
|
442
|
+
defaultModel: "gpt-5.4-nano",
|
|
443
|
+
onSend: handleSend,
|
|
444
|
+
disabled: isStreaming || hasWaitingInteractive,
|
|
445
|
+
}),
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function tryParseJson(str: string): unknown {
|
|
452
|
+
try {
|
|
453
|
+
return JSON.parse(str);
|
|
454
|
+
} catch {
|
|
455
|
+
return str;
|
|
456
|
+
}
|
|
457
|
+
}
|
package/src/react/index.ts
CHANGED
|
@@ -10,6 +10,5 @@ export type {
|
|
|
10
10
|
QuestionAnswers,
|
|
11
11
|
} from "@arcote.tech/arc-ds";
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
export {
|
|
15
|
-
export type { UseChatConfig, UseChatReturn } from "./use-chat";
|
|
13
|
+
// Internal — chat component factory (used by chat-builder.ts toReactComponent)
|
|
14
|
+
export { createChatComponent } from "./chat-component";
|