react-optimistic-chat 1.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/dist/index.js ADDED
@@ -0,0 +1,698 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __defProps = Object.defineProperties;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
+ var __getProtoOf = Object.getPrototypeOf;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
12
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
13
+ var __spreadValues = (a, b) => {
14
+ for (var prop in b || (b = {}))
15
+ if (__hasOwnProp.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ if (__getOwnPropSymbols)
18
+ for (var prop of __getOwnPropSymbols(b)) {
19
+ if (__propIsEnum.call(b, prop))
20
+ __defNormalProp(a, prop, b[prop]);
21
+ }
22
+ return a;
23
+ };
24
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
25
+ var __export = (target, all) => {
26
+ for (var name in all)
27
+ __defProp(target, name, { get: all[name], enumerable: true });
28
+ };
29
+ var __copyProps = (to, from, except, desc) => {
30
+ if (from && typeof from === "object" || typeof from === "function") {
31
+ for (let key of __getOwnPropNames(from))
32
+ if (!__hasOwnProp.call(to, key) && key !== except)
33
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
34
+ }
35
+ return to;
36
+ };
37
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
38
+ // If the importer is in node compatibility mode or this is not an ESM
39
+ // file that has been converted to a CommonJS file using a Babel-
40
+ // compatible transform (i.e. "__esModule" has not been set), then set
41
+ // "default" to the CommonJS "module.exports" for node compatibility.
42
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
43
+ mod
44
+ ));
45
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
46
+
47
+ // src/index.ts
48
+ var index_exports = {};
49
+ __export(index_exports, {
50
+ ChatContainer: () => ChatContainer,
51
+ ChatInput: () => ChatInput,
52
+ ChatList: () => ChatList,
53
+ ChatMessage: () => ChatMessage,
54
+ LoadingSpinner: () => LoadingSpinner,
55
+ SendingDots: () => SendingDots,
56
+ useOptimisticChat: () => useOptimisticChat
57
+ });
58
+ module.exports = __toCommonJS(index_exports);
59
+
60
+ // src/components/indicators/LoadingSpinner.tsx
61
+ var import_jsx_runtime = require("react/jsx-runtime");
62
+ function LoadingSpinner({ size }) {
63
+ const sizeMap = {
64
+ xs: 24,
65
+ sm: 32,
66
+ md: 48,
67
+ lg: 64
68
+ };
69
+ const px = sizeMap[size];
70
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "roc-spinner-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
71
+ "div",
72
+ {
73
+ className: "roc-spinner",
74
+ style: {
75
+ width: px,
76
+ height: px
77
+ }
78
+ }
79
+ ) });
80
+ }
81
+
82
+ // src/components/indicators/SendingDots.tsx
83
+ var import_jsx_runtime2 = require("react/jsx-runtime");
84
+ function SendingDots({ size = "md" }) {
85
+ const sizeMap = {
86
+ xs: { wrapperWidth: 12, fontSize: 8 },
87
+ sm: { wrapperWidth: 16, fontSize: 10 },
88
+ md: { wrapperWidth: 20, fontSize: 12 },
89
+ lg: { wrapperWidth: 24, fontSize: 14 }
90
+ };
91
+ const { wrapperWidth, fontSize } = sizeMap[size];
92
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
93
+ "span",
94
+ {
95
+ className: "roc-sending-dots",
96
+ style: { width: wrapperWidth },
97
+ "aria-label": "sending",
98
+ children: [
99
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
100
+ "span",
101
+ {
102
+ className: "roc-sending-dot",
103
+ style: { fontSize, animationDelay: "0ms" },
104
+ children: "."
105
+ }
106
+ ),
107
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
108
+ "span",
109
+ {
110
+ className: "roc-sending-dot",
111
+ style: { fontSize, animationDelay: "200ms" },
112
+ children: "."
113
+ }
114
+ ),
115
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
116
+ "span",
117
+ {
118
+ className: "roc-sending-dot",
119
+ style: { fontSize, animationDelay: "400ms" },
120
+ children: "."
121
+ }
122
+ )
123
+ ]
124
+ }
125
+ );
126
+ }
127
+
128
+ // src/components/ChatMessage.tsx
129
+ var import_react = require("react");
130
+ var import_jsx_runtime3 = require("react/jsx-runtime");
131
+ function ChatMessage({
132
+ id,
133
+ role,
134
+ content,
135
+ isLoading = false,
136
+ wrapperClassName = "",
137
+ icon,
138
+ aiIconWrapperClassName = "",
139
+ aiIconColor = "",
140
+ bubbleClassName = "",
141
+ aiBubbleClassName = "",
142
+ userBubbleClassName = "",
143
+ position = "auto",
144
+ loadingRenderer
145
+ }) {
146
+ const isAI = role === "AI";
147
+ const justify = position === "auto" ? isAI ? "justify-start" : "justify-end" : position === "left" ? "justify-start" : "justify-end";
148
+ const defaultAIIcon = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
149
+ "svg",
150
+ {
151
+ xmlns: "http://www.w3.org/2000/svg",
152
+ className: `${aiIconColor}`,
153
+ width: "24",
154
+ height: "24",
155
+ viewBox: "0 0 24 24",
156
+ fill: "none",
157
+ stroke: "currentColor",
158
+ strokeWidth: "2",
159
+ strokeLinecap: "round",
160
+ strokeLinejoin: "round",
161
+ children: [
162
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 8V4H8" }),
163
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }),
164
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M2 14h2" }),
165
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M20 14h2" }),
166
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M15 13v2" }),
167
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M9 13v2" })
168
+ ]
169
+ }
170
+ );
171
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `flex mb-4 items-start ${justify} ${wrapperClassName}`, children: [
172
+ isAI && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
173
+ "div",
174
+ {
175
+ className: `
176
+ mr-2 bg-gray-100 rounded-full p-2 border-2 border-black
177
+ ${aiIconWrapperClassName}
178
+ `,
179
+ children: icon || defaultAIIcon
180
+ }
181
+ ),
182
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
183
+ "div",
184
+ {
185
+ className: `
186
+ py-3 px-3 max-h-96 overflow-y-auto w-fit max-w-[calc(100%-3rem)]
187
+ whitespace-pre-wrap break-words text-sm
188
+ ${isAI ? `bg-gray-100 border-gray-200 rounded-b-xl rounded-t-xl ${aiBubbleClassName}` : `bg-white border border-gray-200 rounded-b-xl rounded-tl-xl ${userBubbleClassName}`}
189
+ ${bubbleClassName}
190
+ `,
191
+ children: isLoading ? loadingRenderer != null ? loadingRenderer : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoadingSpinner, { size: "xs" }) : content
192
+ }
193
+ )
194
+ ] }, id);
195
+ }
196
+
197
+ // src/components/ChatList.tsx
198
+ var import_react2 = __toESM(require("react"));
199
+ var import_jsx_runtime4 = require("react/jsx-runtime");
200
+ function ChatList({
201
+ messages,
202
+ messageMapper,
203
+ messageRenderer,
204
+ className,
205
+ loadingRenderer
206
+ }) {
207
+ const mappedMessages = messageMapper ? messages.map(messageMapper) : messages;
208
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `flex flex-col ${className}`, children: mappedMessages.map((msg) => {
209
+ if (messageRenderer) {
210
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react2.default.Fragment, { children: messageRenderer(msg) }, msg.id);
211
+ }
212
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
213
+ ChatMessage,
214
+ __spreadProps(__spreadValues({}, msg), {
215
+ loadingRenderer
216
+ }),
217
+ msg.id
218
+ );
219
+ }) });
220
+ }
221
+
222
+ // src/components/ChatInput.tsx
223
+ var import_react3 = require("react");
224
+ var import_jsx_runtime5 = require("react/jsx-runtime");
225
+ function ChatInput({
226
+ onSend,
227
+ disableVoice = false,
228
+ placeholder = "\uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD558\uC138\uC694...",
229
+ className = "",
230
+ inputClassName = "",
231
+ micButton,
232
+ recordingButton,
233
+ sendButton,
234
+ sendingButton,
235
+ maxHeight = 150,
236
+ value,
237
+ onChange,
238
+ isSending,
239
+ submitOnEnter = false,
240
+ speechLang = "ko-KR"
241
+ }) {
242
+ const [innerText, setInnerText] = (0, import_react3.useState)("");
243
+ const [isRecording, setIsRecording] = (0, import_react3.useState)(false);
244
+ const textareaRef = (0, import_react3.useRef)(null);
245
+ const isControlled = value !== void 0;
246
+ const text = isControlled ? value : innerText;
247
+ const isEmpty = text.trim().length === 0;
248
+ const recognition = (0, import_react3.useRef)(null);
249
+ const isVoiceMode = !disableVoice && !isSending && (isEmpty || isRecording);
250
+ (0, import_react3.useEffect)(() => {
251
+ return () => {
252
+ const r = recognition.current;
253
+ if (r) {
254
+ r.onresult = null;
255
+ r.onstart = null;
256
+ r.onend = null;
257
+ try {
258
+ r.stop();
259
+ } catch (e) {
260
+ console.warn("SpeechRecognition stop error:", e);
261
+ }
262
+ }
263
+ recognition.current = null;
264
+ };
265
+ }, []);
266
+ (0, import_react3.useEffect)(() => {
267
+ const el = textareaRef.current;
268
+ if (!el) return;
269
+ el.style.height = "auto";
270
+ const newHeight = Math.min(el.scrollHeight, maxHeight);
271
+ el.style.height = `${newHeight}px`;
272
+ el.style.overflowY = el.scrollHeight > maxHeight ? "auto" : "hidden";
273
+ }, [text, maxHeight]);
274
+ const handleChange = (e) => {
275
+ const next = e.target.value;
276
+ if (!isControlled) {
277
+ setInnerText(next);
278
+ }
279
+ onChange == null ? void 0 : onChange(next);
280
+ };
281
+ const handleSend = async () => {
282
+ if (isVoiceMode || isEmpty || isSending) return;
283
+ const trimmed = text.trim();
284
+ if (!trimmed) return;
285
+ try {
286
+ if (!isControlled)
287
+ setInnerText("");
288
+ await onSend(trimmed);
289
+ } catch (error) {
290
+ console.error("ChatInput.handleSend.error: ", error);
291
+ }
292
+ };
293
+ const handleKeyDown = async (e) => {
294
+ if (!submitOnEnter) return;
295
+ if (e.key === "Enter" && e.shiftKey) return;
296
+ if (e.key === "Enter") {
297
+ e.preventDefault();
298
+ await handleSend();
299
+ }
300
+ };
301
+ const handleRecord = () => {
302
+ var _a, _b;
303
+ try {
304
+ if (!isRecording) {
305
+ const Speech = window.SpeechRecognition || window.webkitSpeechRecognition;
306
+ if (!Speech) {
307
+ console.error("Browser does not support SpeechRecognition");
308
+ alert("\uD604\uC7AC \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C\uB294 \uC74C\uC131 \uC778\uC2DD \uAE30\uB2A5\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
309
+ return;
310
+ }
311
+ recognition.current = new Speech();
312
+ recognition.current.lang = speechLang;
313
+ recognition.current.continuous = true;
314
+ recognition.current.interimResults = true;
315
+ recognition.current.onstart = () => {
316
+ setIsRecording(true);
317
+ };
318
+ recognition.current.onend = () => {
319
+ setIsRecording(false);
320
+ };
321
+ recognition.current.onresult = (event) => {
322
+ const newTranscript = Array.from(event.results).map((r) => {
323
+ var _a2;
324
+ return (_a2 = r[0]) == null ? void 0 : _a2.transcript;
325
+ }).join("");
326
+ setInnerText(newTranscript);
327
+ };
328
+ (_a = recognition.current) == null ? void 0 : _a.start();
329
+ } else {
330
+ (_b = recognition.current) == null ? void 0 : _b.stop();
331
+ }
332
+ } catch (e) {
333
+ console.error("Speech Recognition error: ", e);
334
+ alert("\uC74C\uC131 \uC785\uB825\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD14D\uC2A4\uD2B8\uB85C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
335
+ setIsRecording(false);
336
+ }
337
+ };
338
+ const getActivityLayer = () => {
339
+ if (isSending) return "sending";
340
+ if (!disableVoice) {
341
+ if (isRecording) return "recording";
342
+ if (isVoiceMode) return "mic";
343
+ return "send";
344
+ }
345
+ if (disableVoice) {
346
+ if (!isEmpty) return "send";
347
+ return null;
348
+ }
349
+ return null;
350
+ };
351
+ const activeLayer = getActivityLayer();
352
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
353
+ "div",
354
+ {
355
+ className: `
356
+ flex border border-gray-300 p-2 rounded-3xl
357
+ ${className}
358
+ `,
359
+ children: [
360
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
361
+ "textarea",
362
+ {
363
+ ref: textareaRef,
364
+ value: text,
365
+ onChange: handleChange,
366
+ placeholder,
367
+ rows: 1,
368
+ onKeyDown: handleKeyDown,
369
+ className: `
370
+ w-full px-3 py-2
371
+ resize-none border-none
372
+ text-sm focus:outline-none
373
+ overflow-hidden chatinput-scroll
374
+ ${inputClassName}
375
+ `
376
+ }
377
+ ),
378
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
379
+ "button",
380
+ {
381
+ type: "button",
382
+ disabled: isSending,
383
+ onClick: activeLayer === "mic" || activeLayer === "recording" ? handleRecord : handleSend,
384
+ className: "relative w-10 h-10 ml-2 mt-auto flex-shrink-0",
385
+ children: [
386
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
387
+ "div",
388
+ {
389
+ className: `
390
+ absolute inset-0 flex items-center justify-center rounded-3xl
391
+ transition-opacity duration-150
392
+ ${activeLayer === "mic" ? "opacity-100" : "opacity-0"}
393
+ bg-gray-100 text-gray-700
394
+ ${(micButton == null ? void 0 : micButton.className) || ""}
395
+ `,
396
+ children: (micButton == null ? void 0 : micButton.icon) || /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { width: "24", height: "24", stroke: "currentColor", fill: "none", strokeWidth: "2", children: [
397
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 19v3" }),
398
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
399
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
400
+ ] })
401
+ }
402
+ ),
403
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
404
+ "div",
405
+ {
406
+ className: `
407
+ absolute inset-0 flex items-center justify-center rounded-3xl
408
+ transition-opacity duration-150
409
+ ${activeLayer === "recording" ? "opacity-100" : "opacity-0"}
410
+ bg-red-600 text-white
411
+ ${(recordingButton == null ? void 0 : recordingButton.className) || ""}
412
+ `,
413
+ children: (recordingButton == null ? void 0 : recordingButton.icon) || /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { width: "24", height: "24", stroke: "currentColor", fill: "none", strokeWidth: "2", children: [
414
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 19v3" }),
415
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
416
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
417
+ ] })
418
+ }
419
+ ),
420
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
421
+ "div",
422
+ {
423
+ className: `
424
+ absolute inset-0 flex items-center justify-center rounded-3xl
425
+ transition-opacity duration-150
426
+ ${activeLayer === "send" ? "opacity-100" : "opacity-0"}
427
+ bg-black text-white
428
+ ${(sendButton == null ? void 0 : sendButton.className) || ""}
429
+ `,
430
+ children: (sendButton == null ? void 0 : sendButton.icon) || /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
431
+ "svg",
432
+ {
433
+ width: "20",
434
+ height: "20",
435
+ viewBox: "0 0 22 24",
436
+ fill: "none",
437
+ stroke: "currentColor",
438
+ "stroke-width": "2",
439
+ children: [
440
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z" }),
441
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M6 12h16" })
442
+ ]
443
+ }
444
+ )
445
+ }
446
+ ),
447
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
448
+ "div",
449
+ {
450
+ className: `
451
+ absolute inset-0 flex items-center justify-center rounded-3xl
452
+ transition-opacity duration-150
453
+ ${activeLayer === "sending" ? "opacity-100" : "opacity-0"}
454
+ bg-gray-400 text-white
455
+ ${(sendingButton == null ? void 0 : sendingButton.className) || ""}
456
+ `,
457
+ children: (sendingButton == null ? void 0 : sendingButton.icon) || /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
458
+ "svg",
459
+ {
460
+ width: "20",
461
+ height: "20",
462
+ viewBox: "0 0 22 24",
463
+ fill: "none",
464
+ stroke: "currentColor",
465
+ "stroke-width": "2",
466
+ children: [
467
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z" }),
468
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M6 12h16" })
469
+ ]
470
+ }
471
+ )
472
+ }
473
+ )
474
+ ]
475
+ }
476
+ )
477
+ ]
478
+ }
479
+ );
480
+ }
481
+
482
+ // src/components/ChatContainer.tsx
483
+ var import_react4 = require("react");
484
+ var import_jsx_runtime6 = require("react/jsx-runtime");
485
+ function ChatContainer({
486
+ messages,
487
+ messageMapper,
488
+ messageRenderer,
489
+ loadingRenderer,
490
+ listClassName,
491
+ onSend,
492
+ isSending,
493
+ disableVoice,
494
+ placeholder,
495
+ inputClassName,
496
+ className
497
+ }) {
498
+ const [isAtBottom, setIsAtBottom] = (0, import_react4.useState)(true);
499
+ const scrollRef = (0, import_react4.useRef)(null);
500
+ (0, import_react4.useEffect)(() => {
501
+ const el = scrollRef.current;
502
+ if (!el) return;
503
+ el.scrollTop = el.scrollHeight;
504
+ const handleScroll = () => {
505
+ const isBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 10;
506
+ setIsAtBottom(isBottom);
507
+ };
508
+ el.addEventListener("scroll", handleScroll);
509
+ return () => el.removeEventListener("scroll", handleScroll);
510
+ }, []);
511
+ (0, import_react4.useEffect)(() => {
512
+ const el = scrollRef.current;
513
+ if (!el) return;
514
+ if (isAtBottom) {
515
+ el.scrollTop = el.scrollHeight;
516
+ }
517
+ }, [messages, isAtBottom]);
518
+ const scrollToBottom = () => {
519
+ const el = scrollRef.current;
520
+ if (!el) return;
521
+ el.scrollTop = el.scrollHeight;
522
+ setIsAtBottom(true);
523
+ };
524
+ const handleSend = async (value) => {
525
+ setIsAtBottom(true);
526
+ requestAnimationFrame(() => {
527
+ scrollToBottom();
528
+ });
529
+ await onSend(value);
530
+ };
531
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
532
+ "div",
533
+ {
534
+ className: `
535
+ flex flex-col ${className || ""}
536
+ `,
537
+ children: [
538
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
539
+ "div",
540
+ {
541
+ ref: scrollRef,
542
+ className: `flex-1 overflow-y-auto chatContainer-scroll p-2`,
543
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
544
+ ChatList,
545
+ __spreadValues(__spreadValues(__spreadValues(__spreadValues({
546
+ messages
547
+ }, messageMapper && { messageMapper }), messageRenderer && { messageRenderer }), loadingRenderer && { loadingRenderer }), listClassName && { className: listClassName })
548
+ )
549
+ }
550
+ ),
551
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex-shrink-0 relative", children: [
552
+ !isAtBottom && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
553
+ "button",
554
+ {
555
+ onClick: scrollToBottom,
556
+ className: "\r\n absolute bottom-20 left-1/2 -translate-x-1/2\r\n w-10 h-10 rounded-full bg-white font-bold\r\n flex items-center justify-center\r\n border-gray-200 border-[1px]\r\n ",
557
+ "aria-label": "scroll to bottom",
558
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
559
+ "svg",
560
+ {
561
+ xmlns: "http://www.w3.org/2000/svg",
562
+ width: "24",
563
+ height: "24",
564
+ viewBox: "0 0 24 24",
565
+ fill: "none",
566
+ stroke: "currentColor",
567
+ strokeWidth: "2",
568
+ strokeLinecap: "round",
569
+ strokeLinejoin: "round",
570
+ children: [
571
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M12 5v14" }),
572
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "m19 12-7 7-7-7" })
573
+ ]
574
+ }
575
+ )
576
+ }
577
+ ),
578
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
579
+ ChatInput,
580
+ __spreadValues(__spreadValues(__spreadValues({
581
+ onSend: handleSend,
582
+ isSending
583
+ }, disableVoice && { disableVoice }), placeholder && { placeholder }), inputClassName && { className: inputClassName })
584
+ )
585
+ ] })
586
+ ]
587
+ }
588
+ ) });
589
+ }
590
+
591
+ // src/hooks/useOptimisticChat.ts
592
+ var import_react_query = require("@tanstack/react-query");
593
+ var import_react5 = require("react");
594
+ function useOptimisticChat({
595
+ queryKey,
596
+ queryFn,
597
+ mutationFn,
598
+ map,
599
+ onError,
600
+ staleTime = 0,
601
+ gcTime = 0
602
+ }) {
603
+ const [isPending, setIsPending] = (0, import_react5.useState)(false);
604
+ const queryClient = (0, import_react_query.useQueryClient)();
605
+ const {
606
+ data: messages = [],
607
+ isLoading: isInitialLoading
608
+ } = (0, import_react_query.useQuery)({
609
+ queryKey,
610
+ queryFn: async () => {
611
+ const rawList = await queryFn();
612
+ return rawList.map(map);
613
+ },
614
+ staleTime,
615
+ gcTime
616
+ });
617
+ const mutation = (0, import_react_query.useMutation)({
618
+ mutationFn,
619
+ // (content: string) => Promise<TMutationRaw>
620
+ onMutate: async (content) => {
621
+ setIsPending(true);
622
+ const prev = queryClient.getQueryData(queryKey);
623
+ if (prev) {
624
+ await queryClient.cancelQueries({ queryKey });
625
+ }
626
+ queryClient.setQueryData(queryKey, (old) => {
627
+ const base = old != null ? old : [];
628
+ return [
629
+ ...base,
630
+ // user 메시지 추가
631
+ {
632
+ id: crypto.randomUUID(),
633
+ role: "USER",
634
+ content
635
+ },
636
+ // AI placeholder 추가
637
+ {
638
+ id: crypto.randomUUID(),
639
+ role: "AI",
640
+ content: "",
641
+ isLoading: true
642
+ }
643
+ ];
644
+ });
645
+ return { prev };
646
+ },
647
+ onSuccess: (rawAiResponse) => {
648
+ const aiMessage = map(rawAiResponse);
649
+ queryClient.setQueryData(queryKey, (old) => {
650
+ if (!old || old.length === 0) {
651
+ return [aiMessage];
652
+ }
653
+ const next = [...old];
654
+ const lastIndex = next.length - 1;
655
+ next[lastIndex] = __spreadProps(__spreadValues(__spreadValues({}, next[lastIndex]), aiMessage), {
656
+ isLoading: false
657
+ });
658
+ return next;
659
+ });
660
+ setIsPending(false);
661
+ },
662
+ onError: (error, _variables, context) => {
663
+ setIsPending(false);
664
+ if (context == null ? void 0 : context.prev) {
665
+ queryClient.setQueryData(queryKey, context.prev);
666
+ }
667
+ onError == null ? void 0 : onError(error);
668
+ },
669
+ // mutation 이후 서버 기준 최신 데이터 재동기화
670
+ onSettled: () => {
671
+ queryClient.invalidateQueries({ queryKey });
672
+ }
673
+ });
674
+ const sendUserMessage = (content) => {
675
+ if (!content.trim()) return;
676
+ mutation.mutate(content);
677
+ };
678
+ return {
679
+ messages,
680
+ // Message[]
681
+ sendUserMessage,
682
+ // (content: string) => void
683
+ isPending,
684
+ // 사용자가 채팅 전송 후 AI 응답이 올 때까지의 로딩
685
+ isInitialLoading
686
+ // 초기 로딩 상태
687
+ };
688
+ }
689
+ // Annotate the CommonJS export names for ESM import in node:
690
+ 0 && (module.exports = {
691
+ ChatContainer,
692
+ ChatInput,
693
+ ChatList,
694
+ ChatMessage,
695
+ LoadingSpinner,
696
+ SendingDots,
697
+ useOptimisticChat
698
+ });