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.mjs ADDED
@@ -0,0 +1,658 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
+
21
+ // src/components/indicators/LoadingSpinner.tsx
22
+ import { jsx } from "react/jsx-runtime";
23
+ function LoadingSpinner({ size }) {
24
+ const sizeMap = {
25
+ xs: 24,
26
+ sm: 32,
27
+ md: 48,
28
+ lg: 64
29
+ };
30
+ const px = sizeMap[size];
31
+ return /* @__PURE__ */ jsx("div", { className: "roc-spinner-wrapper", children: /* @__PURE__ */ jsx(
32
+ "div",
33
+ {
34
+ className: "roc-spinner",
35
+ style: {
36
+ width: px,
37
+ height: px
38
+ }
39
+ }
40
+ ) });
41
+ }
42
+
43
+ // src/components/indicators/SendingDots.tsx
44
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
45
+ function SendingDots({ size = "md" }) {
46
+ const sizeMap = {
47
+ xs: { wrapperWidth: 12, fontSize: 8 },
48
+ sm: { wrapperWidth: 16, fontSize: 10 },
49
+ md: { wrapperWidth: 20, fontSize: 12 },
50
+ lg: { wrapperWidth: 24, fontSize: 14 }
51
+ };
52
+ const { wrapperWidth, fontSize } = sizeMap[size];
53
+ return /* @__PURE__ */ jsxs(
54
+ "span",
55
+ {
56
+ className: "roc-sending-dots",
57
+ style: { width: wrapperWidth },
58
+ "aria-label": "sending",
59
+ children: [
60
+ /* @__PURE__ */ jsx2(
61
+ "span",
62
+ {
63
+ className: "roc-sending-dot",
64
+ style: { fontSize, animationDelay: "0ms" },
65
+ children: "."
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsx2(
69
+ "span",
70
+ {
71
+ className: "roc-sending-dot",
72
+ style: { fontSize, animationDelay: "200ms" },
73
+ children: "."
74
+ }
75
+ ),
76
+ /* @__PURE__ */ jsx2(
77
+ "span",
78
+ {
79
+ className: "roc-sending-dot",
80
+ style: { fontSize, animationDelay: "400ms" },
81
+ children: "."
82
+ }
83
+ )
84
+ ]
85
+ }
86
+ );
87
+ }
88
+
89
+ // src/components/ChatMessage.tsx
90
+ import "react";
91
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
92
+ function ChatMessage({
93
+ id,
94
+ role,
95
+ content,
96
+ isLoading = false,
97
+ wrapperClassName = "",
98
+ icon,
99
+ aiIconWrapperClassName = "",
100
+ aiIconColor = "",
101
+ bubbleClassName = "",
102
+ aiBubbleClassName = "",
103
+ userBubbleClassName = "",
104
+ position = "auto",
105
+ loadingRenderer
106
+ }) {
107
+ const isAI = role === "AI";
108
+ const justify = position === "auto" ? isAI ? "justify-start" : "justify-end" : position === "left" ? "justify-start" : "justify-end";
109
+ const defaultAIIcon = /* @__PURE__ */ jsxs2(
110
+ "svg",
111
+ {
112
+ xmlns: "http://www.w3.org/2000/svg",
113
+ className: `${aiIconColor}`,
114
+ width: "24",
115
+ height: "24",
116
+ viewBox: "0 0 24 24",
117
+ fill: "none",
118
+ stroke: "currentColor",
119
+ strokeWidth: "2",
120
+ strokeLinecap: "round",
121
+ strokeLinejoin: "round",
122
+ children: [
123
+ /* @__PURE__ */ jsx3("path", { d: "M12 8V4H8" }),
124
+ /* @__PURE__ */ jsx3("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }),
125
+ /* @__PURE__ */ jsx3("path", { d: "M2 14h2" }),
126
+ /* @__PURE__ */ jsx3("path", { d: "M20 14h2" }),
127
+ /* @__PURE__ */ jsx3("path", { d: "M15 13v2" }),
128
+ /* @__PURE__ */ jsx3("path", { d: "M9 13v2" })
129
+ ]
130
+ }
131
+ );
132
+ return /* @__PURE__ */ jsxs2("div", { className: `flex mb-4 items-start ${justify} ${wrapperClassName}`, children: [
133
+ isAI && /* @__PURE__ */ jsx3(
134
+ "div",
135
+ {
136
+ className: `
137
+ mr-2 bg-gray-100 rounded-full p-2 border-2 border-black
138
+ ${aiIconWrapperClassName}
139
+ `,
140
+ children: icon || defaultAIIcon
141
+ }
142
+ ),
143
+ /* @__PURE__ */ jsx3(
144
+ "div",
145
+ {
146
+ className: `
147
+ py-3 px-3 max-h-96 overflow-y-auto w-fit max-w-[calc(100%-3rem)]
148
+ whitespace-pre-wrap break-words text-sm
149
+ ${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}`}
150
+ ${bubbleClassName}
151
+ `,
152
+ children: isLoading ? loadingRenderer != null ? loadingRenderer : /* @__PURE__ */ jsx3(LoadingSpinner, { size: "xs" }) : content
153
+ }
154
+ )
155
+ ] }, id);
156
+ }
157
+
158
+ // src/components/ChatList.tsx
159
+ import React2 from "react";
160
+ import { jsx as jsx4 } from "react/jsx-runtime";
161
+ function ChatList({
162
+ messages,
163
+ messageMapper,
164
+ messageRenderer,
165
+ className,
166
+ loadingRenderer
167
+ }) {
168
+ const mappedMessages = messageMapper ? messages.map(messageMapper) : messages;
169
+ return /* @__PURE__ */ jsx4("div", { className: `flex flex-col ${className}`, children: mappedMessages.map((msg) => {
170
+ if (messageRenderer) {
171
+ return /* @__PURE__ */ jsx4(React2.Fragment, { children: messageRenderer(msg) }, msg.id);
172
+ }
173
+ return /* @__PURE__ */ jsx4(
174
+ ChatMessage,
175
+ __spreadProps(__spreadValues({}, msg), {
176
+ loadingRenderer
177
+ }),
178
+ msg.id
179
+ );
180
+ }) });
181
+ }
182
+
183
+ // src/components/ChatInput.tsx
184
+ import { useState, useRef, useEffect } from "react";
185
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
186
+ function ChatInput({
187
+ onSend,
188
+ disableVoice = false,
189
+ placeholder = "\uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD558\uC138\uC694...",
190
+ className = "",
191
+ inputClassName = "",
192
+ micButton,
193
+ recordingButton,
194
+ sendButton,
195
+ sendingButton,
196
+ maxHeight = 150,
197
+ value,
198
+ onChange,
199
+ isSending,
200
+ submitOnEnter = false,
201
+ speechLang = "ko-KR"
202
+ }) {
203
+ const [innerText, setInnerText] = useState("");
204
+ const [isRecording, setIsRecording] = useState(false);
205
+ const textareaRef = useRef(null);
206
+ const isControlled = value !== void 0;
207
+ const text = isControlled ? value : innerText;
208
+ const isEmpty = text.trim().length === 0;
209
+ const recognition = useRef(null);
210
+ const isVoiceMode = !disableVoice && !isSending && (isEmpty || isRecording);
211
+ useEffect(() => {
212
+ return () => {
213
+ const r = recognition.current;
214
+ if (r) {
215
+ r.onresult = null;
216
+ r.onstart = null;
217
+ r.onend = null;
218
+ try {
219
+ r.stop();
220
+ } catch (e) {
221
+ console.warn("SpeechRecognition stop error:", e);
222
+ }
223
+ }
224
+ recognition.current = null;
225
+ };
226
+ }, []);
227
+ useEffect(() => {
228
+ const el = textareaRef.current;
229
+ if (!el) return;
230
+ el.style.height = "auto";
231
+ const newHeight = Math.min(el.scrollHeight, maxHeight);
232
+ el.style.height = `${newHeight}px`;
233
+ el.style.overflowY = el.scrollHeight > maxHeight ? "auto" : "hidden";
234
+ }, [text, maxHeight]);
235
+ const handleChange = (e) => {
236
+ const next = e.target.value;
237
+ if (!isControlled) {
238
+ setInnerText(next);
239
+ }
240
+ onChange == null ? void 0 : onChange(next);
241
+ };
242
+ const handleSend = async () => {
243
+ if (isVoiceMode || isEmpty || isSending) return;
244
+ const trimmed = text.trim();
245
+ if (!trimmed) return;
246
+ try {
247
+ if (!isControlled)
248
+ setInnerText("");
249
+ await onSend(trimmed);
250
+ } catch (error) {
251
+ console.error("ChatInput.handleSend.error: ", error);
252
+ }
253
+ };
254
+ const handleKeyDown = async (e) => {
255
+ if (!submitOnEnter) return;
256
+ if (e.key === "Enter" && e.shiftKey) return;
257
+ if (e.key === "Enter") {
258
+ e.preventDefault();
259
+ await handleSend();
260
+ }
261
+ };
262
+ const handleRecord = () => {
263
+ var _a, _b;
264
+ try {
265
+ if (!isRecording) {
266
+ const Speech = window.SpeechRecognition || window.webkitSpeechRecognition;
267
+ if (!Speech) {
268
+ console.error("Browser does not support SpeechRecognition");
269
+ 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.");
270
+ return;
271
+ }
272
+ recognition.current = new Speech();
273
+ recognition.current.lang = speechLang;
274
+ recognition.current.continuous = true;
275
+ recognition.current.interimResults = true;
276
+ recognition.current.onstart = () => {
277
+ setIsRecording(true);
278
+ };
279
+ recognition.current.onend = () => {
280
+ setIsRecording(false);
281
+ };
282
+ recognition.current.onresult = (event) => {
283
+ const newTranscript = Array.from(event.results).map((r) => {
284
+ var _a2;
285
+ return (_a2 = r[0]) == null ? void 0 : _a2.transcript;
286
+ }).join("");
287
+ setInnerText(newTranscript);
288
+ };
289
+ (_a = recognition.current) == null ? void 0 : _a.start();
290
+ } else {
291
+ (_b = recognition.current) == null ? void 0 : _b.stop();
292
+ }
293
+ } catch (e) {
294
+ console.error("Speech Recognition error: ", e);
295
+ alert("\uC74C\uC131 \uC785\uB825\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD14D\uC2A4\uD2B8\uB85C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
296
+ setIsRecording(false);
297
+ }
298
+ };
299
+ const getActivityLayer = () => {
300
+ if (isSending) return "sending";
301
+ if (!disableVoice) {
302
+ if (isRecording) return "recording";
303
+ if (isVoiceMode) return "mic";
304
+ return "send";
305
+ }
306
+ if (disableVoice) {
307
+ if (!isEmpty) return "send";
308
+ return null;
309
+ }
310
+ return null;
311
+ };
312
+ const activeLayer = getActivityLayer();
313
+ return /* @__PURE__ */ jsxs3(
314
+ "div",
315
+ {
316
+ className: `
317
+ flex border border-gray-300 p-2 rounded-3xl
318
+ ${className}
319
+ `,
320
+ children: [
321
+ /* @__PURE__ */ jsx5(
322
+ "textarea",
323
+ {
324
+ ref: textareaRef,
325
+ value: text,
326
+ onChange: handleChange,
327
+ placeholder,
328
+ rows: 1,
329
+ onKeyDown: handleKeyDown,
330
+ className: `
331
+ w-full px-3 py-2
332
+ resize-none border-none
333
+ text-sm focus:outline-none
334
+ overflow-hidden chatinput-scroll
335
+ ${inputClassName}
336
+ `
337
+ }
338
+ ),
339
+ /* @__PURE__ */ jsxs3(
340
+ "button",
341
+ {
342
+ type: "button",
343
+ disabled: isSending,
344
+ onClick: activeLayer === "mic" || activeLayer === "recording" ? handleRecord : handleSend,
345
+ className: "relative w-10 h-10 ml-2 mt-auto flex-shrink-0",
346
+ children: [
347
+ /* @__PURE__ */ jsx5(
348
+ "div",
349
+ {
350
+ className: `
351
+ absolute inset-0 flex items-center justify-center rounded-3xl
352
+ transition-opacity duration-150
353
+ ${activeLayer === "mic" ? "opacity-100" : "opacity-0"}
354
+ bg-gray-100 text-gray-700
355
+ ${(micButton == null ? void 0 : micButton.className) || ""}
356
+ `,
357
+ children: (micButton == null ? void 0 : micButton.icon) || /* @__PURE__ */ jsxs3("svg", { width: "24", height: "24", stroke: "currentColor", fill: "none", strokeWidth: "2", children: [
358
+ /* @__PURE__ */ jsx5("path", { d: "M12 19v3" }),
359
+ /* @__PURE__ */ jsx5("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
360
+ /* @__PURE__ */ jsx5("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
361
+ ] })
362
+ }
363
+ ),
364
+ /* @__PURE__ */ jsx5(
365
+ "div",
366
+ {
367
+ className: `
368
+ absolute inset-0 flex items-center justify-center rounded-3xl
369
+ transition-opacity duration-150
370
+ ${activeLayer === "recording" ? "opacity-100" : "opacity-0"}
371
+ bg-red-600 text-white
372
+ ${(recordingButton == null ? void 0 : recordingButton.className) || ""}
373
+ `,
374
+ children: (recordingButton == null ? void 0 : recordingButton.icon) || /* @__PURE__ */ jsxs3("svg", { width: "24", height: "24", stroke: "currentColor", fill: "none", strokeWidth: "2", children: [
375
+ /* @__PURE__ */ jsx5("path", { d: "M12 19v3" }),
376
+ /* @__PURE__ */ jsx5("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
377
+ /* @__PURE__ */ jsx5("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
378
+ ] })
379
+ }
380
+ ),
381
+ /* @__PURE__ */ jsx5(
382
+ "div",
383
+ {
384
+ className: `
385
+ absolute inset-0 flex items-center justify-center rounded-3xl
386
+ transition-opacity duration-150
387
+ ${activeLayer === "send" ? "opacity-100" : "opacity-0"}
388
+ bg-black text-white
389
+ ${(sendButton == null ? void 0 : sendButton.className) || ""}
390
+ `,
391
+ children: (sendButton == null ? void 0 : sendButton.icon) || /* @__PURE__ */ jsxs3(
392
+ "svg",
393
+ {
394
+ width: "20",
395
+ height: "20",
396
+ viewBox: "0 0 22 24",
397
+ fill: "none",
398
+ stroke: "currentColor",
399
+ "stroke-width": "2",
400
+ children: [
401
+ /* @__PURE__ */ jsx5("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" }),
402
+ /* @__PURE__ */ jsx5("path", { d: "M6 12h16" })
403
+ ]
404
+ }
405
+ )
406
+ }
407
+ ),
408
+ /* @__PURE__ */ jsx5(
409
+ "div",
410
+ {
411
+ className: `
412
+ absolute inset-0 flex items-center justify-center rounded-3xl
413
+ transition-opacity duration-150
414
+ ${activeLayer === "sending" ? "opacity-100" : "opacity-0"}
415
+ bg-gray-400 text-white
416
+ ${(sendingButton == null ? void 0 : sendingButton.className) || ""}
417
+ `,
418
+ children: (sendingButton == null ? void 0 : sendingButton.icon) || /* @__PURE__ */ jsxs3(
419
+ "svg",
420
+ {
421
+ width: "20",
422
+ height: "20",
423
+ viewBox: "0 0 22 24",
424
+ fill: "none",
425
+ stroke: "currentColor",
426
+ "stroke-width": "2",
427
+ children: [
428
+ /* @__PURE__ */ jsx5("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" }),
429
+ /* @__PURE__ */ jsx5("path", { d: "M6 12h16" })
430
+ ]
431
+ }
432
+ )
433
+ }
434
+ )
435
+ ]
436
+ }
437
+ )
438
+ ]
439
+ }
440
+ );
441
+ }
442
+
443
+ // src/components/ChatContainer.tsx
444
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
445
+ import { Fragment, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
446
+ function ChatContainer({
447
+ messages,
448
+ messageMapper,
449
+ messageRenderer,
450
+ loadingRenderer,
451
+ listClassName,
452
+ onSend,
453
+ isSending,
454
+ disableVoice,
455
+ placeholder,
456
+ inputClassName,
457
+ className
458
+ }) {
459
+ const [isAtBottom, setIsAtBottom] = useState2(true);
460
+ const scrollRef = useRef2(null);
461
+ useEffect2(() => {
462
+ const el = scrollRef.current;
463
+ if (!el) return;
464
+ el.scrollTop = el.scrollHeight;
465
+ const handleScroll = () => {
466
+ const isBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 10;
467
+ setIsAtBottom(isBottom);
468
+ };
469
+ el.addEventListener("scroll", handleScroll);
470
+ return () => el.removeEventListener("scroll", handleScroll);
471
+ }, []);
472
+ useEffect2(() => {
473
+ const el = scrollRef.current;
474
+ if (!el) return;
475
+ if (isAtBottom) {
476
+ el.scrollTop = el.scrollHeight;
477
+ }
478
+ }, [messages, isAtBottom]);
479
+ const scrollToBottom = () => {
480
+ const el = scrollRef.current;
481
+ if (!el) return;
482
+ el.scrollTop = el.scrollHeight;
483
+ setIsAtBottom(true);
484
+ };
485
+ const handleSend = async (value) => {
486
+ setIsAtBottom(true);
487
+ requestAnimationFrame(() => {
488
+ scrollToBottom();
489
+ });
490
+ await onSend(value);
491
+ };
492
+ return /* @__PURE__ */ jsx6(Fragment, { children: /* @__PURE__ */ jsxs4(
493
+ "div",
494
+ {
495
+ className: `
496
+ flex flex-col ${className || ""}
497
+ `,
498
+ children: [
499
+ /* @__PURE__ */ jsx6(
500
+ "div",
501
+ {
502
+ ref: scrollRef,
503
+ className: `flex-1 overflow-y-auto chatContainer-scroll p-2`,
504
+ children: /* @__PURE__ */ jsx6(
505
+ ChatList,
506
+ __spreadValues(__spreadValues(__spreadValues(__spreadValues({
507
+ messages
508
+ }, messageMapper && { messageMapper }), messageRenderer && { messageRenderer }), loadingRenderer && { loadingRenderer }), listClassName && { className: listClassName })
509
+ )
510
+ }
511
+ ),
512
+ /* @__PURE__ */ jsxs4("div", { className: "flex-shrink-0 relative", children: [
513
+ !isAtBottom && /* @__PURE__ */ jsx6(
514
+ "button",
515
+ {
516
+ onClick: scrollToBottom,
517
+ 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 ",
518
+ "aria-label": "scroll to bottom",
519
+ children: /* @__PURE__ */ jsxs4(
520
+ "svg",
521
+ {
522
+ xmlns: "http://www.w3.org/2000/svg",
523
+ width: "24",
524
+ height: "24",
525
+ viewBox: "0 0 24 24",
526
+ fill: "none",
527
+ stroke: "currentColor",
528
+ strokeWidth: "2",
529
+ strokeLinecap: "round",
530
+ strokeLinejoin: "round",
531
+ children: [
532
+ /* @__PURE__ */ jsx6("path", { d: "M12 5v14" }),
533
+ /* @__PURE__ */ jsx6("path", { d: "m19 12-7 7-7-7" })
534
+ ]
535
+ }
536
+ )
537
+ }
538
+ ),
539
+ /* @__PURE__ */ jsx6(
540
+ ChatInput,
541
+ __spreadValues(__spreadValues(__spreadValues({
542
+ onSend: handleSend,
543
+ isSending
544
+ }, disableVoice && { disableVoice }), placeholder && { placeholder }), inputClassName && { className: inputClassName })
545
+ )
546
+ ] })
547
+ ]
548
+ }
549
+ ) });
550
+ }
551
+
552
+ // src/hooks/useOptimisticChat.ts
553
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
554
+ import { useState as useState3 } from "react";
555
+ function useOptimisticChat({
556
+ queryKey,
557
+ queryFn,
558
+ mutationFn,
559
+ map,
560
+ onError,
561
+ staleTime = 0,
562
+ gcTime = 0
563
+ }) {
564
+ const [isPending, setIsPending] = useState3(false);
565
+ const queryClient = useQueryClient();
566
+ const {
567
+ data: messages = [],
568
+ isLoading: isInitialLoading
569
+ } = useQuery({
570
+ queryKey,
571
+ queryFn: async () => {
572
+ const rawList = await queryFn();
573
+ return rawList.map(map);
574
+ },
575
+ staleTime,
576
+ gcTime
577
+ });
578
+ const mutation = useMutation({
579
+ mutationFn,
580
+ // (content: string) => Promise<TMutationRaw>
581
+ onMutate: async (content) => {
582
+ setIsPending(true);
583
+ const prev = queryClient.getQueryData(queryKey);
584
+ if (prev) {
585
+ await queryClient.cancelQueries({ queryKey });
586
+ }
587
+ queryClient.setQueryData(queryKey, (old) => {
588
+ const base = old != null ? old : [];
589
+ return [
590
+ ...base,
591
+ // user 메시지 추가
592
+ {
593
+ id: crypto.randomUUID(),
594
+ role: "USER",
595
+ content
596
+ },
597
+ // AI placeholder 추가
598
+ {
599
+ id: crypto.randomUUID(),
600
+ role: "AI",
601
+ content: "",
602
+ isLoading: true
603
+ }
604
+ ];
605
+ });
606
+ return { prev };
607
+ },
608
+ onSuccess: (rawAiResponse) => {
609
+ const aiMessage = map(rawAiResponse);
610
+ queryClient.setQueryData(queryKey, (old) => {
611
+ if (!old || old.length === 0) {
612
+ return [aiMessage];
613
+ }
614
+ const next = [...old];
615
+ const lastIndex = next.length - 1;
616
+ next[lastIndex] = __spreadProps(__spreadValues(__spreadValues({}, next[lastIndex]), aiMessage), {
617
+ isLoading: false
618
+ });
619
+ return next;
620
+ });
621
+ setIsPending(false);
622
+ },
623
+ onError: (error, _variables, context) => {
624
+ setIsPending(false);
625
+ if (context == null ? void 0 : context.prev) {
626
+ queryClient.setQueryData(queryKey, context.prev);
627
+ }
628
+ onError == null ? void 0 : onError(error);
629
+ },
630
+ // mutation 이후 서버 기준 최신 데이터 재동기화
631
+ onSettled: () => {
632
+ queryClient.invalidateQueries({ queryKey });
633
+ }
634
+ });
635
+ const sendUserMessage = (content) => {
636
+ if (!content.trim()) return;
637
+ mutation.mutate(content);
638
+ };
639
+ return {
640
+ messages,
641
+ // Message[]
642
+ sendUserMessage,
643
+ // (content: string) => void
644
+ isPending,
645
+ // 사용자가 채팅 전송 후 AI 응답이 올 때까지의 로딩
646
+ isInitialLoading
647
+ // 초기 로딩 상태
648
+ };
649
+ }
650
+ export {
651
+ ChatContainer,
652
+ ChatInput,
653
+ ChatList,
654
+ ChatMessage,
655
+ LoadingSpinner,
656
+ SendingDots,
657
+ useOptimisticChat
658
+ };
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.18 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,fieldset,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.mt-auto{margin-top:auto}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-6{height:1.5rem}.h-8{height:2rem}.max-h-96{max-height:24rem}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.max-w-\[calc\(100\%-3rem\)\]{max-width:calc(100% - 3rem)}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.animate-\[chatinput-loading-bounce_1\.4s_ease-in-out_0\.2s_infinite\]{animation:chatinput-loading-bounce 1.4s ease-in-out .2s infinite}.animate-\[chatinput-loading-bounce_1\.4s_ease-in-out_0\.4s_infinite\]{animation:chatinput-loading-bounce 1.4s ease-in-out .4s infinite}.animate-\[chatinput-loading-bounce_1\.4s_ease-in-out_infinite\]{animation:chatinput-loading-bounce 1.4s ease-in-out infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.resize-none{resize:none}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-b-xl{border-bottom-right-radius:.75rem;border-bottom-left-radius:.75rem}.rounded-t-xl{border-top-right-radius:.75rem}.rounded-t-xl,.rounded-tl-xl{border-top-left-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-none{border-style:none}.border-black{--tw-border-opacity:1;border-color:rgb(0 0 0/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity,1))}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.p-2{padding:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.text-\[10px\]{font-size:10px}.text-\[12px\]{font-size:12px}.text-\[14px\]{font-size:14px}.text-\[8px\]{font-size:8px}.text-sm{font-size:.875rem;line-height:1.25rem}.font-extrabold{font-weight:800}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.opacity-0{opacity:0}.opacity-100{opacity:1}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-opacity{transition-duration:.15s}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.chatContainer-scroll::-webkit-scrollbar{width:6px}.chatContainer-scroll::-webkit-scrollbar-track{background:transparent}.chatContainer-scroll::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:10px}.chatContainer-scroll::-webkit-scrollbar-button{display:none}.chatinput-scroll::-webkit-scrollbar{width:6px}.chatinput-scroll::-webkit-scrollbar-track{background:transparent}.chatinput-scroll::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:10px}.chatinput-scroll::-webkit-scrollbar-button{display:none}.roc-spinner-wrapper{display:flex;align-items:center;justify-content:center}.roc-spinner{border:4px solid #e5e7eb;border-right-color:transparent;border-radius:50%;animation:roc-spin 1s linear infinite}@keyframes roc-spin{to{transform:rotate(1turn)}}.roc-sending-dots{display:inline-flex;justify-content:space-between;align-items:center}.roc-sending-dot{font-weight:800;line-height:1;animation:roc-sending-bounce 1.4s ease-in-out infinite}@keyframes roc-sending-bounce{0%,to{opacity:.2;transform:translateY(0)}50%{opacity:1;transform:translateY(-2px)}}