react-optimistic-chat 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +57 -23
- package/dist/index.d.ts +57 -23
- package/dist/index.js +190 -98
- package/dist/index.mjs +190 -98
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React$1 from 'react';
|
|
3
|
+
import * as _tanstack_query_core from '@tanstack/query-core';
|
|
4
|
+
import { InfiniteData } from '@tanstack/react-query';
|
|
3
5
|
|
|
4
6
|
type ChatRole = "AI" | "USER";
|
|
5
|
-
type
|
|
7
|
+
type BaseMessage = {
|
|
6
8
|
id: number | string;
|
|
7
9
|
role: ChatRole;
|
|
8
10
|
content: string;
|
|
9
11
|
isLoading?: boolean;
|
|
10
12
|
};
|
|
13
|
+
type Message<T = {}> = BaseMessage & T;
|
|
11
14
|
|
|
12
15
|
type Size$1 = 'xs' | 'sm' | 'md' | 'lg';
|
|
13
16
|
type Props$5 = {
|
|
@@ -34,14 +37,15 @@ type Props$3 = Message & {
|
|
|
34
37
|
};
|
|
35
38
|
declare function ChatMessage({ id, role, content, isLoading, wrapperClassName, icon, aiIconWrapperClassName, aiIconColor, bubbleClassName, aiBubbleClassName, userBubbleClassName, position, loadingRenderer, }: Props$3): react_jsx_runtime.JSX.Element;
|
|
36
39
|
|
|
37
|
-
type
|
|
40
|
+
type MessagePatch = Partial<BaseMessage> & Record<string, unknown>;
|
|
41
|
+
type Props$2<T extends Message = Message> = {
|
|
38
42
|
messages: T[];
|
|
39
|
-
messageMapper?: (msg: T) =>
|
|
40
|
-
messageRenderer?: (msg:
|
|
43
|
+
messageMapper?: (msg: T) => MessagePatch;
|
|
44
|
+
messageRenderer?: (msg: T) => React$1.ReactNode;
|
|
41
45
|
className?: string;
|
|
42
46
|
loadingRenderer?: React$1.ReactNode;
|
|
43
47
|
};
|
|
44
|
-
declare function ChatList<T>({ messages, messageMapper, messageRenderer, className, loadingRenderer, }: Props$2<T>): react_jsx_runtime.JSX.Element;
|
|
48
|
+
declare function ChatList<T extends Message>({ messages, messageMapper, messageRenderer, className, loadingRenderer, }: Props$2<T>): react_jsx_runtime.JSX.Element;
|
|
45
49
|
|
|
46
50
|
type VoiceRecognitionController$1 = {
|
|
47
51
|
start: () => void;
|
|
@@ -70,9 +74,15 @@ type Props$1 = {
|
|
|
70
74
|
};
|
|
71
75
|
declare function ChatInput({ onSend, voice, placeholder, className, inputClassName, micButton, recordingButton, sendButton, sendingButton, maxHeight, value, onChange, isSending, submitOnEnter, }: Props$1): react_jsx_runtime.JSX.Element;
|
|
72
76
|
|
|
73
|
-
type
|
|
77
|
+
type MessageProps = {
|
|
78
|
+
messages: Message[];
|
|
79
|
+
messageMapper?: never;
|
|
80
|
+
};
|
|
81
|
+
type RawProps<T> = {
|
|
74
82
|
messages: T[];
|
|
75
|
-
messageMapper
|
|
83
|
+
messageMapper: (msg: T) => Message;
|
|
84
|
+
};
|
|
85
|
+
type CommonProps = {
|
|
76
86
|
messageRenderer?: (msg: Message) => React.ReactNode;
|
|
77
87
|
loadingRenderer?: React.ReactNode;
|
|
78
88
|
listClassName?: string;
|
|
@@ -81,25 +91,39 @@ type Props<T> = {
|
|
|
81
91
|
disableVoice?: boolean;
|
|
82
92
|
placeholder?: string;
|
|
83
93
|
inputClassName?: string;
|
|
94
|
+
fetchNextPage?: () => void;
|
|
95
|
+
hasNextPage?: boolean;
|
|
96
|
+
isFetchingNextPage?: boolean;
|
|
84
97
|
className?: string;
|
|
85
98
|
};
|
|
86
|
-
|
|
99
|
+
type Props<T> = CommonProps & (MessageProps | RawProps<T>);
|
|
100
|
+
declare function ChatContainer<T>(props: Props<T>): react_jsx_runtime.JSX.Element;
|
|
87
101
|
|
|
88
|
-
type
|
|
89
|
-
type
|
|
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> = {
|
|
90
109
|
queryKey: readonly unknown[];
|
|
91
|
-
queryFn: () => Promise<
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
queryFn: (pageParam: unknown) => Promise<TRaw[]>;
|
|
111
|
+
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;
|
|
94
115
|
onError?: (error: unknown) => void;
|
|
95
116
|
staleTime?: number;
|
|
96
117
|
gcTime?: number;
|
|
97
118
|
};
|
|
98
|
-
declare function
|
|
99
|
-
messages:
|
|
119
|
+
declare function useChat<TRaw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, onError, staleTime, gcTime, }: Options$2<TRaw>): {
|
|
120
|
+
messages: MessageMapper$1<TRaw>[];
|
|
100
121
|
sendUserMessage: (content: string) => void;
|
|
101
122
|
isPending: boolean;
|
|
102
123
|
isInitialLoading: boolean;
|
|
124
|
+
fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<MessageMapper$1<TRaw>[], unknown>, Error>>;
|
|
125
|
+
hasNextPage: boolean;
|
|
126
|
+
isFetchingNextPage: boolean;
|
|
103
127
|
};
|
|
104
128
|
|
|
105
129
|
interface SpeechGrammar {
|
|
@@ -172,29 +196,39 @@ declare function useBrowserSpeechRecognition({ lang, onStart, onEnd, onError, }?
|
|
|
172
196
|
onTranscript: (text: string) => void;
|
|
173
197
|
};
|
|
174
198
|
|
|
175
|
-
type MessageMapper<TRaw> = (raw: TRaw) => Message;
|
|
176
199
|
type VoiceRecognitionController = {
|
|
177
200
|
start: () => void;
|
|
178
201
|
stop: () => void;
|
|
179
202
|
isRecording: boolean;
|
|
180
203
|
onTranscript: (text: string) => void;
|
|
181
204
|
};
|
|
182
|
-
type
|
|
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> = {
|
|
183
212
|
queryKey: readonly unknown[];
|
|
184
|
-
queryFn: () => Promise<
|
|
185
|
-
|
|
186
|
-
|
|
213
|
+
queryFn: (pageParam: unknown) => Promise<TRaw[]>;
|
|
214
|
+
initialPageParam: unknown;
|
|
215
|
+
getNextPageParam: (lastPage: MessageMapper<TRaw>[], allPages: MessageMapper<TRaw>[][]) => unknown;
|
|
216
|
+
mutationFn: (content: string) => Promise<TRaw>;
|
|
217
|
+
map: (raw: TRaw) => MessageMapperResult;
|
|
187
218
|
voice: VoiceRecognitionController;
|
|
188
219
|
onError?: (error: unknown) => void;
|
|
189
220
|
staleTime?: number;
|
|
190
221
|
gcTime?: number;
|
|
191
222
|
};
|
|
192
|
-
declare function
|
|
193
|
-
messages:
|
|
223
|
+
declare function useVoiceChat<TRaw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, voice, onError, staleTime, gcTime, }: Options<TRaw>): {
|
|
224
|
+
messages: MessageMapper<TRaw>[];
|
|
194
225
|
isPending: boolean;
|
|
195
226
|
isInitialLoading: boolean;
|
|
196
227
|
startRecording: () => Promise<void>;
|
|
197
228
|
stopRecording: () => void;
|
|
229
|
+
fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<MessageMapper<TRaw>[], unknown>, Error>>;
|
|
230
|
+
hasNextPage: boolean;
|
|
231
|
+
isFetchingNextPage: boolean;
|
|
198
232
|
};
|
|
199
233
|
|
|
200
|
-
export { ChatContainer, ChatInput, ChatList, ChatMessage, LoadingSpinner, type Message, SendingDots, useBrowserSpeechRecognition,
|
|
234
|
+
export { ChatContainer, ChatInput, ChatList, ChatMessage, LoadingSpinner, type Message, SendingDots, useBrowserSpeechRecognition, useChat, useVoiceChat };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React$1 from 'react';
|
|
3
|
+
import * as _tanstack_query_core from '@tanstack/query-core';
|
|
4
|
+
import { InfiniteData } from '@tanstack/react-query';
|
|
3
5
|
|
|
4
6
|
type ChatRole = "AI" | "USER";
|
|
5
|
-
type
|
|
7
|
+
type BaseMessage = {
|
|
6
8
|
id: number | string;
|
|
7
9
|
role: ChatRole;
|
|
8
10
|
content: string;
|
|
9
11
|
isLoading?: boolean;
|
|
10
12
|
};
|
|
13
|
+
type Message<T = {}> = BaseMessage & T;
|
|
11
14
|
|
|
12
15
|
type Size$1 = 'xs' | 'sm' | 'md' | 'lg';
|
|
13
16
|
type Props$5 = {
|
|
@@ -34,14 +37,15 @@ type Props$3 = Message & {
|
|
|
34
37
|
};
|
|
35
38
|
declare function ChatMessage({ id, role, content, isLoading, wrapperClassName, icon, aiIconWrapperClassName, aiIconColor, bubbleClassName, aiBubbleClassName, userBubbleClassName, position, loadingRenderer, }: Props$3): react_jsx_runtime.JSX.Element;
|
|
36
39
|
|
|
37
|
-
type
|
|
40
|
+
type MessagePatch = Partial<BaseMessage> & Record<string, unknown>;
|
|
41
|
+
type Props$2<T extends Message = Message> = {
|
|
38
42
|
messages: T[];
|
|
39
|
-
messageMapper?: (msg: T) =>
|
|
40
|
-
messageRenderer?: (msg:
|
|
43
|
+
messageMapper?: (msg: T) => MessagePatch;
|
|
44
|
+
messageRenderer?: (msg: T) => React$1.ReactNode;
|
|
41
45
|
className?: string;
|
|
42
46
|
loadingRenderer?: React$1.ReactNode;
|
|
43
47
|
};
|
|
44
|
-
declare function ChatList<T>({ messages, messageMapper, messageRenderer, className, loadingRenderer, }: Props$2<T>): react_jsx_runtime.JSX.Element;
|
|
48
|
+
declare function ChatList<T extends Message>({ messages, messageMapper, messageRenderer, className, loadingRenderer, }: Props$2<T>): react_jsx_runtime.JSX.Element;
|
|
45
49
|
|
|
46
50
|
type VoiceRecognitionController$1 = {
|
|
47
51
|
start: () => void;
|
|
@@ -70,9 +74,15 @@ type Props$1 = {
|
|
|
70
74
|
};
|
|
71
75
|
declare function ChatInput({ onSend, voice, placeholder, className, inputClassName, micButton, recordingButton, sendButton, sendingButton, maxHeight, value, onChange, isSending, submitOnEnter, }: Props$1): react_jsx_runtime.JSX.Element;
|
|
72
76
|
|
|
73
|
-
type
|
|
77
|
+
type MessageProps = {
|
|
78
|
+
messages: Message[];
|
|
79
|
+
messageMapper?: never;
|
|
80
|
+
};
|
|
81
|
+
type RawProps<T> = {
|
|
74
82
|
messages: T[];
|
|
75
|
-
messageMapper
|
|
83
|
+
messageMapper: (msg: T) => Message;
|
|
84
|
+
};
|
|
85
|
+
type CommonProps = {
|
|
76
86
|
messageRenderer?: (msg: Message) => React.ReactNode;
|
|
77
87
|
loadingRenderer?: React.ReactNode;
|
|
78
88
|
listClassName?: string;
|
|
@@ -81,25 +91,39 @@ type Props<T> = {
|
|
|
81
91
|
disableVoice?: boolean;
|
|
82
92
|
placeholder?: string;
|
|
83
93
|
inputClassName?: string;
|
|
94
|
+
fetchNextPage?: () => void;
|
|
95
|
+
hasNextPage?: boolean;
|
|
96
|
+
isFetchingNextPage?: boolean;
|
|
84
97
|
className?: string;
|
|
85
98
|
};
|
|
86
|
-
|
|
99
|
+
type Props<T> = CommonProps & (MessageProps | RawProps<T>);
|
|
100
|
+
declare function ChatContainer<T>(props: Props<T>): react_jsx_runtime.JSX.Element;
|
|
87
101
|
|
|
88
|
-
type
|
|
89
|
-
type
|
|
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> = {
|
|
90
109
|
queryKey: readonly unknown[];
|
|
91
|
-
queryFn: () => Promise<
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
queryFn: (pageParam: unknown) => Promise<TRaw[]>;
|
|
111
|
+
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;
|
|
94
115
|
onError?: (error: unknown) => void;
|
|
95
116
|
staleTime?: number;
|
|
96
117
|
gcTime?: number;
|
|
97
118
|
};
|
|
98
|
-
declare function
|
|
99
|
-
messages:
|
|
119
|
+
declare function useChat<TRaw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, onError, staleTime, gcTime, }: Options$2<TRaw>): {
|
|
120
|
+
messages: MessageMapper$1<TRaw>[];
|
|
100
121
|
sendUserMessage: (content: string) => void;
|
|
101
122
|
isPending: boolean;
|
|
102
123
|
isInitialLoading: boolean;
|
|
124
|
+
fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<MessageMapper$1<TRaw>[], unknown>, Error>>;
|
|
125
|
+
hasNextPage: boolean;
|
|
126
|
+
isFetchingNextPage: boolean;
|
|
103
127
|
};
|
|
104
128
|
|
|
105
129
|
interface SpeechGrammar {
|
|
@@ -172,29 +196,39 @@ declare function useBrowserSpeechRecognition({ lang, onStart, onEnd, onError, }?
|
|
|
172
196
|
onTranscript: (text: string) => void;
|
|
173
197
|
};
|
|
174
198
|
|
|
175
|
-
type MessageMapper<TRaw> = (raw: TRaw) => Message;
|
|
176
199
|
type VoiceRecognitionController = {
|
|
177
200
|
start: () => void;
|
|
178
201
|
stop: () => void;
|
|
179
202
|
isRecording: boolean;
|
|
180
203
|
onTranscript: (text: string) => void;
|
|
181
204
|
};
|
|
182
|
-
type
|
|
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> = {
|
|
183
212
|
queryKey: readonly unknown[];
|
|
184
|
-
queryFn: () => Promise<
|
|
185
|
-
|
|
186
|
-
|
|
213
|
+
queryFn: (pageParam: unknown) => Promise<TRaw[]>;
|
|
214
|
+
initialPageParam: unknown;
|
|
215
|
+
getNextPageParam: (lastPage: MessageMapper<TRaw>[], allPages: MessageMapper<TRaw>[][]) => unknown;
|
|
216
|
+
mutationFn: (content: string) => Promise<TRaw>;
|
|
217
|
+
map: (raw: TRaw) => MessageMapperResult;
|
|
187
218
|
voice: VoiceRecognitionController;
|
|
188
219
|
onError?: (error: unknown) => void;
|
|
189
220
|
staleTime?: number;
|
|
190
221
|
gcTime?: number;
|
|
191
222
|
};
|
|
192
|
-
declare function
|
|
193
|
-
messages:
|
|
223
|
+
declare function useVoiceChat<TRaw extends object>({ queryKey, queryFn, initialPageParam, getNextPageParam, mutationFn, map, voice, onError, staleTime, gcTime, }: Options<TRaw>): {
|
|
224
|
+
messages: MessageMapper<TRaw>[];
|
|
194
225
|
isPending: boolean;
|
|
195
226
|
isInitialLoading: boolean;
|
|
196
227
|
startRecording: () => Promise<void>;
|
|
197
228
|
stopRecording: () => void;
|
|
229
|
+
fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<InfiniteData<MessageMapper<TRaw>[], unknown>, Error>>;
|
|
230
|
+
hasNextPage: boolean;
|
|
231
|
+
isFetchingNextPage: boolean;
|
|
198
232
|
};
|
|
199
233
|
|
|
200
|
-
export { ChatContainer, ChatInput, ChatList, ChatMessage, LoadingSpinner, type Message, SendingDots, useBrowserSpeechRecognition,
|
|
234
|
+
export { ChatContainer, ChatInput, ChatList, ChatMessage, LoadingSpinner, type Message, SendingDots, useBrowserSpeechRecognition, useChat, useVoiceChat };
|
package/dist/index.js
CHANGED
|
@@ -54,8 +54,8 @@ __export(index_exports, {
|
|
|
54
54
|
LoadingSpinner: () => LoadingSpinner,
|
|
55
55
|
SendingDots: () => SendingDots,
|
|
56
56
|
useBrowserSpeechRecognition: () => useBrowserSpeechRecognition,
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
useChat: () => useChat,
|
|
58
|
+
useVoiceChat: () => useVoiceChat
|
|
59
59
|
});
|
|
60
60
|
module.exports = __toCommonJS(index_exports);
|
|
61
61
|
|
|
@@ -206,7 +206,7 @@ function ChatList({
|
|
|
206
206
|
className,
|
|
207
207
|
loadingRenderer
|
|
208
208
|
}) {
|
|
209
|
-
const mappedMessages = messageMapper ? messages.map(messageMapper) : messages;
|
|
209
|
+
const mappedMessages = messageMapper ? messages.map((msg) => __spreadValues(__spreadValues({}, msg), messageMapper(msg))) : messages;
|
|
210
210
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `flex flex-col ${className}`, children: mappedMessages.map((msg) => {
|
|
211
211
|
if (messageRenderer) {
|
|
212
212
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react2.default.Fragment, { children: messageRenderer(msg) }, msg.id);
|
|
@@ -518,32 +518,43 @@ function ChatInput({
|
|
|
518
518
|
// src/components/ChatContainer.tsx
|
|
519
519
|
var import_react5 = require("react");
|
|
520
520
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
521
|
-
function ChatContainer({
|
|
522
|
-
messages,
|
|
523
|
-
messageMapper,
|
|
524
|
-
messageRenderer,
|
|
525
|
-
loadingRenderer,
|
|
526
|
-
listClassName,
|
|
527
|
-
onSend,
|
|
528
|
-
isSending,
|
|
529
|
-
disableVoice,
|
|
530
|
-
placeholder,
|
|
531
|
-
inputClassName,
|
|
532
|
-
className
|
|
533
|
-
}) {
|
|
521
|
+
function ChatContainer(props) {
|
|
534
522
|
const [isAtBottom, setIsAtBottom] = (0, import_react5.useState)(true);
|
|
535
523
|
const scrollRef = (0, import_react5.useRef)(null);
|
|
524
|
+
const {
|
|
525
|
+
messages,
|
|
526
|
+
messageRenderer,
|
|
527
|
+
loadingRenderer,
|
|
528
|
+
listClassName,
|
|
529
|
+
onSend,
|
|
530
|
+
isSending,
|
|
531
|
+
disableVoice,
|
|
532
|
+
placeholder,
|
|
533
|
+
inputClassName,
|
|
534
|
+
fetchNextPage,
|
|
535
|
+
hasNextPage,
|
|
536
|
+
isFetchingNextPage,
|
|
537
|
+
className
|
|
538
|
+
} = props;
|
|
539
|
+
const mappedMessages = typeof props.messageMapper === "function" ? props.messages.map(props.messageMapper) : messages;
|
|
536
540
|
(0, import_react5.useEffect)(() => {
|
|
537
541
|
const el = scrollRef.current;
|
|
538
542
|
if (!el) return;
|
|
539
|
-
|
|
540
|
-
const handleScroll = () => {
|
|
543
|
+
const handleScroll = async () => {
|
|
541
544
|
const isBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 10;
|
|
542
545
|
setIsAtBottom(isBottom);
|
|
546
|
+
if (el.scrollTop === 0 && hasNextPage && !isFetchingNextPage && fetchNextPage) {
|
|
547
|
+
const prevScrollHeight = el.scrollHeight;
|
|
548
|
+
await fetchNextPage();
|
|
549
|
+
requestAnimationFrame(() => {
|
|
550
|
+
const newScrollHeight = el.scrollHeight;
|
|
551
|
+
el.scrollTop = newScrollHeight - prevScrollHeight;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
543
554
|
};
|
|
544
555
|
el.addEventListener("scroll", handleScroll);
|
|
545
556
|
return () => el.removeEventListener("scroll", handleScroll);
|
|
546
|
-
}, []);
|
|
557
|
+
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
|
547
558
|
(0, import_react5.useEffect)(() => {
|
|
548
559
|
const el = scrollRef.current;
|
|
549
560
|
if (!el) return;
|
|
@@ -571,17 +582,20 @@ function ChatContainer({
|
|
|
571
582
|
flex flex-col ${className || ""}
|
|
572
583
|
`,
|
|
573
584
|
children: [
|
|
574
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.
|
|
585
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
575
586
|
"div",
|
|
576
587
|
{
|
|
577
588
|
ref: scrollRef,
|
|
578
589
|
className: `flex-1 overflow-y-auto chatContainer-scroll p-2`,
|
|
579
|
-
children:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
590
|
+
children: [
|
|
591
|
+
hasNextPage && isFetchingNextPage && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex justify-center py-2", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LoadingSpinner, { size: "sm" }) }),
|
|
592
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
593
|
+
ChatList,
|
|
594
|
+
__spreadValues(__spreadValues(__spreadValues({
|
|
595
|
+
messages: mappedMessages
|
|
596
|
+
}, messageRenderer && { messageRenderer }), loadingRenderer && { loadingRenderer }), listClassName && { className: listClassName })
|
|
597
|
+
)
|
|
598
|
+
]
|
|
585
599
|
}
|
|
586
600
|
),
|
|
587
601
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex-shrink-0 relative", children: [
|
|
@@ -624,12 +638,26 @@ function ChatContainer({
|
|
|
624
638
|
) });
|
|
625
639
|
}
|
|
626
640
|
|
|
627
|
-
// src/hooks/
|
|
641
|
+
// src/hooks/useChat.ts
|
|
628
642
|
var import_react_query = require("@tanstack/react-query");
|
|
629
643
|
var import_react6 = require("react");
|
|
630
|
-
function
|
|
644
|
+
function splitRawToMessage(raw, mapped) {
|
|
645
|
+
const custom = {};
|
|
646
|
+
const mappedValues = new Set(Object.values(mapped));
|
|
647
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
648
|
+
if (!mappedValues.has(value)) {
|
|
649
|
+
custom[key] = value;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return __spreadProps(__spreadValues({}, mapped), {
|
|
653
|
+
custom
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
function useChat({
|
|
631
657
|
queryKey,
|
|
632
658
|
queryFn,
|
|
659
|
+
initialPageParam,
|
|
660
|
+
getNextPageParam,
|
|
633
661
|
mutationFn,
|
|
634
662
|
map,
|
|
635
663
|
onError,
|
|
@@ -639,17 +667,26 @@ function useOptimisticChat({
|
|
|
639
667
|
const [isPending, setIsPending] = (0, import_react6.useState)(false);
|
|
640
668
|
const queryClient = (0, import_react_query.useQueryClient)();
|
|
641
669
|
const {
|
|
642
|
-
data
|
|
643
|
-
isLoading: isInitialLoading
|
|
644
|
-
|
|
670
|
+
data,
|
|
671
|
+
isLoading: isInitialLoading,
|
|
672
|
+
fetchNextPage,
|
|
673
|
+
hasNextPage,
|
|
674
|
+
isFetchingNextPage
|
|
675
|
+
} = (0, import_react_query.useInfiniteQuery)({
|
|
645
676
|
queryKey,
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
677
|
+
initialPageParam,
|
|
678
|
+
queryFn: async ({ pageParam }) => {
|
|
679
|
+
const raw = await queryFn(pageParam);
|
|
680
|
+
return raw.map((r) => {
|
|
681
|
+
const mapped = map(r);
|
|
682
|
+
return splitRawToMessage(r, mapped);
|
|
683
|
+
});
|
|
649
684
|
},
|
|
685
|
+
getNextPageParam,
|
|
650
686
|
staleTime,
|
|
651
687
|
gcTime
|
|
652
688
|
});
|
|
689
|
+
const messages = data ? [...data.pages].reverse().flat() : [];
|
|
653
690
|
const mutation = (0, import_react_query.useMutation)({
|
|
654
691
|
mutationFn,
|
|
655
692
|
// (content: string) => Promise<TMutationRaw>
|
|
@@ -660,38 +697,47 @@ function useOptimisticChat({
|
|
|
660
697
|
await queryClient.cancelQueries({ queryKey });
|
|
661
698
|
}
|
|
662
699
|
queryClient.setQueryData(queryKey, (old) => {
|
|
663
|
-
|
|
664
|
-
return
|
|
665
|
-
|
|
666
|
-
|
|
700
|
+
var _a;
|
|
701
|
+
if (!old) return old;
|
|
702
|
+
const pages = [...old.pages];
|
|
703
|
+
const firstPage = (_a = pages[0]) != null ? _a : [];
|
|
704
|
+
pages[0] = [
|
|
705
|
+
...firstPage,
|
|
667
706
|
{
|
|
668
707
|
id: crypto.randomUUID(),
|
|
669
708
|
role: "USER",
|
|
670
|
-
content
|
|
709
|
+
content,
|
|
710
|
+
custom: {}
|
|
671
711
|
},
|
|
672
|
-
// AI placeholder 추가
|
|
673
712
|
{
|
|
674
713
|
id: crypto.randomUUID(),
|
|
675
714
|
role: "AI",
|
|
676
715
|
content: "",
|
|
677
|
-
isLoading: true
|
|
716
|
+
isLoading: true,
|
|
717
|
+
custom: {}
|
|
678
718
|
}
|
|
679
719
|
];
|
|
720
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
721
|
+
pages
|
|
722
|
+
});
|
|
680
723
|
});
|
|
681
|
-
return { prev };
|
|
724
|
+
return prev ? { prev } : {};
|
|
682
725
|
},
|
|
683
726
|
onSuccess: (rawAiResponse) => {
|
|
684
|
-
const
|
|
727
|
+
const mapped = map(rawAiResponse);
|
|
728
|
+
const aiMessage = splitRawToMessage(rawAiResponse, mapped);
|
|
685
729
|
queryClient.setQueryData(queryKey, (old) => {
|
|
686
|
-
if (!old
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
next[lastIndex] = __spreadProps(__spreadValues(__spreadValues({}, next[lastIndex]), aiMessage), {
|
|
730
|
+
if (!old) return old;
|
|
731
|
+
const pages = [...old.pages];
|
|
732
|
+
const firstPage = [...pages[0]];
|
|
733
|
+
const lastIndex = firstPage.length - 1;
|
|
734
|
+
firstPage[lastIndex] = __spreadProps(__spreadValues(__spreadValues({}, firstPage[lastIndex]), aiMessage), {
|
|
692
735
|
isLoading: false
|
|
693
736
|
});
|
|
694
|
-
|
|
737
|
+
pages[0] = firstPage;
|
|
738
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
739
|
+
pages
|
|
740
|
+
});
|
|
695
741
|
});
|
|
696
742
|
setIsPending(false);
|
|
697
743
|
},
|
|
@@ -701,10 +747,6 @@ function useOptimisticChat({
|
|
|
701
747
|
queryClient.setQueryData(queryKey, context.prev);
|
|
702
748
|
}
|
|
703
749
|
onError == null ? void 0 : onError(error);
|
|
704
|
-
},
|
|
705
|
-
// mutation 이후 서버 기준 최신 데이터 재동기화
|
|
706
|
-
onSettled: () => {
|
|
707
|
-
queryClient.invalidateQueries({ queryKey });
|
|
708
750
|
}
|
|
709
751
|
});
|
|
710
752
|
const sendUserMessage = (content) => {
|
|
@@ -718,17 +760,35 @@ function useOptimisticChat({
|
|
|
718
760
|
// (content: string) => void
|
|
719
761
|
isPending,
|
|
720
762
|
// 사용자가 채팅 전송 후 AI 응답이 올 때까지의 로딩
|
|
721
|
-
isInitialLoading
|
|
763
|
+
isInitialLoading,
|
|
722
764
|
// 초기 로딩 상태
|
|
765
|
+
// infinite query용
|
|
766
|
+
fetchNextPage,
|
|
767
|
+
hasNextPage,
|
|
768
|
+
isFetchingNextPage
|
|
723
769
|
};
|
|
724
770
|
}
|
|
725
771
|
|
|
726
|
-
// src/hooks/
|
|
772
|
+
// src/hooks/useVoiceChat.ts
|
|
727
773
|
var import_react_query2 = require("@tanstack/react-query");
|
|
728
774
|
var import_react7 = require("react");
|
|
729
|
-
function
|
|
775
|
+
function splitRawToMessage2(raw, mapped) {
|
|
776
|
+
const custom = {};
|
|
777
|
+
const mappedValues = new Set(Object.values(mapped));
|
|
778
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
779
|
+
if (!mappedValues.has(value)) {
|
|
780
|
+
custom[key] = value;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return __spreadProps(__spreadValues({}, mapped), {
|
|
784
|
+
custom
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
function useVoiceChat({
|
|
730
788
|
queryKey,
|
|
731
789
|
queryFn,
|
|
790
|
+
initialPageParam,
|
|
791
|
+
getNextPageParam,
|
|
732
792
|
mutationFn,
|
|
733
793
|
map,
|
|
734
794
|
voice,
|
|
@@ -741,17 +801,26 @@ function useVoiceOptimisticChat({
|
|
|
741
801
|
const currentTextRef = (0, import_react7.useRef)("");
|
|
742
802
|
const rollbackRef = (0, import_react7.useRef)(void 0);
|
|
743
803
|
const {
|
|
744
|
-
data
|
|
745
|
-
isLoading: isInitialLoading
|
|
746
|
-
|
|
804
|
+
data,
|
|
805
|
+
isLoading: isInitialLoading,
|
|
806
|
+
fetchNextPage,
|
|
807
|
+
hasNextPage,
|
|
808
|
+
isFetchingNextPage
|
|
809
|
+
} = (0, import_react_query2.useInfiniteQuery)({
|
|
747
810
|
queryKey,
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
811
|
+
initialPageParam,
|
|
812
|
+
queryFn: async ({ pageParam }) => {
|
|
813
|
+
const raw = await queryFn(pageParam);
|
|
814
|
+
return raw.map((r) => {
|
|
815
|
+
const mapped = map(r);
|
|
816
|
+
return splitRawToMessage2(r, mapped);
|
|
817
|
+
});
|
|
751
818
|
},
|
|
819
|
+
getNextPageParam,
|
|
752
820
|
staleTime,
|
|
753
821
|
gcTime
|
|
754
822
|
});
|
|
823
|
+
const messages = data ? data.pages.map((page) => [...page]).reverse().flat() : [];
|
|
755
824
|
const mutation = (0, import_react_query2.useMutation)({
|
|
756
825
|
mutationFn,
|
|
757
826
|
// (content: string) => Promise<TMutationRaw>
|
|
@@ -762,32 +831,41 @@ function useVoiceOptimisticChat({
|
|
|
762
831
|
await queryClient.cancelQueries({ queryKey });
|
|
763
832
|
}
|
|
764
833
|
queryClient.setQueryData(queryKey, (old) => {
|
|
765
|
-
|
|
766
|
-
return
|
|
767
|
-
|
|
768
|
-
|
|
834
|
+
var _a;
|
|
835
|
+
if (!old) return old;
|
|
836
|
+
const pages = [...old.pages];
|
|
837
|
+
const firstPage = (_a = pages[0]) != null ? _a : [];
|
|
838
|
+
pages[0] = [
|
|
839
|
+
...firstPage,
|
|
769
840
|
{
|
|
770
841
|
id: crypto.randomUUID(),
|
|
771
842
|
role: "AI",
|
|
772
843
|
content: "",
|
|
773
|
-
isLoading: true
|
|
844
|
+
isLoading: true,
|
|
845
|
+
custom: {}
|
|
774
846
|
}
|
|
775
847
|
];
|
|
848
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
849
|
+
pages
|
|
850
|
+
});
|
|
776
851
|
});
|
|
777
|
-
return { prev };
|
|
852
|
+
return prev ? { prev } : {};
|
|
778
853
|
},
|
|
779
854
|
onSuccess: (rawAiResponse) => {
|
|
780
|
-
const
|
|
855
|
+
const mapped = map(rawAiResponse);
|
|
856
|
+
const aiMessage = splitRawToMessage2(rawAiResponse, mapped);
|
|
781
857
|
queryClient.setQueryData(queryKey, (old) => {
|
|
782
|
-
if (!old
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
next[lastIndex] = __spreadProps(__spreadValues(__spreadValues({}, next[lastIndex]), aiMessage), {
|
|
858
|
+
if (!old) return old;
|
|
859
|
+
const pages = [...old.pages];
|
|
860
|
+
const firstPage = [...pages[0]];
|
|
861
|
+
const lastIndex = firstPage.length - 1;
|
|
862
|
+
firstPage[lastIndex] = __spreadProps(__spreadValues(__spreadValues({}, firstPage[lastIndex]), aiMessage), {
|
|
788
863
|
isLoading: false
|
|
789
864
|
});
|
|
790
|
-
|
|
865
|
+
pages[0] = firstPage;
|
|
866
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
867
|
+
pages
|
|
868
|
+
});
|
|
791
869
|
});
|
|
792
870
|
setIsPending(false);
|
|
793
871
|
},
|
|
@@ -797,10 +875,6 @@ function useVoiceOptimisticChat({
|
|
|
797
875
|
queryClient.setQueryData(queryKey, context.prev);
|
|
798
876
|
}
|
|
799
877
|
onError == null ? void 0 : onError(error);
|
|
800
|
-
},
|
|
801
|
-
// mutation 이후 서버 기준 최신 데이터 재동기화
|
|
802
|
-
onSettled: () => {
|
|
803
|
-
queryClient.invalidateQueries({ queryKey });
|
|
804
878
|
}
|
|
805
879
|
});
|
|
806
880
|
const startRecording = async () => {
|
|
@@ -810,14 +884,24 @@ function useVoiceOptimisticChat({
|
|
|
810
884
|
if (prev) {
|
|
811
885
|
await queryClient.cancelQueries({ queryKey });
|
|
812
886
|
}
|
|
813
|
-
queryClient.setQueryData(queryKey, (old) =>
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
887
|
+
queryClient.setQueryData(queryKey, (old) => {
|
|
888
|
+
var _a;
|
|
889
|
+
if (!old) return old;
|
|
890
|
+
const pages = [...old.pages];
|
|
891
|
+
const firstPage = (_a = pages[0]) != null ? _a : [];
|
|
892
|
+
pages[0] = [
|
|
893
|
+
...firstPage,
|
|
894
|
+
{
|
|
895
|
+
id: crypto.randomUUID(),
|
|
896
|
+
role: "USER",
|
|
897
|
+
content: "",
|
|
898
|
+
custom: {}
|
|
899
|
+
}
|
|
900
|
+
];
|
|
901
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
902
|
+
pages
|
|
903
|
+
});
|
|
904
|
+
});
|
|
821
905
|
voice.start();
|
|
822
906
|
};
|
|
823
907
|
const onTranscript = (text) => {
|
|
@@ -825,13 +909,17 @@ function useVoiceOptimisticChat({
|
|
|
825
909
|
queryClient.setQueryData(queryKey, (old) => {
|
|
826
910
|
var _a;
|
|
827
911
|
if (!old) return old;
|
|
828
|
-
const
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
912
|
+
const pages = [...old.pages];
|
|
913
|
+
const firstPage = [...pages[0]];
|
|
914
|
+
const lastIndex = firstPage.length - 1;
|
|
915
|
+
if (((_a = firstPage[lastIndex]) == null ? void 0 : _a.role) !== "USER") return old;
|
|
916
|
+
firstPage[lastIndex] = __spreadProps(__spreadValues({}, firstPage[lastIndex]), {
|
|
832
917
|
content: text
|
|
833
918
|
});
|
|
834
|
-
|
|
919
|
+
pages[0] = firstPage;
|
|
920
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
921
|
+
pages
|
|
922
|
+
});
|
|
835
923
|
});
|
|
836
924
|
};
|
|
837
925
|
(0, import_react7.useEffect)(() => {
|
|
@@ -850,15 +938,19 @@ function useVoiceOptimisticChat({
|
|
|
850
938
|
};
|
|
851
939
|
return {
|
|
852
940
|
messages,
|
|
853
|
-
// Message[]
|
|
941
|
+
// Message<TExtra>[]
|
|
854
942
|
isPending,
|
|
855
943
|
// 사용자가 채팅 전송 후 AI 응답이 올 때까지의 로딩
|
|
856
944
|
isInitialLoading,
|
|
857
945
|
// 초기 로딩 상태
|
|
858
946
|
startRecording,
|
|
859
947
|
// 음성 인식 시작 함수
|
|
860
|
-
stopRecording
|
|
948
|
+
stopRecording,
|
|
861
949
|
// 음성 인식 종료 함수
|
|
950
|
+
// infinite query용
|
|
951
|
+
fetchNextPage,
|
|
952
|
+
hasNextPage,
|
|
953
|
+
isFetchingNextPage
|
|
862
954
|
};
|
|
863
955
|
}
|
|
864
956
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -870,6 +962,6 @@ function useVoiceOptimisticChat({
|
|
|
870
962
|
LoadingSpinner,
|
|
871
963
|
SendingDots,
|
|
872
964
|
useBrowserSpeechRecognition,
|
|
873
|
-
|
|
874
|
-
|
|
965
|
+
useChat,
|
|
966
|
+
useVoiceChat
|
|
875
967
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -165,7 +165,7 @@ function ChatList({
|
|
|
165
165
|
className,
|
|
166
166
|
loadingRenderer
|
|
167
167
|
}) {
|
|
168
|
-
const mappedMessages = messageMapper ? messages.map(messageMapper) : messages;
|
|
168
|
+
const mappedMessages = messageMapper ? messages.map((msg) => __spreadValues(__spreadValues({}, msg), messageMapper(msg))) : messages;
|
|
169
169
|
return /* @__PURE__ */ jsx4("div", { className: `flex flex-col ${className}`, children: mappedMessages.map((msg) => {
|
|
170
170
|
if (messageRenderer) {
|
|
171
171
|
return /* @__PURE__ */ jsx4(React2.Fragment, { children: messageRenderer(msg) }, msg.id);
|
|
@@ -477,32 +477,43 @@ function ChatInput({
|
|
|
477
477
|
// src/components/ChatContainer.tsx
|
|
478
478
|
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
479
479
|
import { Fragment, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
480
|
-
function ChatContainer({
|
|
481
|
-
messages,
|
|
482
|
-
messageMapper,
|
|
483
|
-
messageRenderer,
|
|
484
|
-
loadingRenderer,
|
|
485
|
-
listClassName,
|
|
486
|
-
onSend,
|
|
487
|
-
isSending,
|
|
488
|
-
disableVoice,
|
|
489
|
-
placeholder,
|
|
490
|
-
inputClassName,
|
|
491
|
-
className
|
|
492
|
-
}) {
|
|
480
|
+
function ChatContainer(props) {
|
|
493
481
|
const [isAtBottom, setIsAtBottom] = useState3(true);
|
|
494
482
|
const scrollRef = useRef3(null);
|
|
483
|
+
const {
|
|
484
|
+
messages,
|
|
485
|
+
messageRenderer,
|
|
486
|
+
loadingRenderer,
|
|
487
|
+
listClassName,
|
|
488
|
+
onSend,
|
|
489
|
+
isSending,
|
|
490
|
+
disableVoice,
|
|
491
|
+
placeholder,
|
|
492
|
+
inputClassName,
|
|
493
|
+
fetchNextPage,
|
|
494
|
+
hasNextPage,
|
|
495
|
+
isFetchingNextPage,
|
|
496
|
+
className
|
|
497
|
+
} = props;
|
|
498
|
+
const mappedMessages = typeof props.messageMapper === "function" ? props.messages.map(props.messageMapper) : messages;
|
|
495
499
|
useEffect3(() => {
|
|
496
500
|
const el = scrollRef.current;
|
|
497
501
|
if (!el) return;
|
|
498
|
-
|
|
499
|
-
const handleScroll = () => {
|
|
502
|
+
const handleScroll = async () => {
|
|
500
503
|
const isBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 10;
|
|
501
504
|
setIsAtBottom(isBottom);
|
|
505
|
+
if (el.scrollTop === 0 && hasNextPage && !isFetchingNextPage && fetchNextPage) {
|
|
506
|
+
const prevScrollHeight = el.scrollHeight;
|
|
507
|
+
await fetchNextPage();
|
|
508
|
+
requestAnimationFrame(() => {
|
|
509
|
+
const newScrollHeight = el.scrollHeight;
|
|
510
|
+
el.scrollTop = newScrollHeight - prevScrollHeight;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
502
513
|
};
|
|
503
514
|
el.addEventListener("scroll", handleScroll);
|
|
504
515
|
return () => el.removeEventListener("scroll", handleScroll);
|
|
505
|
-
}, []);
|
|
516
|
+
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
|
506
517
|
useEffect3(() => {
|
|
507
518
|
const el = scrollRef.current;
|
|
508
519
|
if (!el) return;
|
|
@@ -530,17 +541,20 @@ function ChatContainer({
|
|
|
530
541
|
flex flex-col ${className || ""}
|
|
531
542
|
`,
|
|
532
543
|
children: [
|
|
533
|
-
/* @__PURE__ */
|
|
544
|
+
/* @__PURE__ */ jsxs4(
|
|
534
545
|
"div",
|
|
535
546
|
{
|
|
536
547
|
ref: scrollRef,
|
|
537
548
|
className: `flex-1 overflow-y-auto chatContainer-scroll p-2`,
|
|
538
|
-
children:
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
549
|
+
children: [
|
|
550
|
+
hasNextPage && isFetchingNextPage && /* @__PURE__ */ jsx6("div", { className: "flex justify-center py-2", children: /* @__PURE__ */ jsx6(LoadingSpinner, { size: "sm" }) }),
|
|
551
|
+
/* @__PURE__ */ jsx6(
|
|
552
|
+
ChatList,
|
|
553
|
+
__spreadValues(__spreadValues(__spreadValues({
|
|
554
|
+
messages: mappedMessages
|
|
555
|
+
}, messageRenderer && { messageRenderer }), loadingRenderer && { loadingRenderer }), listClassName && { className: listClassName })
|
|
556
|
+
)
|
|
557
|
+
]
|
|
544
558
|
}
|
|
545
559
|
),
|
|
546
560
|
/* @__PURE__ */ jsxs4("div", { className: "flex-shrink-0 relative", children: [
|
|
@@ -583,12 +597,26 @@ function ChatContainer({
|
|
|
583
597
|
) });
|
|
584
598
|
}
|
|
585
599
|
|
|
586
|
-
// src/hooks/
|
|
587
|
-
import {
|
|
600
|
+
// src/hooks/useChat.ts
|
|
601
|
+
import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
588
602
|
import { useState as useState4 } from "react";
|
|
589
|
-
function
|
|
603
|
+
function splitRawToMessage(raw, mapped) {
|
|
604
|
+
const custom = {};
|
|
605
|
+
const mappedValues = new Set(Object.values(mapped));
|
|
606
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
607
|
+
if (!mappedValues.has(value)) {
|
|
608
|
+
custom[key] = value;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return __spreadProps(__spreadValues({}, mapped), {
|
|
612
|
+
custom
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
function useChat({
|
|
590
616
|
queryKey,
|
|
591
617
|
queryFn,
|
|
618
|
+
initialPageParam,
|
|
619
|
+
getNextPageParam,
|
|
592
620
|
mutationFn,
|
|
593
621
|
map,
|
|
594
622
|
onError,
|
|
@@ -598,17 +626,26 @@ function useOptimisticChat({
|
|
|
598
626
|
const [isPending, setIsPending] = useState4(false);
|
|
599
627
|
const queryClient = useQueryClient();
|
|
600
628
|
const {
|
|
601
|
-
data
|
|
602
|
-
isLoading: isInitialLoading
|
|
603
|
-
|
|
629
|
+
data,
|
|
630
|
+
isLoading: isInitialLoading,
|
|
631
|
+
fetchNextPage,
|
|
632
|
+
hasNextPage,
|
|
633
|
+
isFetchingNextPage
|
|
634
|
+
} = useInfiniteQuery({
|
|
604
635
|
queryKey,
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
636
|
+
initialPageParam,
|
|
637
|
+
queryFn: async ({ pageParam }) => {
|
|
638
|
+
const raw = await queryFn(pageParam);
|
|
639
|
+
return raw.map((r) => {
|
|
640
|
+
const mapped = map(r);
|
|
641
|
+
return splitRawToMessage(r, mapped);
|
|
642
|
+
});
|
|
608
643
|
},
|
|
644
|
+
getNextPageParam,
|
|
609
645
|
staleTime,
|
|
610
646
|
gcTime
|
|
611
647
|
});
|
|
648
|
+
const messages = data ? [...data.pages].reverse().flat() : [];
|
|
612
649
|
const mutation = useMutation({
|
|
613
650
|
mutationFn,
|
|
614
651
|
// (content: string) => Promise<TMutationRaw>
|
|
@@ -619,38 +656,47 @@ function useOptimisticChat({
|
|
|
619
656
|
await queryClient.cancelQueries({ queryKey });
|
|
620
657
|
}
|
|
621
658
|
queryClient.setQueryData(queryKey, (old) => {
|
|
622
|
-
|
|
623
|
-
return
|
|
624
|
-
|
|
625
|
-
|
|
659
|
+
var _a;
|
|
660
|
+
if (!old) return old;
|
|
661
|
+
const pages = [...old.pages];
|
|
662
|
+
const firstPage = (_a = pages[0]) != null ? _a : [];
|
|
663
|
+
pages[0] = [
|
|
664
|
+
...firstPage,
|
|
626
665
|
{
|
|
627
666
|
id: crypto.randomUUID(),
|
|
628
667
|
role: "USER",
|
|
629
|
-
content
|
|
668
|
+
content,
|
|
669
|
+
custom: {}
|
|
630
670
|
},
|
|
631
|
-
// AI placeholder 추가
|
|
632
671
|
{
|
|
633
672
|
id: crypto.randomUUID(),
|
|
634
673
|
role: "AI",
|
|
635
674
|
content: "",
|
|
636
|
-
isLoading: true
|
|
675
|
+
isLoading: true,
|
|
676
|
+
custom: {}
|
|
637
677
|
}
|
|
638
678
|
];
|
|
679
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
680
|
+
pages
|
|
681
|
+
});
|
|
639
682
|
});
|
|
640
|
-
return { prev };
|
|
683
|
+
return prev ? { prev } : {};
|
|
641
684
|
},
|
|
642
685
|
onSuccess: (rawAiResponse) => {
|
|
643
|
-
const
|
|
686
|
+
const mapped = map(rawAiResponse);
|
|
687
|
+
const aiMessage = splitRawToMessage(rawAiResponse, mapped);
|
|
644
688
|
queryClient.setQueryData(queryKey, (old) => {
|
|
645
|
-
if (!old
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
next[lastIndex] = __spreadProps(__spreadValues(__spreadValues({}, next[lastIndex]), aiMessage), {
|
|
689
|
+
if (!old) return old;
|
|
690
|
+
const pages = [...old.pages];
|
|
691
|
+
const firstPage = [...pages[0]];
|
|
692
|
+
const lastIndex = firstPage.length - 1;
|
|
693
|
+
firstPage[lastIndex] = __spreadProps(__spreadValues(__spreadValues({}, firstPage[lastIndex]), aiMessage), {
|
|
651
694
|
isLoading: false
|
|
652
695
|
});
|
|
653
|
-
|
|
696
|
+
pages[0] = firstPage;
|
|
697
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
698
|
+
pages
|
|
699
|
+
});
|
|
654
700
|
});
|
|
655
701
|
setIsPending(false);
|
|
656
702
|
},
|
|
@@ -660,10 +706,6 @@ function useOptimisticChat({
|
|
|
660
706
|
queryClient.setQueryData(queryKey, context.prev);
|
|
661
707
|
}
|
|
662
708
|
onError == null ? void 0 : onError(error);
|
|
663
|
-
},
|
|
664
|
-
// mutation 이후 서버 기준 최신 데이터 재동기화
|
|
665
|
-
onSettled: () => {
|
|
666
|
-
queryClient.invalidateQueries({ queryKey });
|
|
667
709
|
}
|
|
668
710
|
});
|
|
669
711
|
const sendUserMessage = (content) => {
|
|
@@ -677,17 +719,35 @@ function useOptimisticChat({
|
|
|
677
719
|
// (content: string) => void
|
|
678
720
|
isPending,
|
|
679
721
|
// 사용자가 채팅 전송 후 AI 응답이 올 때까지의 로딩
|
|
680
|
-
isInitialLoading
|
|
722
|
+
isInitialLoading,
|
|
681
723
|
// 초기 로딩 상태
|
|
724
|
+
// infinite query용
|
|
725
|
+
fetchNextPage,
|
|
726
|
+
hasNextPage,
|
|
727
|
+
isFetchingNextPage
|
|
682
728
|
};
|
|
683
729
|
}
|
|
684
730
|
|
|
685
|
-
// src/hooks/
|
|
686
|
-
import {
|
|
731
|
+
// src/hooks/useVoiceChat.ts
|
|
732
|
+
import { useInfiniteQuery as useInfiniteQuery2, useMutation as useMutation2, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
|
|
687
733
|
import { useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
|
|
688
|
-
function
|
|
734
|
+
function splitRawToMessage2(raw, mapped) {
|
|
735
|
+
const custom = {};
|
|
736
|
+
const mappedValues = new Set(Object.values(mapped));
|
|
737
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
738
|
+
if (!mappedValues.has(value)) {
|
|
739
|
+
custom[key] = value;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return __spreadProps(__spreadValues({}, mapped), {
|
|
743
|
+
custom
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
function useVoiceChat({
|
|
689
747
|
queryKey,
|
|
690
748
|
queryFn,
|
|
749
|
+
initialPageParam,
|
|
750
|
+
getNextPageParam,
|
|
691
751
|
mutationFn,
|
|
692
752
|
map,
|
|
693
753
|
voice,
|
|
@@ -700,17 +760,26 @@ function useVoiceOptimisticChat({
|
|
|
700
760
|
const currentTextRef = useRef4("");
|
|
701
761
|
const rollbackRef = useRef4(void 0);
|
|
702
762
|
const {
|
|
703
|
-
data
|
|
704
|
-
isLoading: isInitialLoading
|
|
705
|
-
|
|
763
|
+
data,
|
|
764
|
+
isLoading: isInitialLoading,
|
|
765
|
+
fetchNextPage,
|
|
766
|
+
hasNextPage,
|
|
767
|
+
isFetchingNextPage
|
|
768
|
+
} = useInfiniteQuery2({
|
|
706
769
|
queryKey,
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
770
|
+
initialPageParam,
|
|
771
|
+
queryFn: async ({ pageParam }) => {
|
|
772
|
+
const raw = await queryFn(pageParam);
|
|
773
|
+
return raw.map((r) => {
|
|
774
|
+
const mapped = map(r);
|
|
775
|
+
return splitRawToMessage2(r, mapped);
|
|
776
|
+
});
|
|
710
777
|
},
|
|
778
|
+
getNextPageParam,
|
|
711
779
|
staleTime,
|
|
712
780
|
gcTime
|
|
713
781
|
});
|
|
782
|
+
const messages = data ? data.pages.map((page) => [...page]).reverse().flat() : [];
|
|
714
783
|
const mutation = useMutation2({
|
|
715
784
|
mutationFn,
|
|
716
785
|
// (content: string) => Promise<TMutationRaw>
|
|
@@ -721,32 +790,41 @@ function useVoiceOptimisticChat({
|
|
|
721
790
|
await queryClient.cancelQueries({ queryKey });
|
|
722
791
|
}
|
|
723
792
|
queryClient.setQueryData(queryKey, (old) => {
|
|
724
|
-
|
|
725
|
-
return
|
|
726
|
-
|
|
727
|
-
|
|
793
|
+
var _a;
|
|
794
|
+
if (!old) return old;
|
|
795
|
+
const pages = [...old.pages];
|
|
796
|
+
const firstPage = (_a = pages[0]) != null ? _a : [];
|
|
797
|
+
pages[0] = [
|
|
798
|
+
...firstPage,
|
|
728
799
|
{
|
|
729
800
|
id: crypto.randomUUID(),
|
|
730
801
|
role: "AI",
|
|
731
802
|
content: "",
|
|
732
|
-
isLoading: true
|
|
803
|
+
isLoading: true,
|
|
804
|
+
custom: {}
|
|
733
805
|
}
|
|
734
806
|
];
|
|
807
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
808
|
+
pages
|
|
809
|
+
});
|
|
735
810
|
});
|
|
736
|
-
return { prev };
|
|
811
|
+
return prev ? { prev } : {};
|
|
737
812
|
},
|
|
738
813
|
onSuccess: (rawAiResponse) => {
|
|
739
|
-
const
|
|
814
|
+
const mapped = map(rawAiResponse);
|
|
815
|
+
const aiMessage = splitRawToMessage2(rawAiResponse, mapped);
|
|
740
816
|
queryClient.setQueryData(queryKey, (old) => {
|
|
741
|
-
if (!old
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
next[lastIndex] = __spreadProps(__spreadValues(__spreadValues({}, next[lastIndex]), aiMessage), {
|
|
817
|
+
if (!old) return old;
|
|
818
|
+
const pages = [...old.pages];
|
|
819
|
+
const firstPage = [...pages[0]];
|
|
820
|
+
const lastIndex = firstPage.length - 1;
|
|
821
|
+
firstPage[lastIndex] = __spreadProps(__spreadValues(__spreadValues({}, firstPage[lastIndex]), aiMessage), {
|
|
747
822
|
isLoading: false
|
|
748
823
|
});
|
|
749
|
-
|
|
824
|
+
pages[0] = firstPage;
|
|
825
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
826
|
+
pages
|
|
827
|
+
});
|
|
750
828
|
});
|
|
751
829
|
setIsPending(false);
|
|
752
830
|
},
|
|
@@ -756,10 +834,6 @@ function useVoiceOptimisticChat({
|
|
|
756
834
|
queryClient.setQueryData(queryKey, context.prev);
|
|
757
835
|
}
|
|
758
836
|
onError == null ? void 0 : onError(error);
|
|
759
|
-
},
|
|
760
|
-
// mutation 이후 서버 기준 최신 데이터 재동기화
|
|
761
|
-
onSettled: () => {
|
|
762
|
-
queryClient.invalidateQueries({ queryKey });
|
|
763
837
|
}
|
|
764
838
|
});
|
|
765
839
|
const startRecording = async () => {
|
|
@@ -769,14 +843,24 @@ function useVoiceOptimisticChat({
|
|
|
769
843
|
if (prev) {
|
|
770
844
|
await queryClient.cancelQueries({ queryKey });
|
|
771
845
|
}
|
|
772
|
-
queryClient.setQueryData(queryKey, (old) =>
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
846
|
+
queryClient.setQueryData(queryKey, (old) => {
|
|
847
|
+
var _a;
|
|
848
|
+
if (!old) return old;
|
|
849
|
+
const pages = [...old.pages];
|
|
850
|
+
const firstPage = (_a = pages[0]) != null ? _a : [];
|
|
851
|
+
pages[0] = [
|
|
852
|
+
...firstPage,
|
|
853
|
+
{
|
|
854
|
+
id: crypto.randomUUID(),
|
|
855
|
+
role: "USER",
|
|
856
|
+
content: "",
|
|
857
|
+
custom: {}
|
|
858
|
+
}
|
|
859
|
+
];
|
|
860
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
861
|
+
pages
|
|
862
|
+
});
|
|
863
|
+
});
|
|
780
864
|
voice.start();
|
|
781
865
|
};
|
|
782
866
|
const onTranscript = (text) => {
|
|
@@ -784,13 +868,17 @@ function useVoiceOptimisticChat({
|
|
|
784
868
|
queryClient.setQueryData(queryKey, (old) => {
|
|
785
869
|
var _a;
|
|
786
870
|
if (!old) return old;
|
|
787
|
-
const
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
871
|
+
const pages = [...old.pages];
|
|
872
|
+
const firstPage = [...pages[0]];
|
|
873
|
+
const lastIndex = firstPage.length - 1;
|
|
874
|
+
if (((_a = firstPage[lastIndex]) == null ? void 0 : _a.role) !== "USER") return old;
|
|
875
|
+
firstPage[lastIndex] = __spreadProps(__spreadValues({}, firstPage[lastIndex]), {
|
|
791
876
|
content: text
|
|
792
877
|
});
|
|
793
|
-
|
|
878
|
+
pages[0] = firstPage;
|
|
879
|
+
return __spreadProps(__spreadValues({}, old), {
|
|
880
|
+
pages
|
|
881
|
+
});
|
|
794
882
|
});
|
|
795
883
|
};
|
|
796
884
|
useEffect4(() => {
|
|
@@ -809,15 +897,19 @@ function useVoiceOptimisticChat({
|
|
|
809
897
|
};
|
|
810
898
|
return {
|
|
811
899
|
messages,
|
|
812
|
-
// Message[]
|
|
900
|
+
// Message<TExtra>[]
|
|
813
901
|
isPending,
|
|
814
902
|
// 사용자가 채팅 전송 후 AI 응답이 올 때까지의 로딩
|
|
815
903
|
isInitialLoading,
|
|
816
904
|
// 초기 로딩 상태
|
|
817
905
|
startRecording,
|
|
818
906
|
// 음성 인식 시작 함수
|
|
819
|
-
stopRecording
|
|
907
|
+
stopRecording,
|
|
820
908
|
// 음성 인식 종료 함수
|
|
909
|
+
// infinite query용
|
|
910
|
+
fetchNextPage,
|
|
911
|
+
hasNextPage,
|
|
912
|
+
isFetchingNextPage
|
|
821
913
|
};
|
|
822
914
|
}
|
|
823
915
|
export {
|
|
@@ -828,6 +920,6 @@ export {
|
|
|
828
920
|
LoadingSpinner,
|
|
829
921
|
SendingDots,
|
|
830
922
|
useBrowserSpeechRecognition,
|
|
831
|
-
|
|
832
|
-
|
|
923
|
+
useChat,
|
|
924
|
+
useVoiceChat
|
|
833
925
|
};
|