react-optimistic-chat 2.0.0 β†’ 2.1.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ ## πŸ“„ License
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2025
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md CHANGED
@@ -0,0 +1,845 @@
1
+ <div align="center">
2
+ <img width="350" height="240" alt="Image" src="https://github.com/user-attachments/assets/de84f731-14ae-4023-9649-2c22f7747ed1"/>
3
+ </div>
4
+
5
+ # react-optimistic-chat Β· [![npm](https://img.shields.io/npm/v/react-optimistic-chat)](https://www.npmjs.com/package/react-optimistic-chat) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ <code>react-optimistic-chat</code>은 **React + TanStack Query** 기반으둜
8
+ AI 챗봇 μ„œλΉ„μŠ€μ—μ„œ ν•„μš”ν•œ **μ±„νŒ… μΊμ‹œ 관리 및 optimistic update, μ±„νŒ… UI**λ₯Ό μ†μ‰½κ²Œ κ΅¬ν˜„ν•  수 μžˆλ„λ‘ λ•λŠ” λΌμ΄λΈŒλŸ¬λ¦¬μž…λ‹ˆλ‹€.
9
+
10
+ <br>
11
+
12
+ > 이 λΌμ΄λΈŒλŸ¬λ¦¬λŠ” AI 응닡 생성 κΈ°λŠ₯을 ν¬ν•¨ν•˜μ§€ μ•ŠμœΌλ©°,
13
+ > κΈ°μ‘΄ API와 κ²°ν•©ν•΄ μ±„νŒ… μƒνƒœ 관리와 UI κ΅¬ν˜„μ—λ§Œ μ§‘μ€‘ν•©λ‹ˆλ‹€.
14
+
15
+ <br>
16
+
17
+ ## λͺ©μ°¨
18
+ #### **1.** [Install & Requirements](#install--requirements)
19
+ #### **2.** [Quick Start](#quick-start)
20
+ #### **3.** [Core Types](#core-types)
21
+ **\-** [Message](#message)
22
+ **\-** [VoiceRecognition](#voicerecognition)
23
+ #### **4.** [Hooks](#hooks)
24
+ **\-** [useChat](#usechat)
25
+ **\-** [useBrowserSpeechRecognition](#usebrowserspeechrecognition)
26
+ **\-** [useVoiceChat](#usevoicechat)
27
+ #### **5.** [Components](#components)
28
+ **\-** [Indicators](#indicators)
29
+ **\-** [ChatMessage](#chatmessage)
30
+ **\-** [ChatList](#chatlist)
31
+ **\-** [ChatInput](#chatinput)
32
+ **\-** [ChatContainer](#chatcontainer)
33
+ #### **6.** [Notes](#notes)
34
+
35
+ <br>
36
+
37
+ <h1 id="install--requirements">πŸ“¦ Install & Requirements</h1>
38
+
39
+ ## Installation
40
+ ```bash
41
+ npm install react-optimistic-chat
42
+ # or
43
+ yarn add react-optimistic-chat
44
+ ```
45
+
46
+ <br>
47
+
48
+ ## Peer Dependencies
49
+ 이 λΌμ΄λΈŒλŸ¬λ¦¬λŠ” μ•„λž˜ νŒ¨ν‚€μ§€λ“€μ„ **peer dependency**둜 μ‚¬μš©ν•©λ‹ˆλ‹€.
50
+ ν”„λ‘œμ νŠΈμ— λ°˜λ“œμ‹œ μ„€μΉ˜λ˜μ–΄ μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.
51
+ ```json
52
+ {
53
+ "@tanstack/react-query": ">=5",
54
+ "react": ">=18",
55
+ "react-dom": ">=18"
56
+ }
57
+ ```
58
+
59
+ <br>
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
+ <h1 id="quick-start">πŸš€ Quick Start</h1>
73
+
74
+ μ•„λž˜ μ˜ˆμ œλŠ” μ„œλ²„λ‘œλΆ€ν„° μ „λ‹¬λ˜λŠ” Raw μ±„νŒ… 데이터λ₯Ό
75
+ <code>useChat</code>κ³Ό <code>ChatContainer</code>λ₯Ό μ‘°ν•©ν•΄ **μ΅œμ†Œν•œμ˜ μ„€μ •μœΌλ‘œ μ±„νŒ… UIλ₯Ό κ΅¬μ„±ν•˜λŠ” 방법**을 λ³΄μ—¬μ€λ‹ˆλ‹€.
76
+
77
+ **Raw 데이터 β†’ Message νƒ€μž… μ •κ·œν™” β†’ 캐싱 β†’ λ Œλ”λ§**κΉŒμ§€μ˜ 흐름을 ν•œ λ²ˆμ— 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
78
+
79
+ <br>
80
+
81
+ ## 1️⃣ RawMessage
82
+ μ„œλ²„λ‘œλΆ€ν„° μ „λ‹¬λ˜λŠ” μ±„νŒ… λ°μ΄ν„°λŠ” λ‹€μŒκ³Ό 같은 ν˜•νƒœλΌκ³  κ°€μ •ν•©λ‹ˆλ‹€.
83
+ ```ts
84
+ type Raw = {
85
+ chatId: string;
86
+ sender: "ai" | "user";
87
+ body: string;
88
+ };
89
+ ```
90
+
91
+ <br>
92
+
93
+ ## 2️⃣ getChat & sendAI
94
+ μ±„νŒ… λͺ©λ‘μ„ 뢈러였고, μ‚¬μš©μž λ©”μ‹œμ§€λ₯Ό μ„œλ²„λ‘œ μ „μ†‘ν•˜λŠ” ν•¨μˆ˜λŠ” λ‹€μŒκ³Ό 같은 ν˜•νƒœλΌκ³  κ°€μ •ν•©λ‹ˆλ‹€.
95
+ ```ts
96
+ async function getChat(roomId: string, page: number): Promise<Raw[]> {
97
+ const res = await fetch(`/getChat?roomId=${roomId}&page=${page}`);
98
+
99
+ if (!res.ok) {
100
+ throw new Error("μ±„νŒ… 뢈러였기 μ‹€νŒ¨");
101
+ }
102
+
103
+ const json = await res.json();
104
+ return json.result;
105
+ }
106
+
107
+ async function sendAI(content: string): Promise<Raw> {
108
+ const res = await fetch(`/sendAI`, {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({ content }),
112
+ });
113
+
114
+ if (!res.ok) {
115
+ throw new Error("AI 응닡 μ‹€νŒ¨");
116
+ }
117
+
118
+ const json = await res.json();
119
+ return json.result;
120
+ }
121
+ ```
122
+
123
+ <br>
124
+
125
+ ## 3️⃣ ChatExample
126
+ <code>useChat</code> ν›…μœΌλ‘œ λ©”μ‹œμ§€ μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜κ³ ,
127
+ <code>ChatContainer</code> μ»΄ν¬λ„ŒνŠΈμ— 전달해 μ±„νŒ… UI + λ¬΄ν•œ μŠ€ν¬λ‘€μ„ κ΅¬μ„±ν•©λ‹ˆλ‹€.
128
+
129
+ μ΄λ•Œ μ„œλ²„μ˜ Raw 데이터λ₯Ό Message νƒ€μž…μ˜
130
+ <code>id</code>, <code>role</code>, <code>content</code> ν•„λ“œμ— **μ •ν™•νžˆ λ§€ν•‘**ν•©λ‹ˆλ‹€.
131
+
132
+ ```tsx
133
+ export default function ChatExample() {
134
+ const roomId = "room-1";
135
+ const PAGE_SIZE = 8;
136
+
137
+ const {
138
+ messages,
139
+ sendUserMessage,
140
+ isPending,
141
+ fetchNextPage,
142
+ hasNextPage,
143
+ isFetchingNextPage,
144
+ } = useChat<Raw>({
145
+ queryKey: ["chat", roomId],
146
+ queryFn: (pageParam) => getChat(roomId, pageParam as number),
147
+ initialPageParam: 0,
148
+
149
+ getNextPageParam: (lastPage, allPages) =>
150
+ lastPage.length === PAGE_SIZE ? allPages.length : undefined,
151
+
152
+ mutationFn: sendAI,
153
+
154
+ map: (raw) => ({
155
+ id: raw.chatId,
156
+ role: raw.sender === "ai" ? "AI" : "USER",
157
+ content: raw.body,
158
+ }),
159
+ });
160
+
161
+ return (
162
+ <ChatContainer
163
+ className="h-[80vh]"
164
+ messages={messages}
165
+ onSend={sendUserMessage}
166
+ isSending={isPending}
167
+ fetchNextPage={fetchNextPage}
168
+ hasNextPage={hasNextPage}
169
+ isFetchingNextPage={isFetchingNextPage}
170
+ />
171
+ );
172
+ }
173
+ ```
174
+
175
+ <br>
176
+
177
+ ## 4️⃣ VoiceChatExample
178
+ μŒμ„± μž…λ ₯ 기반 μ±„νŒ…μ„ μ‚¬μš©ν•˜κ³  싢은 경우,
179
+ <code>useBrowserSpeechRecognition</code>을 μƒμ„±ν•œ λ’€
180
+ <code>useVoiceChat</code>의 <code>voice</code> μ˜΅μ…˜μœΌλ‘œ μ „λ‹¬ν•˜λ©΄ λ©λ‹ˆλ‹€.
181
+
182
+ ```tsx
183
+ const voice = useBrowserSpeechRecognition();
184
+
185
+ const {
186
+ // μŒμ„± μ œμ–΄μš© API
187
+ startRecording,
188
+ stopRecording,
189
+ ...
190
+ } = useVoiceChat<Raw>({
191
+ voice,
192
+ ...
193
+ });
194
+ ```
195
+
196
+ <br>
197
+
198
+ <h1 id="core-types">🧩 Core Types</h1>
199
+
200
+ <h2 id="message">🧩 Message</h2>
201
+
202
+ <code>react-optimistic-chat</code>은 μ±„νŒ…μ„ λ‹¨μˆœν•œ λ¬Έμžμ—΄ 배열이 μ•„λ‹Œ
203
+ **μΌκ΄€λœ Message νƒ€μž…μ„ μ€‘μ‹¬μœΌλ‘œ 관리**ν•˜λ„λ‘ μ„€κ³„λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
204
+
205
+ λͺ¨λ“  Hooks와 UI μ»΄ν¬λ„ŒνŠΈλŠ” 이 Core Type을 κΈ°μ€€μœΌλ‘œ λ™μž‘ν•˜λ©°,
206
+ μ„œλ²„λ‘œλΆ€ν„° μ „λ‹¬λ˜λŠ” λ‹€μ–‘ν•œ ν˜•νƒœμ˜ Raw 데이터λ₯Ό **예츑 κ°€λŠ₯ν•œ ꡬ쑰둜 μ •κ·œν™”**ν•˜λŠ” 것을 λͺ©ν‘œλ‘œ ν•©λ‹ˆλ‹€.
207
+ ```ts
208
+ type Message = {
209
+ id: number | string;
210
+ role: "USER" | "AI";
211
+ content: string;
212
+ isLoading?: boolean;
213
+ custom?: Record<string, unknown>;
214
+ };
215
+ ```
216
+
217
+ | field | type | description |
218
+ |------|------|-------------|
219
+ | `id` | `number \| string` | λ©”μ‹œμ§€λ₯Ό μ‹λ³„ν•˜κΈ° μœ„ν•œ 고유 κ°’ |
220
+ | `role` | `"USER" \| "AI"` | λ©”μ‹œμ§€μ˜ 주체<br/>`"USER"`: μ‚¬μš©μžκ°€ μž…λ ₯ν•œ λ©”μ‹œμ§€<br>`"AI"`: AIκ°€ μƒμ„±ν•œ 응닡 λ©”μ‹œμ§€ |
221
+ | `content` | `string` | λ©”μ‹œμ§€μ— ν‘œμ‹œλ  ν…μŠ€νŠΈ λ‚΄μš© |
222
+ | `isLoading` | `boolean` _(optional)_ | AI 응닡을 κΈ°λ‹€λ¦¬λŠ” 쀑인 λ©”μ‹œμ§€μž„μ„ λ‚˜νƒ€λ‚΄λŠ” ν”Œλž˜κ·Έ<br>optimistic update μ‹œ UI μƒνƒœ ν‘œν˜„μ— μ‚¬μš© |
223
+ | `custom` | `Record<string, unknown>` | μ„œλ²„μ—μ„œ μ „λ‹¬λœ Raw 데이터 쀑 `id`, `role`, `content`에<br>ν¬ν•¨λ˜μ§€ μ•Šμ€ λͺ¨λ“  ν•„λ“œλ₯Ό λ³΄μ‘΄ν•˜λŠ” 객체 |
224
+
225
+ <br>
226
+
227
+ ## Example: \<Raw> β†’ \<Message> μ •κ·œν™”
228
+ ```ts
229
+ type Raw = {
230
+ messageId: string;
231
+ sender: "user" | "assistant";
232
+ text: string;
233
+ createdAt: string;
234
+ model: string;
235
+ };
236
+ ```
237
+ μ„œλ²„λ‘œλΆ€ν„° λ‹€μŒκ³Ό 같은 <code>Raw</code> μ±„νŒ… 데이터가 μ „λ‹¬λœλ‹€κ³  κ°€μ •ν•©λ‹ˆλ‹€.
238
+
239
+ ```ts
240
+ map: (raw: RawMessage) => ({
241
+ id: raw.messageId,
242
+ role: raw.sender === "user" ? "USER" : "AI",
243
+ content: raw.text,
244
+ });
245
+ ```
246
+ Hookμ—μ„œ ν•„μˆ˜λ‘œ μ œκ³΅ν•˜λŠ” <code>map</code> ν•¨μˆ˜λ₯Ό λ‹€μŒκ³Ό 같이 μ •μ˜ν•˜λ©΄
247
+
248
+ ```ts
249
+ {
250
+ id: "abc123",
251
+ role: "AI",
252
+ content: "Hello! How can I help you?",
253
+ custom: {
254
+ createdAt: "2024-01-01T10:00:00Z",
255
+ model: "gpt-4o"
256
+ }
257
+ }
258
+ ```
259
+ λ‚΄λΆ€μ μœΌλ‘œ <code>Message</code>λŠ” μ•„λž˜μ™€ 같이 μ •κ·œν™”λ©λ‹ˆλ‹€.
260
+
261
+ <br>
262
+
263
+ <h2 id="voicerecognition">🧩 VoiceRecognition</h2>
264
+
265
+ <code>react-optimistic-chat</code>은 μŒμ„± μž…λ ₯을 λ‹¨μˆœν•œ λΈŒλΌμš°μ € API 호좜이 μ•„λ‹Œ
266
+ **μΌκ΄€λœ VoiceRecognition μΈν„°νŽ˜μ΄μŠ€λ₯Ό 톡해 좔상화**ν•˜λ„λ‘ μ„€κ³„λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
267
+
268
+ 이λ₯Ό 톡해 μž…λ ₯ 방식(λΈŒλΌμš°μ €, μ™ΈλΆ€ SDK, μ»€μŠ€ν…€ STT λ“±)에 관계없이
269
+ <code>useVoiceChat</code> ν›…κ³Ό <code>ChatInput</code> μ»΄ν¬λ„ŒνŠΈμ—μ„œ λ™μΌν•œ λ°©μ‹μœΌλ‘œ μŒμ„± 인식 μƒνƒœλ₯Ό μ œμ–΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
270
+
271
+ ```ts
272
+ type VoiceRecognition = {
273
+ start: () => void;
274
+ stop: () => void;
275
+ isRecording: boolean;
276
+ onTranscript?: (text: string) => void;
277
+ }
278
+ ```
279
+
280
+ | field | type | description |
281
+ | -------------- | ------------------------ | ------------------- |
282
+ | `start` | `() => void` | μŒμ„± 인식을 μ‹œμž‘ν•˜λŠ” ν•¨μˆ˜ |
283
+ | `stop` | `() => void` | μŒμ„± 인식을 μ€‘λ‹¨ν•˜λŠ” ν•¨μˆ˜ |
284
+ | `isRecording` | `boolean` | ν˜„μž¬ μŒμ„± 인식이 μ§„ν–‰ 쀑인지 μ—¬λΆ€ |
285
+ | `onTranscript` | `(text: string) => void` | μΈμ‹λœ μŒμ„± ν…μŠ€νŠΈλ₯Ό μ „λ‹¬λ°›λŠ” 콜백<br>β€’ `useVoiceChat`μ—μ„œλŠ” ν•„μˆ˜<br>β€’ `ChatInput`μ—μ„œλŠ” λ‚΄λΆ€μ—μ„œ μžλ™μœΌλ‘œ 처리 |
286
+
287
+
288
+
289
+ <br>
290
+
291
+ <h1 id="hooks">πŸͺ Hooks</h1>
292
+
293
+ <h2 id="usechat">πŸͺ useChat</h2>
294
+
295
+ <code>useChat</code>은 **TanStack Query의 μΊμ‹œλ₯Ό 기반으둜**
296
+ AI 챗봇 μ„œλΉ„μŠ€μ— ν•„μš”ν•œ **μ±„νŒ… νžˆμŠ€ν† λ¦¬ 관리, optimistic update, λ©”μ‹œμ§€ μ •κ·œν™”**λ₯Ό ν•œ λ²ˆμ— μ œκ³΅ν•˜λŠ” Hookμž…λ‹ˆλ‹€.
297
+
298
+ - <code>useInfiniteQuery</code> 기반 **μ±„νŒ… νžˆμŠ€ν† λ¦¬ 관리**
299
+ - μ±„νŒ… 내역을 νŽ˜μ΄μ§€ λ‹¨μœ„λ‘œ μΊμ‹œμ— μ €μž₯
300
+ - 이미 λ‘œλ“œλœ νŽ˜μ΄μ§€λŠ” μž¬μš”μ²­ 없이 μΊμ‹œμ—μ„œ μ¦‰μ‹œ 볡원
301
+ - μ‚¬μš©μž λ©”μ‹œμ§€ 전솑 μ‹œ **Optimistic Update 적용**
302
+ - μ„œλ²„ 응닡을 기닀리지 μ•Šκ³  UI에 μ¦‰μ‹œ 반영
303
+ - AI 응닡 λŒ€κΈ° 쀑 μƒνƒœλ₯Ό <code>isPending</code>으둜 제곡
304
+ - μ„œλ²„λ‘œλΆ€ν„° 받은 Raw 데이터λ₯Ό **μΌκ΄€λœ Message ꡬ쑰둜 μ •κ·œν™”**
305
+ - <code>id</code>, <code>role</code>, <code>content</code>λŠ” μ΅œμƒμœ„ ν•„λ“œλ‘œ μœ μ§€
306
+ - Message에 ν¬ν•¨λ˜μ§€ μ•Šμ€ λ‚˜λ¨Έμ§€ Raw ν•„λ“œλŠ” <code>custom</code> μ˜μ—­μ— μžλ™ 보쑴
307
+ - TanStack Query의 μΊμ‹œ λ©”μ»€λ‹ˆμ¦˜μ„ ν™œμš©ν•œ **μ•ˆμ •μ μΈ μƒνƒœ 동기화**
308
+ - mutation μ‹€νŒ¨ μ‹œ 이전 μΊμ‹œ μƒνƒœλ‘œ rollback
309
+ - <code>staleTime</code>, <code>gcTime</code>을 ν†΅ν•œ μΊμ‹œ 수λͺ… μ œμ–΄
310
+
311
+ <br>
312
+
313
+ ### Usage
314
+ ```ts
315
+ const {
316
+ messages,
317
+ sendUserMessage,
318
+ isPending,
319
+ isInitialLoading,
320
+ fetchNextPage,
321
+ hasNextPage,
322
+ isFetchingNextPage,
323
+ } = useChat({
324
+ queryKey: ["chat", roomId],
325
+ queryFn: getChat,
326
+ initialPageParam: 0,
327
+ getNextPageParam,
328
+ mutationFn: sendAI,
329
+ map: (raw) => ({
330
+ id: raw.chatId,
331
+ role: raw.sender === "ai" ? "AI" : "USER",
332
+ content: raw.body,
333
+ }),
334
+ });
335
+ ```
336
+
337
+ <br>
338
+
339
+ ### Returned Values
340
+ | name | type | description |
341
+ |------|------|-------------|
342
+ | `messages` | `Message[]` | μ •κ·œν™”λœ λ©”μ‹œμ§€ λ°°μ—΄ |
343
+ | `sendUserMessage` | `(content: string) => void` | μœ μ € λ©”μ‹œμ§€ 전솑 ν•¨μˆ˜ |
344
+ | `isPending` | `boolean` | AI 응닡 λŒ€κΈ° μƒνƒœ |
345
+ | `isInitialLoading` | `boolean` | `messages` λ‘œλ”© μƒνƒœ |
346
+ | `fetchNextPage` | `() => Promise<unknown>` | λ‹€μŒ μ±„νŒ… νŽ˜μ΄μ§€ μš”μ²­ |
347
+ | `hasNextPage` | `boolean \| undefined` | λ‹€μŒ νŽ˜μ΄μ§€ 쑴재 μ—¬λΆ€ |
348
+ | `isFetchingNextPage` | `boolean` | νŽ˜μ΄μ§€ λ‘œλ”© μƒνƒœ |
349
+
350
+ <br>
351
+
352
+ ### Options
353
+ | name | type | required | description |
354
+ |------|------|----------|-------------|
355
+ | `queryKey` | `readonly unknown[]` | βœ… | ν•΄λ‹Ή μ±„νŒ…μ˜ TanStack Query key |
356
+ | `queryFn` | `(pageParam: unknown) => Promise<Raw[]>` | βœ… | κΈ°μ‘΄ μ±„νŒ… 내역을 λΆˆλŸ¬μ˜€λŠ” ν•¨μˆ˜ |
357
+ | `initialPageParam` | `unknown` | βœ… | 첫 νŽ˜μ΄μ§€ μš”μ²­ μ‹œ μ‚¬μš©ν•  pageParam |
358
+ | `getNextPageParam` | `(lastPage: Message[], allPages: Message[][]) => unknown` | βœ… | λ‹€μŒ νŽ˜μ΄μ§€ μš”μ²­μ„ μœ„ν•œ pageParam 계산 ν•¨μˆ˜ |
359
+ | `mutationFn` | `(content: string) => Promise<Raw>` | βœ… | μœ μ € μž…λ ₯을 λ°›μ•„ AI 응닡 1개λ₯Ό λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜ |
360
+ | `map` | `(raw: Raw) => { id; role; content }` | βœ… | Raw 데이터λ₯Ό Message ꡬ쑰둜 λ§€ν•‘ν•˜λŠ” ν•¨μˆ˜ |
361
+ | `onError` | `(error: unknown) => void` | ❌ | mutation μ—λŸ¬ λ°œμƒ μ‹œ ν˜ΈμΆœλ˜λŠ” 콜백 |
362
+ | `staleTime` | `number` | ❌ | μΊμ‹œκ°€ fresh μƒνƒœλ‘œ μœ μ§€λ˜λŠ” μ‹œκ°„ (ms) |
363
+ | `gcTime` | `number` | ❌ | μΊμ‹œκ°€ GC 되기 μ „κΉŒμ§€ μœ μ§€λ˜λŠ” μ‹œκ°„ (ms) |
364
+
365
+ <br>
366
+
367
+ ### πŸ” Optimistic Update Flow
368
+ **1.** μ‚¬μš©μžκ°€ λ©”μ‹œμ§€ 전솑
369
+ **2.** USER λ©”μ‹œμ§€ + λ‘œλ”© 쀑인 AI λ©”μ‹œμ§€λ₯Ό μ¦‰μ‹œ μΊμ‹œμ— μ‚½μž…
370
+ **3.** AI 응닡이 도착
371
+ **4.** λ‘œλ”© 쀑인 AI λ©”μ‹œμ§€λ₯Ό μ‹€μ œ μ‘λ‹΅μœΌλ‘œ ꡐ체
372
+ **5.** μ—λŸ¬ λ°œμƒ μ‹œ 이전 μƒνƒœλ‘œ rollback
373
+
374
+ <br>
375
+
376
+ <h2 id="usebrowserspeechrecognition">πŸͺ useBrowserSpeechRecognition</h2>
377
+
378
+ <code>useBrowserSpeechRecognition</code>은 λΈŒλΌμš°μ €μ—μ„œ μ œκ³΅ν•˜λŠ”
379
+ Speech Recognition APIλ₯Ό **React Hook ν˜•νƒœλ‘œ μΆ”μƒν™”ν•œ ν›…**μž…λ‹ˆλ‹€.
380
+
381
+ 이 훅은 μŒμ„± 인식 λ‘œμ§μ„ 직접 닀루지 μ•Šκ³ λ„, <code>useVoiceChat</code>μ΄λ‚˜ <code>ChatInput</code>κ³Ό 같은 Hook/UIμ—μ„œ
382
+ **μŒμ„± μž…λ ₯ κΈ°λŠ₯을 κ°„νŽΈν•˜κ²Œ μ‚¬μš©ν•˜κ³  싢은 μ‚¬μš©μž**λ₯Ό μœ„ν•΄ μ œκ³΅λ©λ‹ˆλ‹€.
383
+
384
+ - λΈŒλΌμš°μ € λ‚΄μž₯ μŒμ„± 인식 APIλ₯Ό κ°„λ‹¨ν•œ μΈν„°νŽ˜μ΄μŠ€λ‘œ 제곡
385
+ - μŒμ„± 인식 μ‹œμž‘ / μ’…λ£Œ μ œμ–΄
386
+ - ν˜„μž¬ λ…ΉμŒ μƒνƒœλ₯Ό λ‚˜νƒ€λ‚΄λŠ” <code>isRecording</code> 제곡
387
+ - μŒμ„± 인식 κ²°κ³Ό(transcript)λ₯Ό μ™ΈλΆ€ 둜직으둜 전달 κ°€λŠ₯
388
+ - λΈŒλΌμš°μ € 미지원 ν™˜κ²½μ— λŒ€ν•œ μ—λŸ¬ 처리 지원
389
+
390
+ <br>
391
+
392
+ ### Usage
393
+ ```ts
394
+ const voice = useBrowserSpeechRecognition();
395
+ ```
396
+
397
+ <br>
398
+
399
+ ### Returned Values
400
+
401
+ | name | type | description |
402
+ |------|------|-------------|
403
+ | `start` | `() => void` | μŒμ„± 인식 μ‹œμž‘ |
404
+ | `stop` | `() => void` | μŒμ„± 인식 μ’…λ£Œ |
405
+ | `isRecording` | `boolean` | ν˜„μž¬ μŒμ„± 인식 μ§„ν–‰ μƒνƒœ |
406
+ | `onTranscript` | `(fn: (text: string) => void) => void` | μŒμ„± 인식 κ²°κ³Ό(transcript)λ₯Ό μ²˜λ¦¬ν•  콜백 |
407
+
408
+ <br>
409
+
410
+ ### Options
411
+
412
+ | name | type | required | description |
413
+ |------|------|----------|-------------|
414
+ | `lang` | `string` | ❌ | μŒμ„± 인식에 μ‚¬μš©ν•  μ–Έμ–΄ μ½”λ“œ (κΈ°λ³Έκ°’: `"ko-KR"`) |
415
+ | `onStart` | `() => void` | ❌ | μŒμ„± 인식이 μ‹œμž‘λ  λ•Œ μ‹€ν–‰λ˜λŠ” 콜백 |
416
+ | `onEnd` | `() => void` | ❌ | μŒμ„± 인식이 μ’…λ£Œλ  λ•Œ μ‹€ν–‰λ˜λŠ” 콜백 |
417
+ | `onError` | `(error: unknown) => void` | ❌ | μŒμ„± 인식 쀑 μ—λŸ¬κ°€ λ°œμƒν–ˆμ„ λ•Œ μ‹€ν–‰λ˜λŠ” 콜백 |
418
+
419
+ <br>
420
+
421
+ <h2 id="usevoicechat">πŸͺ useVoiceChat</h2>
422
+
423
+ <code>useVoiceChat</code>은 <code>useChat</code>의 μΊμ‹œ ꡬ쑰와 optimistic update 흐름을 κ·ΈλŒ€λ‘œ μœ μ§€ν•˜λ©΄μ„œ,
424
+ **μŒμ„± 인식 기반 μ±„νŒ…** κ²½ν—˜μ„ μ œκ³΅ν•˜λŠ” Hookμž…λ‹ˆλ‹€.
425
+
426
+ μŒμ„± 인식 κ²°κ³Όλ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ μ±„νŒ… UI에 λ°˜μ˜ν•˜κ³ ,
427
+ λ…ΉμŒ μ’…λ£Œ μ‹œ μ΅œμ’… ν…μŠ€νŠΈλ₯Ό AI μš”μ²­μœΌλ‘œ μ—°κ²°ν•˜λŠ” 흐름을 λ‚΄λΆ€μ—μ„œ κ΄€λ¦¬ν•©λ‹ˆλ‹€.
428
+
429
+ - <code>useInfiniteQuery</code> 기반 **μ±„νŒ… νžˆμŠ€ν† λ¦¬ μΊμ‹œ 관리**
430
+ - <code>useChat</code>κ³Ό λ™μΌν•œ νŽ˜μ΄μ§€ λ‹¨μœ„ 캐싱 ꡬ쑰
431
+ - κΈ°μ‘΄ ν…μŠ€νŠΈ μ±„νŒ…κ³Ό λ™μΌν•œ Message μ •κ·œν™” 방식 μœ μ§€
432
+ - μŒμ„± μž…λ ₯ 기반 **Optimistic Update**
433
+ - λ…ΉμŒ μ‹œμž‘ μ‹œ USER λ©”μ‹œμ§€λ₯Ό μ¦‰μ‹œ μΊμ‹œμ— μ‚½μž…
434
+ - μŒμ„± 인식 쀑간 κ²°κ³Όλ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ λ©”μ‹œμ§€ content에 반영
435
+ - μŒμ„± 인식 μ’…λ£Œ μ‹œ **AI μš”μ²­ 트리거**
436
+ - μ΅œμ’… transcriptλ₯Ό mutationFn으둜 전달
437
+ - AI 응닡 λŒ€κΈ° μƒνƒœλ₯Ό <code>isPending</code>으둜 제곡
438
+ - TanStack Query의 μΊμ‹œ λ©”μ»€λ‹ˆμ¦˜μ„ ν™œμš©ν•œ **μ•ˆμ •μ μΈ μƒνƒœ 동기화**
439
+ - μŒμ„± μž…λ ₯ μ·¨μ†Œ λ˜λŠ” μ—λŸ¬ λ°œμƒ μ‹œ 이전 μΊμ‹œ μƒνƒœλ‘œ rollback
440
+ - <code>staleTime</code>, <code>gcTime</code>을 ν†΅ν•œ μΊμ‹œ 수λͺ… μ œμ–΄
441
+ - μŒμ„± 인식 λ‘œμ§μ„ μ™ΈλΆ€μ—μ„œ μ£Όμž… κ°€λŠ₯
442
+ - <code>useBrowserSpeechRecognition</code> λ˜λŠ” μ»€μŠ€ν…€ μŒμ„± 인식 컨트둀러 μ‚¬μš© κ°€λŠ₯
443
+
444
+ <br>
445
+
446
+ ### Usage
447
+ ```ts
448
+ const voice = useBrowserSpeechRecognition();
449
+
450
+ const {
451
+ messages,
452
+ isPending,
453
+ isInitialLoading,
454
+ startRecording,
455
+ stopRecording,
456
+ fetchNextPage,
457
+ hasNextPage,
458
+ isFetchingNextPage,
459
+ } = useVoiceChat({
460
+ voice,
461
+ queryKey: ["chat", roomId],
462
+ queryFn: getChat,
463
+ initialPageParam: 0,
464
+ getNextPageParam,
465
+ mutationFn: sendAI,
466
+ map: (raw) => ({
467
+ id: raw.chatId,
468
+ role: raw.sender === "ai" ? "AI" : "USER",
469
+ content: raw.body,
470
+ }),
471
+ });
472
+ ```
473
+
474
+ <br>
475
+
476
+ ### Returned Values
477
+ | name | type | description |
478
+ | -------------------- | ------------------------ | ---------------------------------------- |
479
+ | `messages` | `Message[]` | μ •κ·œν™”λœ λ©”μ‹œμ§€ λ°°μ—΄ |
480
+ | `isPending` | `boolean` | AI 응닡 λŒ€κΈ° μƒνƒœ |
481
+ | `isInitialLoading` | `boolean` | `messages` λ‘œλ”© μƒνƒœ |
482
+ | `startRecording` | `() => Promise<void>` | μŒμ„± 인식 μ‹œμž‘ ν•¨μˆ˜ |
483
+ | `stopRecording` | `() => void` | μŒμ„± 인식 μ’…λ£Œ 및 μ΅œμ’… ν…μŠ€νŠΈ 전솑 ν•¨μˆ˜ |
484
+ | `fetchNextPage` | `() => Promise<unknown>` | λ‹€μŒ μ±„νŒ… νŽ˜μ΄μ§€ μš”μ²­ |
485
+ | `hasNextPage` | `boolean \| undefined` | λ‹€μŒ νŽ˜μ΄μ§€ 쑴재 μ—¬λΆ€ |
486
+ | `isFetchingNextPage` | `boolean` | νŽ˜μ΄μ§€ λ‘œλ”© μƒνƒœ |
487
+
488
+ <br>
489
+
490
+ ### Options
491
+ | name | type | required | description |
492
+ | ------------------ | --------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------- |
493
+ | `voice` | `VoiceRecognition` | βœ… | μŒμ„± 인식을 μ œμ–΄ν•˜λŠ” 컨트둀러 |
494
+ | `queryKey` | `readonly unknown[]` | βœ… | ν•΄λ‹Ή μ±„νŒ…μ˜ TanStack Query key |
495
+ | `queryFn` | `(pageParam: unknown) => Promise<Raw[]>` | βœ… | κΈ°μ‘΄ μ±„νŒ… 내역을 λΆˆλŸ¬μ˜€λŠ” ν•¨μˆ˜ |
496
+ | `initialPageParam` | `unknown` | βœ… | 첫 νŽ˜μ΄μ§€ μš”μ²­ μ‹œ μ‚¬μš©ν•  pageParam |
497
+ | `getNextPageParam` | `(lastPage: Message[], allPages: Message[][]) => unknown` | βœ… | λ‹€μŒ νŽ˜μ΄μ§€ μš”μ²­μ„ μœ„ν•œ pageParam 계산 ν•¨μˆ˜ |
498
+ | `mutationFn` | `(content: string) => Promise<Raw>` | βœ… | μŒμ„± 인식 κ²°κ³Όλ₯Ό λ°›μ•„ AI 응닡 1개λ₯Ό λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜ |
499
+ | `map` | `(raw: Raw) => { id; role; content }` | βœ… | Raw 데이터λ₯Ό Message ꡬ쑰둜 λ§€ν•‘ν•˜λŠ” ν•¨μˆ˜ |
500
+ | `onError` | `(error: unknown) => void` | ❌ | mutation μ—λŸ¬ λ°œμƒ μ‹œ ν˜ΈμΆœλ˜λŠ” 콜백 |
501
+ | `staleTime` | `number` | ❌ | μΊμ‹œκ°€ fresh μƒνƒœλ‘œ μœ μ§€λ˜λŠ” μ‹œκ°„ (ms) |
502
+ | `gcTime` | `number` | ❌ | μΊμ‹œκ°€ GC 되기 μ „κΉŒμ§€ μœ μ§€λ˜λŠ” μ‹œκ°„ (ms) |
503
+
504
+ <br>
505
+
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
514
+
515
+ <br>
516
+
517
+ <h1 id="components">🎨 Components</h1>
518
+
519
+ <h2 id="indicators">🎨 Indicators</h2>
520
+
521
+ <code>Indicators</code>λŠ” **λ‘œλ”© μƒνƒœλ₯Ό μ‹œκ°μ μœΌλ‘œ ν‘œν˜„ν•˜κΈ° μœ„ν•œ μ»΄ν¬λ„ŒνŠΈ λͺ¨μŒ**μž…λ‹ˆλ‹€.
522
+ ν˜„μž¬ μ•„λž˜ 두 κ°€μ§€ μ»΄ν¬λ„ŒνŠΈλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
523
+
524
+ | <img src="https://github.com/user-attachments/assets/cd480e2f-5518-4588-bf90-e3461607bef1" alt="LoadingSpinner" width="120" /> | <img src="https://github.com/user-attachments/assets/0c30ce29-9535-480b-b74f-0f170a594951" alt="SendingDots" width="120" /> |
525
+ | :---------------: | :---------------: |
526
+ | **LoadingSpinner** | **SendingDots** |
527
+
528
+ <br>
529
+
530
+ ### Usage
531
+ ```tsx
532
+ <LoadingSpinner size="lg" />
533
+ ```
534
+ ```tsx
535
+ <SendingDots size="lg" />
536
+ ```
537
+
538
+ <br>
539
+
540
+ ### Props
541
+ | name | type | required | description |
542
+ | ------ | ------------------------------ |-----| ----------- |
543
+ | `size` | `"xs" \| "sm" \| "md" \| "lg"` | ❌ | μ»΄ν¬λ„ŒνŠΈμ˜ 크기<br>(<code>default</code>: "md") |
544
+
545
+ <br>
546
+
547
+ <h2 id="chatmessage">🎨 ChatMessage</h2>
548
+
549
+ <code>ChatMessage</code>λŠ” **단일 μ±„νŒ… λ©”μ‹œμ§€λ₯Ό λ Œλ”λ§ν•˜λŠ” 말풍선 μ»΄ν¬λ„ŒνŠΈ**μž…λ‹ˆλ‹€.
550
+ λ©”μ‹œμ§€μ˜ <code>role</code>에 따라 AI / USER λ ˆμ΄μ•„μ›ƒμ„ μžλ™μœΌλ‘œ λΆ„κΈ°ν•˜λ©°,
551
+ μ•„μ΄μ½˜, μœ„μΉ˜, μŠ€νƒ€μΌμ„ μœ μ—°ν•˜κ²Œ μ»€μŠ€ν„°λ§ˆμ΄μ§•ν•  수 μžˆλ„λ‘ μ„€κ³„λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
552
+
553
+ | <img width="224" height="63" alt="Image" src="https://github.com/user-attachments/assets/e351d1f2-b476-41f5-a002-eee4119cf0a0" /> | <img width="174" height="61" alt="Image" src="https://github.com/user-attachments/assets/98a8e033-d364-49da-bdc1-bc0a9f842969" /> |
554
+ | :---------------: | :---------------: |
555
+ | **role="AI"** | **role="USER"** |
556
+
557
+ <br>
558
+
559
+ ### Usage
560
+ ```tsx
561
+ <ChatMessage
562
+ id="1"
563
+ role="AI"
564
+ content="μ•ˆλ…•ν•˜μ„Έμš”! 무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?"
565
+ />
566
+
567
+ <ChatMessage
568
+ id="2"
569
+ role="USER"
570
+ content="질문이 μžˆμ–΄μš”."
571
+ />
572
+
573
+ <ChatMessage
574
+ id="3"
575
+ role="AI"
576
+ isLoading
577
+ loadingRenderer={<SendingDots/>}
578
+ />
579
+ ```
580
+
581
+ <br>
582
+
583
+ ### Props
584
+ | name | type | required | description |
585
+ | ----------------- | ----------------------------- | --------------- | --------------- |
586
+ | `id` | `string` | βœ… | λ©”μ‹œμ§€μ˜ 고유 μ‹λ³„μž |
587
+ | `role` | `"AI" \| "USER"` | βœ… | λ©”μ‹œμ§€ 주체<br><code>AI</code>: 쒌츑 λ©”μ‹œμ§€<br> <code>USER</code>: 우츑 λ©”μ‹œμ§€ |
588
+ | `content` | `string` | βœ… | λ©”μ‹œμ§€ ν…μŠ€νŠΈ |
589
+ | `isLoading` | `boolean` | ❌ | λ‘œλ”© μƒνƒœ μ—¬λΆ€ |
590
+ | `wrapperClassName` | `string` | ❌ | λ©”μ‹œμ§€ wrapper μ»€μŠ€ν…€ 클래슀 |
591
+ | `icon` | `React.ReactNode` | ❌ | AI λ©”μ‹œμ§€μ— ν‘œμ‹œν•  μ»€μŠ€ν…€ μ•„μ΄μ½˜ |
592
+ | `aiIconWrapperClassName` | `string` | ❌ | AI μ•„μ΄μ½˜ wrapper μ»€μŠ€ν…€ 클래슀 |
593
+ | `aiIconColor` | `string` | ❌ | κΈ°λ³Έ AI μ•„μ΄μ½˜ 색상 클래슀 |
594
+ | `bubbleClassName` | `string` | ❌ | 곡톡 말풍선 μ»€μŠ€ν…€ 클래슀 |
595
+ | `aiBubbleClassName` | `string` | ❌ | AI 말풍선 μ»€μŠ€ν…€ 클래슀 |
596
+ | `userBubbleClassName` | `string` | ❌ | User 말풍선 μ»€μŠ€ν…€ 클래슀 |
597
+ | `position` | `"auto" \| "left" \| "right"` | ❌ | 말풍선 μœ„μΉ˜ μ„€μ • |
598
+ | `loadingRenderer` | `React.ReactNode` | ❌ | λ‘œλ”© μƒνƒœ μ‹œ λ Œλ”λ§ν•  μ»€μŠ€ν…€ UI<br>(<code>default</code>: \<LoadingSpinner/>) |
599
+
600
+ <br>
601
+
602
+ <h2 id="chatlist">🎨 ChatList</h2>
603
+
604
+ <code>ChatList</code>λŠ” **μ±„νŒ… λ©”μ‹œμ§€ λͺ©λ‘μ„ λ Œλ”λ§ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈ**μž…λ‹ˆλ‹€.
605
+ λ‚΄λΆ€μ—μ„œ <code>ChatMessage</code>λ₯Ό μ‚¬μš©ν•΄ λ©”μ‹œμ§€λ₯Ό μˆœμ„œλŒ€λ‘œ λ‚˜μ—΄ν•˜λ©°,
606
+ λ©”μ‹œμ§€ λ§€ν•‘, μ»€μŠ€ν…€ λ Œλ”λ§μ„ 톡해 μœ μ—°ν•œ λ©”μ‹œμ§€ UI ꡬ성이 κ°€λŠ₯ν•©λ‹ˆλ‹€.
607
+
608
+ | <img width="524" height="450" alt="Image" src="https://github.com/user-attachments/assets/d55f54cc-22b3-4153-9982-5fe086aa5e31" /> |
609
+ | :---------------: |
610
+ | **ChatList** |
611
+
612
+ <br>
613
+
614
+ ### Usage
615
+ ```tsx
616
+ // 이미 Message νƒ€μž…μœΌλ‘œ μ •κ·œν™”λœ 데이터λ₯Ό μ‚¬μš©ν•˜λŠ” 경우
617
+ <ChatList
618
+ messages={messages}
619
+ />
620
+
621
+ // μ„œλ²„μ—μ„œ λ‚΄λ €μ˜€λŠ” Raw 데이터λ₯Ό μ‚¬μš©ν•˜λŠ” 경우
622
+ <ChatList
623
+ messages={messages}
624
+ messageMapper={(msg) => ({
625
+ id: Number(msg.chatId),
626
+ role: msg.sender === "bot" ? "AI" : "USER",
627
+ content: msg.body,
628
+ })}
629
+ />
630
+
631
+ // μ»€μŠ€ν…€ λ©”μ‹œμ§€ UI μ‚¬μš©
632
+ <ChatList
633
+ messages={messages}
634
+ messageRenderer={(msg) => (
635
+ <CustomMessage key={msg.id} {...msg} />
636
+ )}
637
+ />
638
+ ```
639
+
640
+ <br>
641
+
642
+ ### Props
643
+ | name | type | required | description |
644
+ | ----------------- | ---------------------------------------- | -------- | ----------------------------------- |
645
+ | `messages` | `Message[] \| Raw[]` | βœ… | λ Œλ”λ§ν•  λ©”μ‹œμ§€ λ°°μ—΄ |
646
+ | `messageMapper` | `(msg: Raw) => Message` | ❌ | Raw 데이터λ₯Ό Message ꡬ쑰둜 λ§€ν•‘ν•˜λŠ” ν•¨μˆ˜ |
647
+ | `messageRenderer` | `(msg: Message) => React.ReactNode` | ❌ | κΈ°λ³Έ `ChatMessage` λŒ€μ‹  μ‚¬μš©ν•  μ»€μŠ€ν…€ λ©”μ‹œμ§€ λ Œλ”λŸ¬ |
648
+ | `className` | `string` | ❌ | λ©”μ‹œμ§€ 리슀트 wrapper μ»€μŠ€ν…€ 클래슀 |
649
+ | `loadingRenderer` | `React.ReactNode` | ❌ | AI λ©”μ‹œμ§€μ˜ λ‘œλ”© μƒνƒœμ— 전달할 μ»€μŠ€ν…€ λ‘œλ”© UI<br>(<code>default</code>: \<LoadingSpinner/>) |
650
+
651
+ <br>
652
+
653
+ <h2 id="chatinput">🎨 ChatInput</h2>
654
+
655
+ <code>ChatInput</code>은 **ν…μŠ€νŠΈ μž…λ ₯κ³Ό μŒμ„± μž…λ ₯을 λͺ¨λ‘ μ§€μ›ν•˜λŠ” μ±„νŒ… μž…λ ₯ μ»΄ν¬λ„ŒνŠΈ**μž…λ‹ˆλ‹€.
656
+ **textarea** 기반 μž…λ ₯μ°½κ³Ό 전솑 λ²„νŠΌμ„ μ œκ³΅ν•˜λ©°,
657
+ 마이크 λ²„νŠΌμ„ 톡해 μŒμ„±μ„ ν…μŠ€νŠΈλ‘œ λ³€ν™˜ν•΄ μž…λ ₯ν•  수 μžˆλ„λ‘ μ„€κ³„λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
658
+
659
+ 기본적으둜 λΈŒλΌμš°μ € μŒμ„± 인식 κΈ°λŠ₯을 μ‚¬μš©ν•œ
660
+ <code>useBrowserSpeechRecognition</code> 훅이 μ„€μ •λ˜μ–΄ 있으며,
661
+ λ‹€λ₯Έ μŒμ„± 인식 λ‘œμ§μ„ μ‚¬μš©ν•˜κ³  싢은 경우 **voice** μ˜΅μ…˜μœΌλ‘œ ꡐ체할 수 μžˆμŠ΅λ‹ˆλ‹€.
662
+
663
+ | <img width="534" height="69" alt="Image" src="https://github.com/user-attachments/assets/15c90e44-1a44-4243-87ec-7755610eafb2" />|
664
+ | :---------------: |
665
+ | **ChatInput** |
666
+
667
+ <br>
668
+
669
+ ### Usage
670
+ ```tsx
671
+ <ChatInput
672
+ onSend={(value) => {
673
+ console.log(value);
674
+ }}
675
+ isSending={isPending}
676
+ />
677
+ ```
678
+
679
+ <br>
680
+
681
+ ### Props
682
+ | name | type | required | description |
683
+ | ----------------- | ------------------------------------------ | -------- | ----------------------------- |
684
+ | `onSend` | `(value: string) => void \| Promise<void>` | βœ… | λ©”μ‹œμ§€ 전솑 μ‹œ ν˜ΈμΆœλ˜λŠ” 콜백 |
685
+ | `isSending` | `boolean` | βœ… | λ©”μ‹œμ§€ 전솑 쀑 μƒνƒœ μ—¬λΆ€ |
686
+ | `voice` | `boolean \| VoiceRecognition` | ❌ | μŒμ„± 인식 μ‚¬μš© μ—¬λΆ€ λ˜λŠ” μ»€μŠ€ν…€ μŒμ„± 인식 컨트둀러<br>(<code>default</code>: true) |
687
+ | `placeholder` | `string` | ❌ | μž…λ ₯μ°½ placeholder ν…μŠ€νŠΈ |
688
+ | `className` | `string` | ❌ | 전체 wrapper μ»€μŠ€ν…€ 클래슀 |
689
+ | `inputClassName` | `string` | ❌ | textarea μ»€μŠ€ν…€ 클래슀 |
690
+ | `micButton` | `{ className?: string; icon?: ReactNode }` | ❌ | 마이크 λ²„νŠΌ μ»€μŠ€ν„°λ§ˆμ΄μ§• |
691
+ | `recordingButton` | `{ className?: string; icon?: ReactNode }` | ❌ | λ…ΉμŒ 쀑 λ²„νŠΌ μ»€μŠ€ν„°λ§ˆμ΄μ§• |
692
+ | `sendButton` | `{ className?: string; icon?: ReactNode }` | ❌ | 전솑 λ²„νŠΌ μ»€μŠ€ν„°λ§ˆμ΄μ§• |
693
+ | `sendingButton` | `{ className?: string; icon?: ReactNode }` | ❌ | 전솑 쀑 λ²„νŠΌ μ»€μŠ€ν„°λ§ˆμ΄μ§• |
694
+ | `maxHeight` | `number` | ❌ | textarea μ΅œλŒ€ 높이(px) |
695
+ | `value` | `string` | ❌ | μ»¨νŠΈλ‘€λ“œ λͺ¨λ“œ μž…λ ₯κ°’ |
696
+ | `onChange` | `(value: string) => void` | ❌ | μ»¨νŠΈλ‘€λ“œ λͺ¨λ“œ μž…λ ₯ λ³€κ²½ ν•Έλ“€λŸ¬ |
697
+ | `submitOnEnter` | `boolean` | ❌ | Enter ν‚€λ‘œ 전솑할지 μ—¬λΆ€ |
698
+
699
+ <br>
700
+
701
+ <h2 id="chatcontainer">🎨 ChatContainer</h2>
702
+
703
+ <code>ChatContainer</code>λŠ” **μ±„νŒ… UIλ₯Ό λΉ λ₯΄κ²Œ κ΅¬μ„±ν•˜κ³  싢은 μ‚¬μš©μžλ₯Ό μœ„ν•œ μ±„νŒ… μ»¨ν…Œμ΄λ„ˆ μ»΄ν¬λ„ŒνŠΈ**μž…λ‹ˆλ‹€.
704
+ <code>ChatList</code>와 <code>ChatInput</code>을 λ‚΄λΆ€μ—μ„œ ν•¨κ»˜ λ Œλ”λ§ν•˜λ©°,
705
+ <code>useChat</code>, <code>useVoiceChat</code>κ³Ό μžμ—°μŠ€λŸ½κ²Œ κ²°ν•©ν•  수 μžˆλ„λ‘ μ„€κ³„λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
706
+
707
+ λ˜ν•œ <code>fetchNextPage</code>, <code>hasNextPage</code>, <code>isFetchingNextPage</code>λ₯Ό props둜 λ°›μ•„
708
+ μŠ€ν¬λ‘€μ„ μ΅œμƒλ‹¨μœΌλ‘œ 올리면 κ³Όκ±° μ±„νŒ… 내역을 μžλ™μœΌλ‘œ λ‘œλ”©ν•©λ‹ˆλ‹€.
709
+
710
+ | <img width="438" height="532" alt="Image" src="https://github.com/user-attachments/assets/b1713f34-419e-40cf-a79c-016c57145920" />|
711
+ | :---------------: |
712
+ | **ChatContainer** |
713
+
714
+ - λ©”μ‹œμ§€ μΆ”κ°€ μ‹œ 슀크둀이 ν•˜λ‹¨μ— 고정됨
715
+ - 슀크둀 μ΅œμƒλ‹¨ 도달 μ‹œ κ³Όκ±° λ©”μ‹œμ§€ νŽ˜μ΄μ§€ λ‘œλ”©
716
+ - ν•˜λ‹¨μ— λ„λ‹¬ν•˜μ§€ μ•Šμ€ μƒνƒœμ—μ„œλŠ” "scroll to bottom" λ²„νŠΌ λ…ΈμΆœ
717
+
718
+ <br>
719
+
720
+ ### Usage
721
+ ```tsx
722
+ // 이미 Message νƒ€μž…μœΌλ‘œ μ •κ·œν™”λœ 데이터λ₯Ό μ‚¬μš©ν•˜λŠ” 경우
723
+ <ChatContainer
724
+ messages={messages}
725
+ onSend={sendMessage}
726
+ isSending={isPending}
727
+ />
728
+
729
+ // μ„œλ²„μ—μ„œ λ‚΄λ €μ˜€λŠ” Raw 데이터λ₯Ό μ‚¬μš©ν•˜λŠ” 경우
730
+ <ChatContainer
731
+ messages={rawMessages}
732
+ messageMapper={(raw) => ({
733
+ id: raw.id,
734
+ role: raw.sender === "user" ? "USER" : "AI",
735
+ content: raw.text,
736
+ })}
737
+ onSend={sendMessage}
738
+ isSending={isPending}
739
+ />
740
+
741
+ // useChat, useVoiceChatκ³Ό ν•¨κ»˜ μ‚¬μš©ν•˜λŠ” 경우
742
+ <ChatContainer
743
+ messages={messages}
744
+ onSend={sendMessage}
745
+ isSending={isPending}
746
+ fetchNextPage={fetchNextPage}
747
+ hasNextPage={hasNextPage}
748
+ isFetchingNextPage={isFetchingNextPage}
749
+ />
750
+ ```
751
+
752
+ <br>
753
+
754
+ ### Props
755
+ | name | type | required | description |
756
+ | -------------------- | ------------------------------------------ | -------- | ----------------------------------- |
757
+ | `messages` | `Message[] \| Raw[]` | βœ… | λ Œλ”λ§ν•  λ©”μ‹œμ§€ λ°°μ—΄ |
758
+ | `onSend` | `(value: string) => void \| Promise<void>` | βœ… | λ©”μ‹œμ§€ 전솑 μ‹œ ν˜ΈμΆœλ˜λŠ” 콜백 |
759
+ | `isSending` | `boolean` | βœ… | λ©”μ‹œμ§€ 전솑 쀑 μƒνƒœ μ—¬λΆ€ |
760
+ | `messageMapper` | `(msg: Raw) => Message` | ❌ | Raw 데이터λ₯Ό `Message`ꡬ쑰둜 λ§€ν•‘ν•˜λŠ” ν•¨μˆ˜ |
761
+ | `messageRenderer` | `(msg: Message) => React.ReactNode` | ❌ | κΈ°λ³Έ `ChatMessage` λŒ€μ‹  μ‚¬μš©ν•  μ»€μŠ€ν…€ λ©”μ‹œμ§€ λ Œλ”λŸ¬ |
762
+ | `loadingRenderer` | `React.ReactNode` | ❌ | λ©”μ‹œμ§€ λ‘œλ”© μƒνƒœμ— μ‚¬μš©ν•  μ»€μŠ€ν…€ UI |
763
+ | `listClassName` | `string` | ❌ | `ChatList` wrapper μ»€μŠ€ν…€ 클래슀 |
764
+ | `disableVoice` | `boolean` | ❌ | μŒμ„± μž…λ ₯ κΈ°λŠ₯ λΉ„ν™œμ„±ν™” μ—¬λΆ€ |
765
+ | `placeholder` | `string` | ❌ | μž…λ ₯μ°½ placeholder ν…μŠ€νŠΈ |
766
+ | `inputClassName` | `string` | ❌ | `ChatInput` μ»€μŠ€ν…€ 클래슀 |
767
+ | `fetchNextPage` | `() => void` | ❌ | λ‹€μŒ μ±„νŒ… νŽ˜μ΄μ§€λ₯Ό μš”μ²­ν•˜λŠ” ν•¨μˆ˜ |
768
+ | `hasNextPage` | `boolean` | ❌ | λ‹€μŒ νŽ˜μ΄μ§€ 쑴재 μ—¬λΆ€ |
769
+ | `isFetchingNextPage` | `boolean` | ❌ | λ‹€μŒ νŽ˜μ΄μ§€ λ‘œλ”© μƒνƒœ |
770
+ | `className` | `string` | ❌ | 전체 μ»¨ν…Œμ΄λ„ˆ wrapper μ»€μŠ€ν…€ 클래슀 |
771
+
772
+ <br>
773
+
774
+ <h1 id="notes">πŸ“ Notes</h1>
775
+
776
+ ## 1. μ„œλ²„ μ‚¬μ΄λ“œ νŽ˜μ΄μ§€λ„€μ΄μ…˜μ€ ν•„μˆ˜μž…λ‹ˆλ‹€
777
+ 이 λΌμ΄λΈŒλŸ¬λ¦¬λŠ” μ±„νŒ… 데이터λ₯Ό **λ¬΄ν•œ 슀크둀** 기반으둜 κ΄€λ¦¬ν•©λ‹ˆλ‹€.
778
+ λ”°λΌμ„œ μ„œλ²„λŠ” λ°˜λ“œμ‹œ **page λ‹¨μœ„λ‘œ κ³Όκ±° μ±„νŒ… 내역을 쑰회**ν•  수 μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.
779
+
780
+ <br>
781
+
782
+ ## 2. νŽ˜μ΄μ§€ 기쀀은 "κ³Όκ±° β†’ μ΅œμ‹ β€ μˆœμ„œλ₯Ό ꢌμž₯ν•©λ‹ˆλ‹€
783
+ 각 νŽ˜μ΄μ§€λŠ” **μ‹œκ°„ μ˜€λ¦„μ°¨μˆœ(κ³Όκ±° β†’ μ΅œμ‹ )** 으둜 μ •λ ¬λœ 데이터λ₯Ό λ°˜ν™˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.
784
+ 이 ꡬ쑰λ₯Ό κΈ°μ€€μœΌλ‘œ 슀크둀 μœ„μΉ˜λ₯Ό μœ μ§€ν•˜λ©° 이전 νŽ˜μ΄μ§€λ₯Ό μžμ—°μŠ€λŸ½κ²Œ μ—°κ²°ν•©λ‹ˆλ‹€.
785
+ ```ts
786
+ pages = [
787
+ page[0], // κ°€μž₯ 졜근 νŽ˜μ΄μ§€
788
+ page[1],
789
+ page[2],
790
+ page[3], // fetchNextPageParam으둜 뢈러온 κ³Όκ±° μ±„νŒ…
791
+ ];
792
+ ```
793
+ ```ts
794
+ page[0] = [
795
+ { chatId: 0, time: "12:00" }, // κ³Όκ±°
796
+ { chatId: 1, time: "12:10" },
797
+ { chatId: 2, time: "12:20" },
798
+ { chatId: 3, time: "12:30" }, // μ΅œμ‹ 
799
+ ];
800
+ ```
801
+
802
+ <br>
803
+
804
+ ## 3. Optimistic MessageλŠ” μ„œλ²„ μ‘λ‹΅μœΌλ‘œ κ΅μ²΄λ˜λŠ” κ΅¬μ‘°μž…λ‹ˆλ‹€
805
+ λ©”μ‹œμ§€ 전솑 μ‹œ
806
+
807
+ **1.** μ‚¬μš©μž λ©”μ‹œμ§€λ₯Ό μ¦‰μ‹œ μΊμ‹œμ— μΆ”κ°€
808
+ **2.** μ„œλ²„ 응닡 성곡 β†’ ν•΄λ‹Ή λ©”μ‹œμ§€λ₯Ό μ‹€μ œ 응닡 λ©”μ‹œμ§€λ‘œ ꡐ체
809
+ **3.** μ‹€νŒ¨ μ‹œ β†’ optimistic λ©”μ‹œμ§€ λ‘€λ°± + onError 호좜
810
+
811
+ 이 ꡬ쑰λ₯Ό μ „μ œλ‘œ UIκ°€ μ„€κ³„λ˜μ–΄ μžˆμœΌλ―€λ‘œ
812
+ μ„œλ²„λŠ” **단일 λ©”μ‹œμ§€ λ‹¨μœ„ 응닡**을 λ°˜ν™˜ν•˜λŠ” 것을 ꢌμž₯ν•©λ‹ˆλ‹€.
813
+
814
+ <br>
815
+
816
+ ## 4. ChatContainerλŠ” β€œλΉ λ₯Έ κ΅¬ν˜„μš©β€ μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€
817
+ <code>ChatContainer</code>λŠ” λ‹€μŒμ„ ν•œ λ²ˆμ— μ œκ³΅ν•©λ‹ˆλ‹€
818
+ - λ©”μ‹œμ§€ 리슀트 λ Œλ”λ§
819
+ - μž…λ ₯μ°½ + 전솑 처리
820
+ - 상단 슀크둀 기반 κ³Όκ±° λ©”μ‹œμ§€ λ‘œλ”©
821
+ - 슀크둀 μœ„μΉ˜ μžλ™ 보정
822
+
823
+ 보닀 μ„Έλ°€ν•œ UI μ œμ–΄κ°€ ν•„μš”ν•œ κ²½μš°μ—λŠ”
824
+ <code>ChatList</code> + <code>ChatInput</code>을 직접 μ‘°ν•©ν•΄ μ‚¬μš©ν•˜λŠ” 것을 ꢌμž₯ν•©λ‹ˆλ‹€.
825
+
826
+ <br>
827
+
828
+ # πŸ“„ License
829
+ MIT License Β© 2025
830
+ See the [LICENSE](./LICENSE) file for details.
831
+
832
+
833
+
834
+
835
+
836
+
837
+
838
+
839
+
840
+
841
+
842
+
843
+
844
+
845
+
package/dist/index.d.mts CHANGED
@@ -10,11 +10,21 @@ type BaseMessage = {
10
10
  content: string;
11
11
  isLoading?: boolean;
12
12
  };
13
- type Message<T = {}> = BaseMessage & T;
13
+ type MessageCore = Pick<BaseMessage, "id" | "role" | "content">;
14
+ type Message<TCustom = unknown> = BaseMessage & {
15
+ custom?: TCustom;
16
+ };
17
+
18
+ type VoiceRecognition = {
19
+ start: () => void;
20
+ stop: () => void;
21
+ isRecording: boolean;
22
+ onTranscript?: (text: string) => void;
23
+ };
14
24
 
15
25
  type Size$1 = 'xs' | 'sm' | 'md' | 'lg';
16
26
  type Props$5 = {
17
- size: Size$1;
27
+ size?: Size$1;
18
28
  };
19
29
  declare function LoadingSpinner({ size }: Props$5): react_jsx_runtime.JSX.Element;
20
30
 
@@ -37,28 +47,22 @@ type Props$3 = Message & {
37
47
  };
38
48
  declare function ChatMessage({ id, role, content, isLoading, wrapperClassName, icon, aiIconWrapperClassName, aiIconColor, bubbleClassName, aiBubbleClassName, userBubbleClassName, position, loadingRenderer, }: Props$3): react_jsx_runtime.JSX.Element;
39
49
 
40
- type MessagePatch = Partial<BaseMessage> & Record<string, unknown>;
41
- type Props$2<T extends Message = Message> = {
42
- messages: T[];
43
- messageMapper?: (msg: T) => MessagePatch;
44
- messageRenderer?: (msg: T) => React$1.ReactNode;
50
+ type Props$2<Raw> = {
51
+ messages: Raw[];
52
+ messageMapper?: (msg: Raw) => Message;
53
+ messageRenderer?: (msg: Message) => React$1.ReactNode;
45
54
  className?: string;
46
55
  loadingRenderer?: React$1.ReactNode;
47
56
  };
48
- declare function ChatList<T extends Message>({ messages, messageMapper, messageRenderer, className, loadingRenderer, }: Props$2<T>): react_jsx_runtime.JSX.Element;
57
+ declare function ChatList<Raw>({ messages, messageMapper, messageRenderer, className, loadingRenderer, }: Props$2<Raw>): react_jsx_runtime.JSX.Element;
49
58
 
50
- type VoiceRecognitionController$1 = {
51
- start: () => void;
52
- stop: () => void;
53
- isRecording: boolean;
54
- };
55
59
  type ButtonConfig = {
56
60
  className?: string;
57
61
  icon?: React.ReactNode;
58
62
  };
59
63
  type Props$1 = {
60
64
  onSend: (value: string) => void | Promise<void>;
61
- voice?: boolean | VoiceRecognitionController$1;
65
+ voice?: boolean | VoiceRecognition;
62
66
  placeholder?: string;
63
67
  className?: string;
64
68
  inputClassName?: string;
@@ -78,9 +82,9 @@ type MessageProps = {
78
82
  messages: Message[];
79
83
  messageMapper?: never;
80
84
  };
81
- type RawProps<T> = {
82
- messages: T[];
83
- messageMapper: (msg: T) => Message;
85
+ type RawProps<Raw> = {
86
+ messages: Raw[];
87
+ messageMapper: (msg: Raw) => Message;
84
88
  };
85
89
  type CommonProps = {
86
90
  messageRenderer?: (msg: Message) => React.ReactNode;
@@ -96,32 +100,27 @@ type CommonProps = {
96
100
  isFetchingNextPage?: boolean;
97
101
  className?: string;
98
102
  };
99
- type Props<T> = CommonProps & (MessageProps | RawProps<T>);
100
- declare function ChatContainer<T>(props: Props<T>): react_jsx_runtime.JSX.Element;
103
+ type Props<Raw> = CommonProps & (MessageProps | RawProps<Raw>);
104
+ declare function ChatContainer<Raw>(props: Props<Raw>): react_jsx_runtime.JSX.Element;
101
105
 
102
- type ExtraFromRaw$1<TRaw> = Omit<TRaw, keyof BaseMessage>;
103
- type CustomMessage$1<TCustom> = BaseMessage & {
104
- custom: TCustom;
105
- };
106
- type MessageMapper$1<TRaw> = CustomMessage$1<ExtraFromRaw$1<TRaw>>;
107
- type MessageMapperResult$1 = Pick<BaseMessage, "id" | "role" | "content">;
108
- type Options$2<TRaw> = {
106
+ type ExtractCustom$1<Raw> = Omit<Raw, keyof BaseMessage>;
107
+ type Options$2<Raw> = {
109
108
  queryKey: readonly unknown[];
110
- queryFn: (pageParam: unknown) => Promise<TRaw[]>;
109
+ queryFn: (pageParam: unknown) => Promise<Raw[]>;
111
110
  initialPageParam: unknown;
112
- getNextPageParam: (lastPage: MessageMapper$1<TRaw>[], allPages: MessageMapper$1<TRaw>[][]) => unknown;
113
- mutationFn: (content: string) => Promise<TRaw>;
114
- map: (raw: TRaw) => MessageMapperResult$1;
111
+ getNextPageParam: (lastPage: Message<ExtractCustom$1<Raw>>[], allPages: Message<ExtractCustom$1<Raw>>[][]) => unknown;
112
+ mutationFn: (content: string) => Promise<Raw>;
113
+ map: (raw: Raw) => MessageCore;
115
114
  onError?: (error: unknown) => void;
116
115
  staleTime?: number;
117
116
  gcTime?: number;
118
117
  };
119
- declare function useChat<TRaw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, onError, staleTime, gcTime, }: Options$2<TRaw>): {
120
- messages: MessageMapper$1<TRaw>[];
118
+ declare function useChat<Raw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, onError, staleTime, gcTime, }: Options$2<Raw>): {
119
+ messages: Message<ExtractCustom$1<Raw>>[];
121
120
  sendUserMessage: (content: string) => void;
122
121
  isPending: boolean;
123
122
  isInitialLoading: boolean;
124
- fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<MessageMapper$1<TRaw>[], unknown>, Error>>;
123
+ fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<Message<ExtractCustom$1<Raw>>[], unknown>, Error>>;
125
124
  hasNextPage: boolean;
126
125
  isFetchingNextPage: boolean;
127
126
  };
@@ -196,39 +195,28 @@ declare function useBrowserSpeechRecognition({ lang, onStart, onEnd, onError, }?
196
195
  onTranscript: (text: string) => void;
197
196
  };
198
197
 
199
- type VoiceRecognitionController = {
200
- start: () => void;
201
- stop: () => void;
202
- isRecording: boolean;
203
- onTranscript: (text: string) => void;
204
- };
205
- type ExtraFromRaw<TRaw> = Omit<TRaw, keyof BaseMessage>;
206
- type CustomMessage<TCustom> = BaseMessage & {
207
- custom: TCustom;
208
- };
209
- type MessageMapper<TRaw> = CustomMessage<ExtraFromRaw<TRaw>>;
210
- type MessageMapperResult = Pick<BaseMessage, "id" | "role" | "content">;
211
- type Options<TRaw> = {
198
+ type ExtractCustom<Raw> = Omit<Raw, keyof BaseMessage>;
199
+ type Options<Raw> = {
212
200
  queryKey: readonly unknown[];
213
- queryFn: (pageParam: unknown) => Promise<TRaw[]>;
201
+ queryFn: (pageParam: unknown) => Promise<Raw[]>;
214
202
  initialPageParam: unknown;
215
- getNextPageParam: (lastPage: MessageMapper<TRaw>[], allPages: MessageMapper<TRaw>[][]) => unknown;
216
- mutationFn: (content: string) => Promise<TRaw>;
217
- map: (raw: TRaw) => MessageMapperResult;
218
- voice: VoiceRecognitionController;
203
+ getNextPageParam: (lastPage: Message<ExtractCustom<Raw>>[], allPages: Message<ExtractCustom<Raw>>[][]) => unknown;
204
+ mutationFn: (content: string) => Promise<Raw>;
205
+ map: (raw: Raw) => MessageCore;
206
+ voice: VoiceRecognition;
219
207
  onError?: (error: unknown) => void;
220
208
  staleTime?: number;
221
209
  gcTime?: number;
222
210
  };
223
- declare function useVoiceChat<TRaw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, voice, onError, staleTime, gcTime, }: Options<TRaw>): {
224
- messages: MessageMapper<TRaw>[];
211
+ declare function useVoiceChat<Raw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, voice, onError, staleTime, gcTime, }: Options<Raw>): {
212
+ messages: Message<ExtractCustom<Raw>>[];
225
213
  isPending: boolean;
226
214
  isInitialLoading: boolean;
227
215
  startRecording: () => Promise<void>;
228
216
  stopRecording: () => void;
229
- fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<MessageMapper<TRaw>[], unknown>, Error>>;
217
+ fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<Message<ExtractCustom<Raw>>[], unknown>, Error>>;
230
218
  hasNextPage: boolean;
231
219
  isFetchingNextPage: boolean;
232
220
  };
233
221
 
234
- export { ChatContainer, ChatInput, ChatList, ChatMessage, LoadingSpinner, type Message, SendingDots, useBrowserSpeechRecognition, useChat, useVoiceChat };
222
+ export { ChatContainer, ChatInput, ChatList, ChatMessage, LoadingSpinner, type Message, SendingDots, type VoiceRecognition, useBrowserSpeechRecognition, useChat, useVoiceChat };
package/dist/index.d.ts CHANGED
@@ -10,11 +10,21 @@ type BaseMessage = {
10
10
  content: string;
11
11
  isLoading?: boolean;
12
12
  };
13
- type Message<T = {}> = BaseMessage & T;
13
+ type MessageCore = Pick<BaseMessage, "id" | "role" | "content">;
14
+ type Message<TCustom = unknown> = BaseMessage & {
15
+ custom?: TCustom;
16
+ };
17
+
18
+ type VoiceRecognition = {
19
+ start: () => void;
20
+ stop: () => void;
21
+ isRecording: boolean;
22
+ onTranscript?: (text: string) => void;
23
+ };
14
24
 
15
25
  type Size$1 = 'xs' | 'sm' | 'md' | 'lg';
16
26
  type Props$5 = {
17
- size: Size$1;
27
+ size?: Size$1;
18
28
  };
19
29
  declare function LoadingSpinner({ size }: Props$5): react_jsx_runtime.JSX.Element;
20
30
 
@@ -37,28 +47,22 @@ type Props$3 = Message & {
37
47
  };
38
48
  declare function ChatMessage({ id, role, content, isLoading, wrapperClassName, icon, aiIconWrapperClassName, aiIconColor, bubbleClassName, aiBubbleClassName, userBubbleClassName, position, loadingRenderer, }: Props$3): react_jsx_runtime.JSX.Element;
39
49
 
40
- type MessagePatch = Partial<BaseMessage> & Record<string, unknown>;
41
- type Props$2<T extends Message = Message> = {
42
- messages: T[];
43
- messageMapper?: (msg: T) => MessagePatch;
44
- messageRenderer?: (msg: T) => React$1.ReactNode;
50
+ type Props$2<Raw> = {
51
+ messages: Raw[];
52
+ messageMapper?: (msg: Raw) => Message;
53
+ messageRenderer?: (msg: Message) => React$1.ReactNode;
45
54
  className?: string;
46
55
  loadingRenderer?: React$1.ReactNode;
47
56
  };
48
- declare function ChatList<T extends Message>({ messages, messageMapper, messageRenderer, className, loadingRenderer, }: Props$2<T>): react_jsx_runtime.JSX.Element;
57
+ declare function ChatList<Raw>({ messages, messageMapper, messageRenderer, className, loadingRenderer, }: Props$2<Raw>): react_jsx_runtime.JSX.Element;
49
58
 
50
- type VoiceRecognitionController$1 = {
51
- start: () => void;
52
- stop: () => void;
53
- isRecording: boolean;
54
- };
55
59
  type ButtonConfig = {
56
60
  className?: string;
57
61
  icon?: React.ReactNode;
58
62
  };
59
63
  type Props$1 = {
60
64
  onSend: (value: string) => void | Promise<void>;
61
- voice?: boolean | VoiceRecognitionController$1;
65
+ voice?: boolean | VoiceRecognition;
62
66
  placeholder?: string;
63
67
  className?: string;
64
68
  inputClassName?: string;
@@ -78,9 +82,9 @@ type MessageProps = {
78
82
  messages: Message[];
79
83
  messageMapper?: never;
80
84
  };
81
- type RawProps<T> = {
82
- messages: T[];
83
- messageMapper: (msg: T) => Message;
85
+ type RawProps<Raw> = {
86
+ messages: Raw[];
87
+ messageMapper: (msg: Raw) => Message;
84
88
  };
85
89
  type CommonProps = {
86
90
  messageRenderer?: (msg: Message) => React.ReactNode;
@@ -96,32 +100,27 @@ type CommonProps = {
96
100
  isFetchingNextPage?: boolean;
97
101
  className?: string;
98
102
  };
99
- type Props<T> = CommonProps & (MessageProps | RawProps<T>);
100
- declare function ChatContainer<T>(props: Props<T>): react_jsx_runtime.JSX.Element;
103
+ type Props<Raw> = CommonProps & (MessageProps | RawProps<Raw>);
104
+ declare function ChatContainer<Raw>(props: Props<Raw>): react_jsx_runtime.JSX.Element;
101
105
 
102
- type ExtraFromRaw$1<TRaw> = Omit<TRaw, keyof BaseMessage>;
103
- type CustomMessage$1<TCustom> = BaseMessage & {
104
- custom: TCustom;
105
- };
106
- type MessageMapper$1<TRaw> = CustomMessage$1<ExtraFromRaw$1<TRaw>>;
107
- type MessageMapperResult$1 = Pick<BaseMessage, "id" | "role" | "content">;
108
- type Options$2<TRaw> = {
106
+ type ExtractCustom$1<Raw> = Omit<Raw, keyof BaseMessage>;
107
+ type Options$2<Raw> = {
109
108
  queryKey: readonly unknown[];
110
- queryFn: (pageParam: unknown) => Promise<TRaw[]>;
109
+ queryFn: (pageParam: unknown) => Promise<Raw[]>;
111
110
  initialPageParam: unknown;
112
- getNextPageParam: (lastPage: MessageMapper$1<TRaw>[], allPages: MessageMapper$1<TRaw>[][]) => unknown;
113
- mutationFn: (content: string) => Promise<TRaw>;
114
- map: (raw: TRaw) => MessageMapperResult$1;
111
+ getNextPageParam: (lastPage: Message<ExtractCustom$1<Raw>>[], allPages: Message<ExtractCustom$1<Raw>>[][]) => unknown;
112
+ mutationFn: (content: string) => Promise<Raw>;
113
+ map: (raw: Raw) => MessageCore;
115
114
  onError?: (error: unknown) => void;
116
115
  staleTime?: number;
117
116
  gcTime?: number;
118
117
  };
119
- declare function useChat<TRaw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, onError, staleTime, gcTime, }: Options$2<TRaw>): {
120
- messages: MessageMapper$1<TRaw>[];
118
+ declare function useChat<Raw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, onError, staleTime, gcTime, }: Options$2<Raw>): {
119
+ messages: Message<ExtractCustom$1<Raw>>[];
121
120
  sendUserMessage: (content: string) => void;
122
121
  isPending: boolean;
123
122
  isInitialLoading: boolean;
124
- fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<MessageMapper$1<TRaw>[], unknown>, Error>>;
123
+ fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<Message<ExtractCustom$1<Raw>>[], unknown>, Error>>;
125
124
  hasNextPage: boolean;
126
125
  isFetchingNextPage: boolean;
127
126
  };
@@ -196,39 +195,28 @@ declare function useBrowserSpeechRecognition({ lang, onStart, onEnd, onError, }?
196
195
  onTranscript: (text: string) => void;
197
196
  };
198
197
 
199
- type VoiceRecognitionController = {
200
- start: () => void;
201
- stop: () => void;
202
- isRecording: boolean;
203
- onTranscript: (text: string) => void;
204
- };
205
- type ExtraFromRaw<TRaw> = Omit<TRaw, keyof BaseMessage>;
206
- type CustomMessage<TCustom> = BaseMessage & {
207
- custom: TCustom;
208
- };
209
- type MessageMapper<TRaw> = CustomMessage<ExtraFromRaw<TRaw>>;
210
- type MessageMapperResult = Pick<BaseMessage, "id" | "role" | "content">;
211
- type Options<TRaw> = {
198
+ type ExtractCustom<Raw> = Omit<Raw, keyof BaseMessage>;
199
+ type Options<Raw> = {
212
200
  queryKey: readonly unknown[];
213
- queryFn: (pageParam: unknown) => Promise<TRaw[]>;
201
+ queryFn: (pageParam: unknown) => Promise<Raw[]>;
214
202
  initialPageParam: unknown;
215
- getNextPageParam: (lastPage: MessageMapper<TRaw>[], allPages: MessageMapper<TRaw>[][]) => unknown;
216
- mutationFn: (content: string) => Promise<TRaw>;
217
- map: (raw: TRaw) => MessageMapperResult;
218
- voice: VoiceRecognitionController;
203
+ getNextPageParam: (lastPage: Message<ExtractCustom<Raw>>[], allPages: Message<ExtractCustom<Raw>>[][]) => unknown;
204
+ mutationFn: (content: string) => Promise<Raw>;
205
+ map: (raw: Raw) => MessageCore;
206
+ voice: VoiceRecognition;
219
207
  onError?: (error: unknown) => void;
220
208
  staleTime?: number;
221
209
  gcTime?: number;
222
210
  };
223
- declare function useVoiceChat<TRaw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, voice, onError, staleTime, gcTime, }: Options<TRaw>): {
224
- messages: MessageMapper<TRaw>[];
211
+ declare function useVoiceChat<Raw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, voice, onError, staleTime, gcTime, }: Options<Raw>): {
212
+ messages: Message<ExtractCustom<Raw>>[];
225
213
  isPending: boolean;
226
214
  isInitialLoading: boolean;
227
215
  startRecording: () => Promise<void>;
228
216
  stopRecording: () => void;
229
- fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<MessageMapper<TRaw>[], unknown>, Error>>;
217
+ fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<Message<ExtractCustom<Raw>>[], unknown>, Error>>;
230
218
  hasNextPage: boolean;
231
219
  isFetchingNextPage: boolean;
232
220
  };
233
221
 
234
- export { ChatContainer, ChatInput, ChatList, ChatMessage, LoadingSpinner, type Message, SendingDots, useBrowserSpeechRecognition, useChat, useVoiceChat };
222
+ export { ChatContainer, ChatInput, ChatList, ChatMessage, LoadingSpinner, type Message, SendingDots, type VoiceRecognition, useBrowserSpeechRecognition, useChat, useVoiceChat };
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ module.exports = __toCommonJS(index_exports);
61
61
 
62
62
  // src/components/indicators/LoadingSpinner.tsx
63
63
  var import_jsx_runtime = require("react/jsx-runtime");
64
- function LoadingSpinner({ size }) {
64
+ function LoadingSpinner({ size = "md" }) {
65
65
  const sizeMap = {
66
66
  xs: 24,
67
67
  sm: 32,
package/dist/index.mjs CHANGED
@@ -20,7 +20,7 @@ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
20
 
21
21
  // src/components/indicators/LoadingSpinner.tsx
22
22
  import { jsx } from "react/jsx-runtime";
23
- function LoadingSpinner({ size }) {
23
+ function LoadingSpinner({ size = "md" }) {
24
24
  const sizeMap = {
25
25
  xs: 24,
26
26
  sm: 32,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-optimistic-chat",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",