react-optimistic-chat 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,17 +58,6 @@ yarn add react-optimistic-chat
58
58
 
59
59
  <br>
60
60
 
61
- ## styles
62
- <code>react-optimistic-chat</code>의 **채팅 UI 컴포넌트**를 사용하려면
63
- 아래 스타일 파일을 반드시 import 해야 합니다.
64
- ```ts
65
- import "react-optimistic-chat/style.css";
66
- ```
67
- > React 프로젝트에서는 `App.tsx`에,
68
- > Next.js(App Router)에서는 루트 `Layout.tsx`에서 import 하는 것을 권장합니다.
69
-
70
- <br>
71
-
72
61
  <h1 id="quick-start">🚀 Quick Start</h1>
73
62
 
74
63
  아래 예제는 서버로부터 전달되는 Raw 채팅 데이터를
@@ -364,12 +353,44 @@ const {
364
353
 
365
354
  <br>
366
355
 
367
- ### 🔁 Optimistic Update Flow
368
- **1.** 사용자가 메시지 전송
369
- **2.** USER 메시지 + 로딩 중인 AI 메시지를 즉시 캐시에 삽입
370
- **3.** AI 응답이 도착
371
- **4.** 로딩 중인 AI 메시지를 실제 응답으로 교체
372
- **5.** 에러 발생 시 이전 상태로 rollback
356
+ ### 🔁 useChat Flow
357
+ <table width="892" align="center" bgcolor="white">
358
+ <tr>
359
+ <td align="center" bgcolor="white">
360
+ <img
361
+ src="https://github.com/user-attachments/assets/6e61356b-0da3-45d9-8791-c11aa0d346e9"
362
+ width="690"
363
+ style="display: block; margin: 0 auto;"
364
+ alt="useChat 호출"
365
+ />
366
+ </td>
367
+ </tr>
368
+ <tr>
369
+ <td align="center" bgcolor="white">
370
+ <b>useChat 호출</b>
371
+ </td>
372
+ </tr>
373
+ </table>
374
+
375
+ <table width="892" align="center" bgcolor="white">
376
+ <tr>
377
+ <td align="center" bgcolor="white">
378
+ <img
379
+ src="https://github.com/user-attachments/assets/10218350-8844-4bee-b78e-fe4564844e57"
380
+ width="690"
381
+ style="display: block; margin: 0 auto;"
382
+ alt="useChat 실행"
383
+ />
384
+ </td>
385
+ </tr>
386
+ <tr>
387
+ <td align="center" bgcolor="white">
388
+ <b>useChat 실행</b>
389
+ </td>
390
+ </tr>
391
+ </table>
392
+
393
+
373
394
 
374
395
  <br>
375
396
 
@@ -503,14 +524,42 @@ const {
503
524
 
504
525
  <br>
505
526
 
506
- ### 🔁 Voice-based Optimistic Update Flow
507
- **1.** 음성 인식 시작
508
- **2.** USER 메시지를 빈 content로 캐시에 즉시 삽입
509
- **3.** 음성 인식 중간 결과를 실시간으로 메시지에 반영
510
- **4.** 음성 인식 종료 + 로딩 중인 AI 메시지를 즉시 캐시에 삽입
511
- **5.** 최종 transcript로 AI 요청 전송
512
- **6.** AI placeholder 메시지를 실제 응답으로 교체
513
- **7.** 에러 또는 입력 시 이전 상태로 rollback
527
+ ### 🔁 useVoiceChat Flow
528
+ <table width="1001" align="center" bgcolor="white">
529
+ <tr>
530
+ <td align="center" bgcolor="white">
531
+ <img
532
+ src="https://github.com/user-attachments/assets/c80acf37-4886-4c36-a952-e629ccd73088"
533
+ width="690"
534
+ style="display: block; margin: 0 auto;"
535
+ alt="useVoiceChat 호출"
536
+ />
537
+ </td>
538
+ </tr>
539
+ <tr>
540
+ <td align="center" bgcolor="white">
541
+ <b>useVoiceChat 호출</b>
542
+ </td>
543
+ </tr>
544
+ </table>
545
+
546
+ <table width="1001" align="center" bgcolor="white">
547
+ <tr>
548
+ <td align="center" bgcolor="white">
549
+ <img
550
+ src="https://github.com/user-attachments/assets/92b44439-5db4-474d-875e-8e62a09f157b"
551
+ width="690"
552
+ style="display: block; margin: 0 auto;"
553
+ alt="useVoiceChat 실행"
554
+ />
555
+ </td>
556
+ </tr>
557
+ <tr>
558
+ <td align="center" bgcolor="white">
559
+ <b>useVoiceChat 실행</b>
560
+ </td>
561
+ </tr>
562
+ </table>
514
563
 
515
564
  <br>
516
565
 
@@ -843,3 +892,6 @@ See the [LICENSE](./LICENSE) file for details.
843
892
 
844
893
 
845
894
 
895
+
896
+
897
+
package/dist/index.js CHANGED
@@ -59,6 +59,46 @@ __export(index_exports, {
59
59
  });
60
60
  module.exports = __toCommonJS(index_exports);
61
61
 
62
+ // #style-inject:#style-inject
63
+ function styleInject(css, { insertAt } = {}) {
64
+ if (!css || typeof document === "undefined") return;
65
+ const head = document.head || document.getElementsByTagName("head")[0];
66
+ const style = document.createElement("style");
67
+ style.type = "text/css";
68
+ if (insertAt === "top") {
69
+ if (head.firstChild) {
70
+ head.insertBefore(style, head.firstChild);
71
+ } else {
72
+ head.appendChild(style);
73
+ }
74
+ } else {
75
+ head.appendChild(style);
76
+ }
77
+ if (style.styleSheet) {
78
+ style.styleSheet.cssText = css;
79
+ } else {
80
+ style.appendChild(document.createTextNode(css));
81
+ }
82
+ }
83
+
84
+ // src/styles/chatMessage.css
85
+ styleInject(".roc-chat-message {\n display: flex;\n align-items: flex-start;\n margin-bottom: 1rem;\n width: 100%;\n box-sizing: border-box;\n}\n.roc-chat-message--left {\n justify-content: flex-start;\n}\n.roc-chat-message--right {\n justify-content: flex-end;\n}\n.roc-chat-message__icon {\n width: 40px;\n height: 40px;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: #f3f4f6;\n border-radius: 9999px;\n border: 2px solid #000;\n flex-shrink: 0;\n margin-right: 0.5rem;\n}\n.roc-chat-message__icon svg {\n display: block;\n}\n.roc-chat-message__bubble {\n padding: 0.75rem;\n max-width: calc(100% - 3rem);\n max-height: 24rem;\n overflow-y: auto;\n white-space: pre-wrap;\n word-break: break-word;\n font-size: 0.875rem;\n line-height: 1.4;\n box-sizing: border-box;\n}\n.roc-chat-message__bubble--ai {\n background-color: #f3f4f6;\n border-radius: 0.75rem;\n}\n.roc-chat-message__bubble--user {\n background-color: #ffffff;\n border: 1px solid #e5e7eb;\n border-radius: 0.75rem 0.75rem 0 0.75rem;\n}\n");
86
+
87
+ // src/styles/chatList.css
88
+ styleInject(".roc-chat-list {\n display: flex;\n flex-direction: column;\n}\n");
89
+
90
+ // src/styles/chatInput.css
91
+ styleInject('.roc-chat-input {\n display: flex;\n border: 1px solid #d1d5db;\n padding: 8px 8px;\n border-radius: 24px;\n}\n.roc-chat-input__textarea {\n width: 100%;\n resize: none;\n border: none;\n font-size: 16px;\n line-height: 2.2;\n outline: none;\n overflow: hidden;\n padding: 0;\n padding-left: 8px;\n}\n.roc-chat-input__textarea::-moz-placeholder {\n color: #9ca3af;\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n "Apple SD Gothic Neo",\n "Noto Sans KR",\n "Noto Sans",\n system-ui,\n sans-serif;\n}\n.roc-chat-input__textarea::placeholder {\n color: #9ca3af;\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n "Apple SD Gothic Neo",\n "Noto Sans KR",\n "Noto Sans",\n system-ui,\n sans-serif;\n}\n.roc-chat-input__button {\n position: relative;\n width: 36px;\n height: 36px;\n margin-top: auto;\n flex-shrink: 0;\n background: none;\n border: none;\n margin-left: 8px;\n cursor: pointer;\n}\n.roc-chat-input__layer {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 24px;\n opacity: 0;\n transition: opacity 150ms ease;\n}\n.roc-chat-input__layer.is-active {\n opacity: 1;\n}\n.roc-chat-input__layer--mic {\n background: #f3f4f6;\n color: #374151;\n}\n.roc-chat-input__layer--recording {\n background: #dc2626;\n color: #ffffff;\n}\n.roc-chat-input__layer--send {\n background: #000000;\n color: #ffffff;\n}\n.roc-chat-input__layer--sending {\n background: #9ca3af;\n color: #ffffff;\n}\n');
92
+
93
+ // src/styles/chatContainer.css
94
+ styleInject(".roc-chat-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n.roc-chat-container__list {\n flex: 1;\n overflow-y: auto;\n padding: 8px;\n}\n.roc-chat-container__loading {\n display: flex;\n justify-content: center;\n padding: 8px 0;\n}\n.roc-chat-container__input {\n position: relative;\n flex-shrink: 0;\n}\n.roc-chat-container__scroll-button {\n position: absolute;\n bottom: 80px;\n left: 50%;\n transform: translateX(-50%);\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: #ffffff;\n border: 1px solid #e5e7eb;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n}\n");
95
+
96
+ // src/styles/loadingSpinner.css
97
+ styleInject(".roc-spinner-wrapper {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.roc-spinner {\n border: 4px solid #e5e7eb;\n border-right-color: transparent;\n border-radius: 50%;\n animation: roc-spin 1s linear infinite;\n}\n@keyframes roc-spin {\n to {\n transform: rotate(1turn);\n }\n}\n");
98
+
99
+ // src/styles/sendingDots.css
100
+ styleInject(".roc-sending-dots {\n display: inline-flex;\n justify-content: space-between;\n align-items: center;\n}\n.roc-sending-dot {\n font-weight: 800;\n line-height: 1;\n animation: roc-sending-bounce 1.4s ease-in-out infinite;\n}\n@keyframes roc-sending-bounce {\n 0%, to {\n opacity: .2;\n transform: translateY(0);\n }\n 50% {\n opacity: 1;\n transform: translateY(-2px);\n }\n}\n");
101
+
62
102
  // src/components/indicators/LoadingSpinner.tsx
63
103
  var import_jsx_runtime = require("react/jsx-runtime");
64
104
  function LoadingSpinner({ size = "md" }) {
@@ -146,12 +186,12 @@ function ChatMessage({
146
186
  loadingRenderer
147
187
  }) {
148
188
  const isAI = role === "AI";
149
- const justify = position === "auto" ? isAI ? "justify-start" : "justify-end" : position === "left" ? "justify-start" : "justify-end";
189
+ const justify = position === "auto" ? isAI ? "left" : "right" : position;
150
190
  const defaultAIIcon = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
151
191
  "svg",
152
192
  {
153
193
  xmlns: "http://www.w3.org/2000/svg",
154
- className: `${aiIconColor}`,
194
+ className: aiIconColor,
155
195
  width: "24",
156
196
  height: "24",
157
197
  viewBox: "0 0 24 24",
@@ -170,30 +210,41 @@ function ChatMessage({
170
210
  ]
171
211
  }
172
212
  );
173
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `flex mb-4 items-start ${justify} ${wrapperClassName}`, children: [
174
- isAI && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
175
- "div",
176
- {
177
- className: `
178
- mr-2 bg-gray-100 rounded-full p-2 border-2 border-black
213
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
214
+ "div",
215
+ {
216
+ className: `
217
+ roc-chat-message
218
+ roc-chat-message--${justify}
219
+ ${wrapperClassName}
220
+ `,
221
+ children: [
222
+ isAI && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
223
+ "div",
224
+ {
225
+ className: `
226
+ roc-chat-message__icon
179
227
  ${aiIconWrapperClassName}
180
228
  `,
181
- children: icon || defaultAIIcon
182
- }
183
- ),
184
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
185
- "div",
186
- {
187
- className: `
188
- py-3 px-3 max-h-96 overflow-y-auto w-fit max-w-[calc(100%-3rem)]
189
- whitespace-pre-wrap break-words text-sm
190
- ${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}`}
229
+ children: icon || defaultAIIcon
230
+ }
231
+ ),
232
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
233
+ "div",
234
+ {
235
+ className: `
236
+ roc-chat-message__bubble
237
+ ${isAI ? "roc-chat-message__bubble--ai" : "roc-chat-message__bubble--user"}
238
+ ${isAI ? aiBubbleClassName : userBubbleClassName}
191
239
  ${bubbleClassName}
192
240
  `,
193
- children: isLoading ? loadingRenderer != null ? loadingRenderer : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoadingSpinner, { size: "xs" }) : content
194
- }
195
- )
196
- ] }, id);
241
+ children: isLoading ? loadingRenderer != null ? loadingRenderer : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoadingSpinner, { size: "xs" }) : content
242
+ }
243
+ )
244
+ ]
245
+ },
246
+ id
247
+ );
197
248
  }
198
249
 
199
250
  // src/components/ChatList.tsx
@@ -207,7 +258,7 @@ function ChatList({
207
258
  loadingRenderer
208
259
  }) {
209
260
  const mappedMessages = messageMapper ? messages.map((msg) => __spreadValues(__spreadValues({}, msg), messageMapper(msg))) : messages;
210
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `flex flex-col ${className}`, children: mappedMessages.map((msg) => {
261
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `roc-chat-list ${className != null ? className : ""}`, children: mappedMessages.map((msg) => {
211
262
  if (messageRenderer) {
212
263
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react2.default.Fragment, { children: messageRenderer(msg) }, msg.id);
213
264
  }
@@ -385,134 +436,95 @@ function ChatInput({
385
436
  return null;
386
437
  };
387
438
  const activeLayer = getActivityLayer();
388
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
389
- "div",
390
- {
391
- className: `
392
- flex border border-gray-300 p-2 rounded-3xl
393
- ${className}
394
- `,
395
- children: [
396
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
397
- "textarea",
398
- {
399
- ref: textareaRef,
400
- value: text,
401
- onChange: handleChange,
402
- placeholder,
403
- rows: 1,
404
- onKeyDown: handleKeyDown,
405
- className: `
406
- w-full px-3 py-2
407
- resize-none border-none
408
- text-sm focus:outline-none
409
- overflow-hidden chatinput-scroll
410
- ${inputClassName}
411
- `
412
- }
413
- ),
414
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
415
- "button",
416
- {
417
- type: "button",
418
- disabled: isSending,
419
- onClick: activeLayer === "mic" || activeLayer === "recording" ? handleRecord : handleSend,
420
- className: "relative w-10 h-10 ml-2 mt-auto flex-shrink-0",
421
- children: [
422
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
423
- "div",
424
- {
425
- className: `
426
- absolute inset-0 flex items-center justify-center rounded-3xl
427
- transition-opacity duration-150
428
- ${activeLayer === "mic" ? "opacity-100" : "opacity-0"}
429
- bg-gray-100 text-gray-700
430
- ${(micButton == null ? void 0 : micButton.className) || ""}
431
- `,
432
- 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: [
433
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 19v3" }),
434
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
435
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
436
- ] })
437
- }
438
- ),
439
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
440
- "div",
441
- {
442
- className: `
443
- absolute inset-0 flex items-center justify-center rounded-3xl
444
- transition-opacity duration-150
445
- ${activeLayer === "recording" ? "opacity-100" : "opacity-0"}
446
- bg-red-600 text-white
447
- ${(recordingButton == null ? void 0 : recordingButton.className) || ""}
448
- `,
449
- 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: [
450
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 19v3" }),
451
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
452
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
453
- ] })
454
- }
455
- ),
456
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
457
- "div",
439
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: `roc-chat-input ${className}`, children: [
440
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
441
+ "textarea",
442
+ {
443
+ ref: textareaRef,
444
+ value: text,
445
+ onChange: handleChange,
446
+ placeholder,
447
+ rows: 1,
448
+ onKeyDown: handleKeyDown,
449
+ className: `roc-chat-input__textarea ${inputClassName}`
450
+ }
451
+ ),
452
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
453
+ "button",
454
+ {
455
+ type: "button",
456
+ disabled: isSending,
457
+ onClick: activeLayer === "mic" || activeLayer === "recording" ? handleRecord : handleSend,
458
+ className: "roc-chat-input__button",
459
+ children: [
460
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
461
+ "div",
462
+ {
463
+ className: `roc-chat-input__layer roc-chat-input__layer--mic ${activeLayer === "mic" ? "is-active" : ""} ${(micButton == null ? void 0 : micButton.className) || ""}`,
464
+ 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: [
465
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 19v3" }),
466
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
467
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
468
+ ] })
469
+ }
470
+ ),
471
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
472
+ "div",
473
+ {
474
+ className: `roc-chat-input__layer roc-chat-input__layer--recording ${activeLayer === "recording" ? "is-active" : ""} ${(recordingButton == null ? void 0 : recordingButton.className) || ""}`,
475
+ 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: [
476
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 19v3" }),
477
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
478
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
479
+ ] })
480
+ }
481
+ ),
482
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
483
+ "div",
484
+ {
485
+ className: `roc-chat-input__layer roc-chat-input__layer--send ${activeLayer === "send" ? "is-active" : ""} ${(sendButton == null ? void 0 : sendButton.className) || ""}`,
486
+ children: (sendButton == null ? void 0 : sendButton.icon) || /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
487
+ "svg",
458
488
  {
459
- className: `
460
- absolute inset-0 flex items-center justify-center rounded-3xl
461
- transition-opacity duration-150
462
- ${activeLayer === "send" ? "opacity-100" : "opacity-0"}
463
- bg-black text-white
464
- ${(sendButton == null ? void 0 : sendButton.className) || ""}
465
- `,
466
- children: (sendButton == null ? void 0 : sendButton.icon) || /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
467
- "svg",
468
- {
469
- width: "20",
470
- height: "20",
471
- viewBox: "0 0 22 24",
472
- fill: "none",
473
- stroke: "currentColor",
474
- "stroke-width": "2",
475
- children: [
476
- /* @__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" }),
477
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M6 12h16" })
478
- ]
479
- }
480
- )
489
+ width: "20",
490
+ height: "20",
491
+ viewBox: "0 0 22 24",
492
+ fill: "none",
493
+ stroke: "currentColor",
494
+ "stroke-width": "2",
495
+ children: [
496
+ /* @__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" }),
497
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M6 12h16" })
498
+ ]
481
499
  }
482
- ),
483
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
484
- "div",
500
+ )
501
+ }
502
+ ),
503
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
504
+ "div",
505
+ {
506
+ className: `roc-chat-input__layer roc-chat-input__layer--sending ${activeLayer === "sending" ? "is-active" : ""} ${(sendingButton == null ? void 0 : sendingButton.className) || ""}`,
507
+ children: (sendingButton == null ? void 0 : sendingButton.icon) || /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
508
+ "svg",
485
509
  {
486
- className: `
487
- absolute inset-0 flex items-center justify-center rounded-3xl
488
- transition-opacity duration-150
489
- ${activeLayer === "sending" ? "opacity-100" : "opacity-0"}
490
- bg-gray-400 text-white
491
- ${(sendingButton == null ? void 0 : sendingButton.className) || ""}
492
- `,
493
- children: (sendingButton == null ? void 0 : sendingButton.icon) || /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
494
- "svg",
495
- {
496
- width: "20",
497
- height: "20",
498
- viewBox: "0 0 22 24",
499
- fill: "none",
500
- stroke: "currentColor",
501
- "stroke-width": "2",
502
- children: [
503
- /* @__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" }),
504
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M6 12h16" })
505
- ]
506
- }
507
- )
510
+ width: "20",
511
+ height: "20",
512
+ viewBox: "0 0 22 24",
513
+ fill: "none",
514
+ stroke: "currentColor",
515
+ "stroke-width": "2",
516
+ children: [
517
+ /* @__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" }),
518
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M6 12h16" })
519
+ ]
508
520
  }
509
521
  )
510
- ]
511
- }
512
- )
513
- ]
514
- }
515
- );
522
+ }
523
+ )
524
+ ]
525
+ }
526
+ )
527
+ ] });
516
528
  }
517
529
 
518
530
  // src/components/ChatContainer.tsx
@@ -575,67 +587,59 @@ function ChatContainer(props) {
575
587
  });
576
588
  await onSend(value);
577
589
  };
578
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
579
- "div",
580
- {
581
- className: `
582
- flex flex-col ${className || ""}
583
- `,
584
- children: [
585
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
586
- "div",
587
- {
588
- ref: scrollRef,
589
- className: `flex-1 overflow-y-auto chatContainer-scroll p-2`,
590
- children: [
591
- hasNextPage && isFetchingNextPage && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex justify-center py-2", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LoadingSpinner, { size: "sm" }) }),
592
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
593
- ChatList,
594
- __spreadValues(__spreadValues(__spreadValues({
595
- messages: mappedMessages
596
- }, messageRenderer && { messageRenderer }), loadingRenderer && { loadingRenderer }), listClassName && { className: listClassName })
597
- )
598
- ]
599
- }
600
- ),
601
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex-shrink-0 relative", children: [
602
- !isAtBottom && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
603
- "button",
604
- {
605
- onClick: scrollToBottom,
606
- 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 ",
607
- "aria-label": "scroll to bottom",
608
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
609
- "svg",
610
- {
611
- xmlns: "http://www.w3.org/2000/svg",
612
- width: "24",
613
- height: "24",
614
- viewBox: "0 0 24 24",
615
- fill: "none",
616
- stroke: "currentColor",
617
- strokeWidth: "2",
618
- strokeLinecap: "round",
619
- strokeLinejoin: "round",
620
- children: [
621
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M12 5v14" }),
622
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "m19 12-7 7-7-7" })
623
- ]
624
- }
625
- )
626
- }
627
- ),
590
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `roc-chat-container ${className || ""}`, children: [
591
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
592
+ "div",
593
+ {
594
+ ref: scrollRef,
595
+ className: "roc-chat-container__list",
596
+ children: [
597
+ hasNextPage && isFetchingNextPage && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "roc-chat-container__loading", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LoadingSpinner, { size: "sm" }) }),
628
598
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
629
- ChatInput,
599
+ ChatList,
630
600
  __spreadValues(__spreadValues(__spreadValues({
631
- onSend: handleSend,
632
- isSending
633
- }, disableVoice && { disableVoice }), placeholder && { placeholder }), inputClassName && { className: inputClassName })
601
+ messages: mappedMessages
602
+ }, messageRenderer && { messageRenderer }), loadingRenderer && { loadingRenderer }), listClassName && { className: listClassName })
634
603
  )
635
- ] })
636
- ]
637
- }
638
- ) });
604
+ ]
605
+ }
606
+ ),
607
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "roc-chat-container__input", children: [
608
+ !isAtBottom && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
609
+ "button",
610
+ {
611
+ className: "roc-chat-container__scroll-button",
612
+ onClick: scrollToBottom,
613
+ "aria-label": "scroll to bottom",
614
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
615
+ "svg",
616
+ {
617
+ xmlns: "http://www.w3.org/2000/svg",
618
+ width: "24",
619
+ height: "24",
620
+ viewBox: "0 0 24 24",
621
+ fill: "none",
622
+ stroke: "currentColor",
623
+ strokeWidth: "2",
624
+ strokeLinecap: "round",
625
+ strokeLinejoin: "round",
626
+ children: [
627
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M12 5v14" }),
628
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "m19 12-7 7-7-7" })
629
+ ]
630
+ }
631
+ )
632
+ }
633
+ ),
634
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
635
+ ChatInput,
636
+ __spreadValues(__spreadValues(__spreadValues({
637
+ onSend: handleSend,
638
+ isSending
639
+ }, disableVoice && { disableVoice }), placeholder && { placeholder }), inputClassName && { className: inputClassName })
640
+ )
641
+ ] })
642
+ ] }) });
639
643
  }
640
644
 
641
645
  // src/hooks/useChat.ts
package/dist/index.mjs CHANGED
@@ -18,6 +18,46 @@ var __spreadValues = (a, b) => {
18
18
  };
19
19
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
20
 
21
+ // #style-inject:#style-inject
22
+ function styleInject(css, { insertAt } = {}) {
23
+ if (!css || typeof document === "undefined") return;
24
+ const head = document.head || document.getElementsByTagName("head")[0];
25
+ const style = document.createElement("style");
26
+ style.type = "text/css";
27
+ if (insertAt === "top") {
28
+ if (head.firstChild) {
29
+ head.insertBefore(style, head.firstChild);
30
+ } else {
31
+ head.appendChild(style);
32
+ }
33
+ } else {
34
+ head.appendChild(style);
35
+ }
36
+ if (style.styleSheet) {
37
+ style.styleSheet.cssText = css;
38
+ } else {
39
+ style.appendChild(document.createTextNode(css));
40
+ }
41
+ }
42
+
43
+ // src/styles/chatMessage.css
44
+ styleInject(".roc-chat-message {\n display: flex;\n align-items: flex-start;\n margin-bottom: 1rem;\n width: 100%;\n box-sizing: border-box;\n}\n.roc-chat-message--left {\n justify-content: flex-start;\n}\n.roc-chat-message--right {\n justify-content: flex-end;\n}\n.roc-chat-message__icon {\n width: 40px;\n height: 40px;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: #f3f4f6;\n border-radius: 9999px;\n border: 2px solid #000;\n flex-shrink: 0;\n margin-right: 0.5rem;\n}\n.roc-chat-message__icon svg {\n display: block;\n}\n.roc-chat-message__bubble {\n padding: 0.75rem;\n max-width: calc(100% - 3rem);\n max-height: 24rem;\n overflow-y: auto;\n white-space: pre-wrap;\n word-break: break-word;\n font-size: 0.875rem;\n line-height: 1.4;\n box-sizing: border-box;\n}\n.roc-chat-message__bubble--ai {\n background-color: #f3f4f6;\n border-radius: 0.75rem;\n}\n.roc-chat-message__bubble--user {\n background-color: #ffffff;\n border: 1px solid #e5e7eb;\n border-radius: 0.75rem 0.75rem 0 0.75rem;\n}\n");
45
+
46
+ // src/styles/chatList.css
47
+ styleInject(".roc-chat-list {\n display: flex;\n flex-direction: column;\n}\n");
48
+
49
+ // src/styles/chatInput.css
50
+ styleInject('.roc-chat-input {\n display: flex;\n border: 1px solid #d1d5db;\n padding: 8px 8px;\n border-radius: 24px;\n}\n.roc-chat-input__textarea {\n width: 100%;\n resize: none;\n border: none;\n font-size: 16px;\n line-height: 2.2;\n outline: none;\n overflow: hidden;\n padding: 0;\n padding-left: 8px;\n}\n.roc-chat-input__textarea::-moz-placeholder {\n color: #9ca3af;\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n "Apple SD Gothic Neo",\n "Noto Sans KR",\n "Noto Sans",\n system-ui,\n sans-serif;\n}\n.roc-chat-input__textarea::placeholder {\n color: #9ca3af;\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n "Segoe UI",\n "Apple SD Gothic Neo",\n "Noto Sans KR",\n "Noto Sans",\n system-ui,\n sans-serif;\n}\n.roc-chat-input__button {\n position: relative;\n width: 36px;\n height: 36px;\n margin-top: auto;\n flex-shrink: 0;\n background: none;\n border: none;\n margin-left: 8px;\n cursor: pointer;\n}\n.roc-chat-input__layer {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 24px;\n opacity: 0;\n transition: opacity 150ms ease;\n}\n.roc-chat-input__layer.is-active {\n opacity: 1;\n}\n.roc-chat-input__layer--mic {\n background: #f3f4f6;\n color: #374151;\n}\n.roc-chat-input__layer--recording {\n background: #dc2626;\n color: #ffffff;\n}\n.roc-chat-input__layer--send {\n background: #000000;\n color: #ffffff;\n}\n.roc-chat-input__layer--sending {\n background: #9ca3af;\n color: #ffffff;\n}\n');
51
+
52
+ // src/styles/chatContainer.css
53
+ styleInject(".roc-chat-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n.roc-chat-container__list {\n flex: 1;\n overflow-y: auto;\n padding: 8px;\n}\n.roc-chat-container__loading {\n display: flex;\n justify-content: center;\n padding: 8px 0;\n}\n.roc-chat-container__input {\n position: relative;\n flex-shrink: 0;\n}\n.roc-chat-container__scroll-button {\n position: absolute;\n bottom: 80px;\n left: 50%;\n transform: translateX(-50%);\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: #ffffff;\n border: 1px solid #e5e7eb;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n}\n");
54
+
55
+ // src/styles/loadingSpinner.css
56
+ styleInject(".roc-spinner-wrapper {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.roc-spinner {\n border: 4px solid #e5e7eb;\n border-right-color: transparent;\n border-radius: 50%;\n animation: roc-spin 1s linear infinite;\n}\n@keyframes roc-spin {\n to {\n transform: rotate(1turn);\n }\n}\n");
57
+
58
+ // src/styles/sendingDots.css
59
+ styleInject(".roc-sending-dots {\n display: inline-flex;\n justify-content: space-between;\n align-items: center;\n}\n.roc-sending-dot {\n font-weight: 800;\n line-height: 1;\n animation: roc-sending-bounce 1.4s ease-in-out infinite;\n}\n@keyframes roc-sending-bounce {\n 0%, to {\n opacity: .2;\n transform: translateY(0);\n }\n 50% {\n opacity: 1;\n transform: translateY(-2px);\n }\n}\n");
60
+
21
61
  // src/components/indicators/LoadingSpinner.tsx
22
62
  import { jsx } from "react/jsx-runtime";
23
63
  function LoadingSpinner({ size = "md" }) {
@@ -105,12 +145,12 @@ function ChatMessage({
105
145
  loadingRenderer
106
146
  }) {
107
147
  const isAI = role === "AI";
108
- const justify = position === "auto" ? isAI ? "justify-start" : "justify-end" : position === "left" ? "justify-start" : "justify-end";
148
+ const justify = position === "auto" ? isAI ? "left" : "right" : position;
109
149
  const defaultAIIcon = /* @__PURE__ */ jsxs2(
110
150
  "svg",
111
151
  {
112
152
  xmlns: "http://www.w3.org/2000/svg",
113
- className: `${aiIconColor}`,
153
+ className: aiIconColor,
114
154
  width: "24",
115
155
  height: "24",
116
156
  viewBox: "0 0 24 24",
@@ -129,30 +169,41 @@ function ChatMessage({
129
169
  ]
130
170
  }
131
171
  );
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
172
+ return /* @__PURE__ */ jsxs2(
173
+ "div",
174
+ {
175
+ className: `
176
+ roc-chat-message
177
+ roc-chat-message--${justify}
178
+ ${wrapperClassName}
179
+ `,
180
+ children: [
181
+ isAI && /* @__PURE__ */ jsx3(
182
+ "div",
183
+ {
184
+ className: `
185
+ roc-chat-message__icon
138
186
  ${aiIconWrapperClassName}
139
187
  `,
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}`}
188
+ children: icon || defaultAIIcon
189
+ }
190
+ ),
191
+ /* @__PURE__ */ jsx3(
192
+ "div",
193
+ {
194
+ className: `
195
+ roc-chat-message__bubble
196
+ ${isAI ? "roc-chat-message__bubble--ai" : "roc-chat-message__bubble--user"}
197
+ ${isAI ? aiBubbleClassName : userBubbleClassName}
150
198
  ${bubbleClassName}
151
199
  `,
152
- children: isLoading ? loadingRenderer != null ? loadingRenderer : /* @__PURE__ */ jsx3(LoadingSpinner, { size: "xs" }) : content
153
- }
154
- )
155
- ] }, id);
200
+ children: isLoading ? loadingRenderer != null ? loadingRenderer : /* @__PURE__ */ jsx3(LoadingSpinner, { size: "xs" }) : content
201
+ }
202
+ )
203
+ ]
204
+ },
205
+ id
206
+ );
156
207
  }
