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 +23 -0
- package/README.md +845 -0
- package/dist/index.d.mts +43 -55
- package/dist/index.d.ts +43 -55
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
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 Β· [](https://www.npmjs.com/package/react-optimistic-chat) [](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
|
|
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
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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<
|
|
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 |
|
|
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<
|
|
82
|
-
messages:
|
|
83
|
-
messageMapper: (msg:
|
|
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<
|
|
100
|
-
declare function ChatContainer<
|
|
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
|
|
103
|
-
type
|
|
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<
|
|
109
|
+
queryFn: (pageParam: unknown) => Promise<Raw[]>;
|
|
111
110
|
initialPageParam: unknown;
|
|
112
|
-
getNextPageParam: (lastPage:
|
|
113
|
-
mutationFn: (content: string) => Promise<
|
|
114
|
-
map: (raw:
|
|
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<
|
|
120
|
-
messages:
|
|
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<
|
|
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
|
|
200
|
-
|
|
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<
|
|
201
|
+
queryFn: (pageParam: unknown) => Promise<Raw[]>;
|
|
214
202
|
initialPageParam: unknown;
|
|
215
|
-
getNextPageParam: (lastPage:
|
|
216
|
-
mutationFn: (content: string) => Promise<
|
|
217
|
-
map: (raw:
|
|
218
|
-
voice:
|
|
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<
|
|
224
|
-
messages:
|
|
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<
|
|
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
|
|
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
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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<
|
|
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 |
|
|
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<
|
|
82
|
-
messages:
|
|
83
|
-
messageMapper: (msg:
|
|
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<
|
|
100
|
-
declare function ChatContainer<
|
|
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
|
|
103
|
-
type
|
|
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<
|
|
109
|
+
queryFn: (pageParam: unknown) => Promise<Raw[]>;
|
|
111
110
|
initialPageParam: unknown;
|
|
112
|
-
getNextPageParam: (lastPage:
|
|
113
|
-
mutationFn: (content: string) => Promise<
|
|
114
|
-
map: (raw:
|
|
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<
|
|
120
|
-
messages:
|
|
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<
|
|
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
|
|
200
|
-
|
|
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<
|
|
201
|
+
queryFn: (pageParam: unknown) => Promise<Raw[]>;
|
|
214
202
|
initialPageParam: unknown;
|
|
215
|
-
getNextPageParam: (lastPage:
|
|
216
|
-
mutationFn: (content: string) => Promise<
|
|
217
|
-
map: (raw:
|
|
218
|
-
voice:
|
|
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<
|
|
224
|
-
messages:
|
|
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<
|
|
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,
|