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/README.md +0 -0
- package/dist/index.d.mts +158 -0
- package/dist/index.d.ts +158 -0
- package/dist/index.js +698 -0
- package/dist/index.mjs +658 -0
- package/dist/style.css +1 -0
- package/package.json +47 -0
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
|
+
});
|