157
208
 
158
209
  // src/components/ChatList.tsx
@@ -166,7 +217,7 @@ function ChatList({
166
217
  loadingRenderer
167
218
  }) {
168
219
  const mappedMessages = messageMapper ? messages.map((msg) => __spreadValues(__spreadValues({}, msg), messageMapper(msg))) : messages;
169
- return /* @__PURE__ */ jsx4("div", { className: `flex flex-col ${className}`, children: mappedMessages.map((msg) => {
220
+ return /* @__PURE__ */ jsx4("div", { className: `roc-chat-list ${className != null ? className : ""}`, children: mappedMessages.map((msg) => {
170
221
  if (messageRenderer) {
171
222
  return /* @__PURE__ */ jsx4(React2.Fragment, { children: messageRenderer(msg) }, msg.id);
172
223
  }
@@ -344,134 +395,95 @@ function ChatInput({
344
395
  return null;
345
396
  };
346
397
  const activeLayer = getActivityLayer();
347
- return /* @__PURE__ */ jsxs3(
348
- "div",
349
- {
350
- className: `
351
- flex border border-gray-300 p-2 rounded-3xl
352
- ${className}
353
- `,
354
- children: [
355
- /* @__PURE__ */ jsx5(
356
- "textarea",
357
- {
358
- ref: textareaRef,
359
- value: text,
360
- onChange: handleChange,
361
- placeholder,
362
- rows: 1,
363
- onKeyDown: handleKeyDown,
364
- className: `
365
- w-full px-3 py-2
366
- resize-none border-none
367
- text-sm focus:outline-none
368
- overflow-hidden chatinput-scroll
369
- ${inputClassName}
370
- `
371
- }
372
- ),
373
- /* @__PURE__ */ jsxs3(
374
- "button",
375
- {
376
- type: "button",
377
- disabled: isSending,
378
- onClick: activeLayer === "mic" || activeLayer === "recording" ? handleRecord : handleSend,
379
- className: "relative w-10 h-10 ml-2 mt-auto flex-shrink-0",
380
- children: [
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 === "mic" ? "opacity-100" : "opacity-0"}
388
- bg-gray-100 text-gray-700
389
- ${(micButton == null ? void 0 : micButton.className) || ""}
390
- `,
391
- children: (micButton == null ? void 0 : micButton.icon) || /* @__PURE__ */ jsxs3("svg", { width: "24", height: "24", stroke: "currentColor", fill: "none", strokeWidth: "2", children: [
392
- /* @__PURE__ */ jsx5("path", { d: "M12 19v3" }),
393
- /* @__PURE__ */ jsx5("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
394
- /* @__PURE__ */ jsx5("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
395
- ] })
396
- }
397
- ),
398
- /* @__PURE__ */ jsx5(
399
- "div",
400
- {
401
- className: `
402
- absolute inset-0 flex items-center justify-center rounded-3xl
403
- transition-opacity duration-150
404
- ${activeLayer === "recording" ? "opacity-100" : "opacity-0"}
405
- bg-red-600 text-white
406
- ${(recordingButton == null ? void 0 : recordingButton.className) || ""}
407
- `,
408
- children: (recordingButton == null ? void 0 : recordingButton.icon) || /* @__PURE__ */ jsxs3("svg", { width: "24", height: "24", stroke: "currentColor", fill: "none", strokeWidth: "2", children: [
409
- /* @__PURE__ */ jsx5("path", { d: "M12 19v3" }),
410
- /* @__PURE__ */ jsx5("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
411
- /* @__PURE__ */ jsx5("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
412
- ] })
413
- }
414
- ),
415
- /* @__PURE__ */ jsx5(
416
- "div",
398
+ return /* @__PURE__ */ jsxs3("div", { className: `roc-chat-input ${className}`, children: [
399
+ /* @__PURE__ */ jsx5(
400
+ "textarea",
401
+ {
402
+ ref: textareaRef,
403
+ value: text,
404
+ onChange: handleChange,
405
+ placeholder,
406
+ rows: 1,
407
+ onKeyDown: handleKeyDown,
408
+ className: `roc-chat-input__textarea ${inputClassName}`
409
+ }
410
+ ),
411
+ /* @__PURE__ */ jsxs3(
412
+ "button",
413
+ {
414
+ type: "button",
415
+ disabled: isSending,
416
+ onClick: activeLayer === "mic" || activeLayer === "recording" ? handleRecord : handleSend,
417
+ className: "roc-chat-input__button",
418
+ children: [
419
+ /* @__PURE__ */ jsx5(
420
+ "div",
421
+ {
422
+ className: `roc-chat-input__layer roc-chat-input__layer--mic ${activeLayer === "mic" ? "is-active" : ""} ${(micButton == null ? void 0 : micButton.className) || ""}`,
423
+ children: (micButton == null ? void 0 : micButton.icon) || /* @__PURE__ */ jsxs3("svg", { width: "24", height: "24", stroke: "currentColor", fill: "none", strokeWidth: "2", children: [
424
+ /* @__PURE__ */ jsx5("path", { d: "M12 19v3" }),
425
+ /* @__PURE__ */ jsx5("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
426
+ /* @__PURE__ */ jsx5("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
427
+ ] })
428
+ }
429
+ ),
430
+ /* @__PURE__ */ jsx5(
431
+ "div",
432
+ {
433
+ className: `roc-chat-input__layer roc-chat-input__layer--recording ${activeLayer === "recording" ? "is-active" : ""} ${(recordingButton == null ? void 0 : recordingButton.className) || ""}`,
434
+ children: (recordingButton == null ? void 0 : recordingButton.icon) || /* @__PURE__ */ jsxs3("svg", { width: "24", height: "24", stroke: "currentColor", fill: "none", strokeWidth: "2", children: [
435
+ /* @__PURE__ */ jsx5("path", { d: "M12 19v3" }),
436
+ /* @__PURE__ */ jsx5("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
437
+ /* @__PURE__ */ jsx5("rect", { x: "9", y: "2", width: "6", height: "13", rx: "3" })
438
+ ] })
439
+ }
440
+ ),
441
+ /* @__PURE__ */ jsx5(
442
+ "div",
443
+ {
444
+ className: `roc-chat-input__layer roc-chat-input__layer--send ${activeLayer === "send" ? "is-active" : ""} ${(sendButton == null ? void 0 : sendButton.className) || ""}`,
445
+ children: (sendButton == null ? void 0 : sendButton.icon) || /* @__PURE__ */ jsxs3(
446
+ "svg",
417
447
  {
418
- className: `
419
- absolute inset-0 flex items-center justify-center rounded-3xl
420
- transition-opacity duration-150
421
- ${activeLayer === "send" ? "opacity-100" : "opacity-0"}
422
- bg-black text-white
423
- ${(sendButton == null ? void 0 : sendButton.className) || ""}
424
- `,
425
- children: (sendButton == null ? void 0 : sendButton.icon) || /* @__PURE__ */ jsxs3(
426
- "svg",
427
- {
428
- width: "20",
429
- height: "20",
430
- viewBox: "0 0 22 24",
431
- fill: "none",
432
- stroke: "currentColor",
433
- "stroke-width": "2",
434
- children: [
435
- /* @__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" }),
436
- /* @__PURE__ */ jsx5("path", { d: "M6 12h16" })
437
- ]
438
- }
439
- )
448
+ width: "20",
449
+ height: "20",
450
+ viewBox: "0 0 22 24",
451
+ fill: "none",
452
+ stroke: "currentColor",
453
+ "stroke-width": "2",
454
+ children: [
455
+ /* @__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" }),
456
+ /* @__PURE__ */ jsx5("path", { d: "M6 12h16" })
457
+ ]
440
458
  }
441
- ),
442
- /* @__PURE__ */ jsx5(
443
- "div",
459
+ )
460
+ }
461
+ ),
462
+ /* @__PURE__ */ jsx5(
463
+ "div",
464
+ {
465
+ className: `roc-chat-input__layer roc-chat-input__layer--sending ${activeLayer === "sending" ? "is-active" : ""} ${(sendingButton == null ? void 0 : sendingButton.className) || ""}`,
466
+ children: (sendingButton == null ? void 0 : sendingButton.icon) || /* @__PURE__ */ jsxs3(
467
+ "svg",
444
468
  {
445
- className: `
446
- absolute inset-0 flex items-center justify-center rounded-3xl
447
- transition-opacity duration-150
448
- ${activeLayer === "sending" ? "opacity-100" : "opacity-0"}
449
- bg-gray-400 text-white
450
- ${(sendingButton == null ? void 0 : sendingButton.className) || ""}
451
- `,
452
- children: (sendingButton == null ? void 0 : sendingButton.icon) || /* @__PURE__ */ jsxs3(
453
- "svg",
454
- {
455
- width: "20",
456
- height: "20",
457
- viewBox: "0 0 22 24",
458
- fill: "none",
459
- stroke: "currentColor",
460
- "stroke-width": "2",
461
- children: [
462
- /* @__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" }),
463
- /* @__PURE__ */ jsx5("path", { d: "M6 12h16" })
464
- ]
465
- }
466
- )
469
+ width: "20",
470
+ height: "20",
471
+ viewBox: "0 0 22 24",
472
+ fill: "none",
473
+ stroke: "currentColor",
474
+ "stroke-width": "2",
475
+ children: [
476
+ /* @__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" }),
477
+ /* @__PURE__ */ jsx5("path", { d: "M6 12h16" })
478
+ ]
467
479
  }
468
480
  )
469
- ]
470
- }
471
- )
472
- ]
473
- }
474
- );
481
+ }
482
+ )
483
+ ]
484
+ }
485
+ )
486
+ ] });
475
487
  }
476
488
 
477
489
  // src/components/ChatContainer.tsx
@@ -534,67 +546,59 @@ function ChatContainer(props) {
534
546
  });
535
547
  await onSend(value);
536
548
  };
537
- return /* @__PURE__ */ jsx6(Fragment, { children: /* @__PURE__ */ jsxs4(
538
- "div",
539
- {
540
- className: `
541
- flex flex-col ${className || ""}
542
- `,
543
- children: [
544
- /* @__PURE__ */ jsxs4(
545
- "div",
546
- {
547
- ref: scrollRef,
548
- className: `flex-1 overflow-y-auto chatContainer-scroll p-2`,
549
- children: [
550
- hasNextPage && isFetchingNextPage && /* @__PURE__ */ jsx6("div", { className: "flex justify-center py-2", children: /* @__PURE__ */ jsx6(LoadingSpinner, { size: "sm" }) }),
551
- /* @__PURE__ */ jsx6(
552
- ChatList,
553
- __spreadValues(__spreadValues(__spreadValues({
554
- messages: mappedMessages
555
- }, messageRenderer && { messageRenderer }), loadingRenderer && { loadingRenderer }), listClassName && { className: listClassName })
556
- )
557
- ]
558
- }
559
- ),
560
- /* @__PURE__ */ jsxs4("div", { className: "flex-shrink-0 relative", children: [
561
- !isAtBottom && /* @__PURE__ */ jsx6(
562
- "button",
563
- {
564
- onClick: scrollToBottom,
565
- 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 ",
566
- "aria-label": "scroll to bottom",
567
- children: /* @__PURE__ */ jsxs4(
568
- "svg",
569
- {
570
- xmlns: "http://www.w3.org/2000/svg",
571
- width: "24",
572
- height: "24",
573
- viewBox: "0 0 24 24",
574
- fill: "none",
575
- stroke: "currentColor",
576
- strokeWidth: "2",
577
- strokeLinecap: "round",
578
- strokeLinejoin: "round",
579
- children: [
580
- /* @__PURE__ */ jsx6("path", { d: "M12 5v14" }),
581
- /* @__PURE__ */ jsx6("path", { d: "m19 12-7 7-7-7" })
582
- ]
583
- }
584
- )
585
- }
586
- ),
549
+ return /* @__PURE__ */ jsx6(Fragment, { children: /* @__PURE__ */ jsxs4("div", { className: `roc-chat-container ${className || ""}`, children: [
550
+ /* @__PURE__ */ jsxs4(
551
+ "div",
552
+ {
553
+ ref: scrollRef,
554
+ className: "roc-chat-container__list",
555
+ children: [
556
+ hasNextPage && isFetchingNextPage && /* @__PURE__ */ jsx6("div", { className: "roc-chat-container__loading", children: /* @__PURE__ */ jsx6(LoadingSpinner, { size: "sm" }) }),
587
557
  /* @__PURE__ */ jsx6(
588
- ChatInput,
558
+ ChatList,
589
559
  __spreadValues(__spreadValues(__spreadValues({
590
- onSend: handleSend,
591
- isSending
592
- }, disableVoice && { disableVoice }), placeholder && { placeholder }), inputClassName && { className: inputClassName })
560
+ messages: mappedMessages
561
+ }, messageRenderer && { messageRenderer }), loadingRenderer && { loadingRenderer }), listClassName && { className: listClassName })
593
562
  )
594
- ] })
595
- ]
596
- }
597
- ) });
563
+ ]
564
+ }
565
+ ),
566
+ /* @__PURE__ */ jsxs4("div", { className: "roc-chat-container__input", children: [
567
+ !isAtBottom && /* @__PURE__ */ jsx6(
568
+ "button",
569
+ {
570
+ className: "roc-chat-container__scroll-button",
571
+ onClick: scrollToBottom,
572
+ "aria-label": "scroll to bottom",
573
+ children: /* @__PURE__ */ jsxs4(
574
+ "svg",
575
+ {
576
+ xmlns: "http://www.w3.org/2000/svg",
577
+ width: "24",
578
+ height: "24",
579
+ viewBox: "0 0 24 24",
580
+ fill: "none",
581
+ stroke: "currentColor",
582
+ strokeWidth: "2",
583
+ strokeLinecap: "round",
584
+ strokeLinejoin: "round",
585
+ children: [
586
+ /* @__PURE__ */ jsx6("path", { d: "M12 5v14" }),
587
+ /* @__PURE__ */ jsx6("path", { d: "m19 12-7 7-7-7" })
588
+ ]
589
+ }
590
+ )
591
+ }
592
+ ),
593
+ /* @__PURE__ */ jsx6(
594
+ ChatInput,
595
+ __spreadValues(__spreadValues(__spreadValues({
596
+ onSend: handleSend,
597
+ isSending
598
+ }, disableVoice && { disableVoice }), placeholder && { placeholder }), inputClassName && { className: inputClassName })
599
+ )
600
+ ] })
601
+ ] }) });
598
602
  }
599
603
 
600
604
  // src/hooks/useChat.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-optimistic-chat",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -11,10 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "test": "echo \"Error: no test specified\" && exit 1",
14
- "build:css": "npx tailwindcss -i ./src/styles/style.css -o ./src/styles/style.css --minify",
15
- "copy:css": "copy src\\styles\\style.css public\\style.css",
16
- "build:lib": "tsup",
17
- "build": "npm run build:css && npm run copy:css && npm run build:lib"
14
+ "build": "tsup"
18
15
  },
19
16
  "files": [
20
17
  "dist"
@@ -42,6 +39,6 @@
42
39
  }
43
40
  },
44
41
  "sideEffects": [
45
- "./dist/style.css"
42
+ "*.css"
46
43
  ]
47
44
  }
package/dist/style.css DELETED
@@ -1 +0,0 @@
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)}}