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 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 Message = {
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 Props$2<T> = {
40
+ type MessagePatch = Partial<BaseMessage> & Record<string, unknown>;
41
+ type Props$2<T extends Message = Message> = {
38
42
  messages: T[];
39
- messageMapper?: (msg: T) => Message;
40
- messageRenderer?: (msg: Message) => React$1.ReactNode;
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 Props<T> = {
77
+ type MessageProps = {
78
+ messages: Message[];
79
+ messageMapper?: never;
80
+ };
81
+ type RawProps<T> = {
74
82
  messages: T[];
75
- messageMapper?: (msg: T) => Message;
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
- declare function ChatContainer<T>({ messages, messageMapper, messageRenderer, loadingRenderer, listClassName, onSend, isSending, disableVoice, placeholder, inputClassName, className, }: Props<T>): react_jsx_runtime.JSX.Element;
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 MessageMapper$1<TRaw> = (raw: TRaw) => Message;
89
- type Options$2<TQueryRaw, TMutationRaw> = {
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<TQueryRaw[]>;
92
- mutationFn: (content: string) => Promise<TMutationRaw>;
93
- map: MessageMapper$1<TQueryRaw | TMutationRaw>;
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 useOptimisticChat<TQeuryRaw, TMutationRaw>({ queryKey, queryFn, mutationFn, map, onError, staleTime, gcTime, }: Options$2<TQeuryRaw, TMutationRaw>): {
99
- messages: Message[];
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 Options<TQueryRaw, TMutationRaw> = {
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<TQueryRaw[]>;
185
- mutationFn: (content: string) => Promise<TMutationRaw>;
186
- map: MessageMapper<TQueryRaw | TMutationRaw>;
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 useVoiceOptimisticChat<TQeuryRaw, TMutationRaw>({ queryKey, queryFn, mutationFn, map, voice, onError, staleTime, gcTime, }: Options<TQeuryRaw, TMutationRaw>): {
193
- messages: Message[];
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, useOptimisticChat, useVoiceOptimisticChat };
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 Message = {
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 Props$2<T> = {
40
+ type MessagePatch = Partial<BaseMessage> & Record<string, unknown>;
41
+ type Props$2<T extends Message = Message> = {
38
42
  messages: T[];
39
- messageMapper?: (msg: T) => Message;
40
- messageRenderer?: (msg: Message) => React$1.ReactNode;
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 Props<T> = {
77
+ type MessageProps = {
78
+ messages: Message[];
79
+ messageMapper?: never;
80
+ };
81
+ type RawProps<T> = {
74
82
  messages: T[];
75
- messageMapper?: (msg: T) => Message;
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
- declare function ChatContainer<T>({ messages, messageMapper, messageRenderer, loadingRenderer, listClassName, onSend, isSending, disableVoice, placeholder, inputClassName, className, }: Props<T>): react_jsx_runtime.JSX.Element;
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 MessageMapper$1<TRaw> = (raw: TRaw) => Message;
89
- type Options$2<TQueryRaw, TMutationRaw> = {
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<TQueryRaw[]>;
92
- mutationFn: (content: string) => Promise<TMutationRaw>;
93
- map: MessageMapper$1<TQueryRaw | TMutationRaw>;
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 useOptimisticChat<TQeuryRaw, TMutationRaw>({ queryKey, queryFn, mutationFn, map, onError, staleTime, gcTime, }: Options$2<TQeuryRaw, TMutationRaw>): {
99
- messages: Message[];
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 Options<TQueryRaw, TMutationRaw> = {
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<TQueryRaw[]>;
185
- mutationFn: (content: string) => Promise<TMutationRaw>;
186
- map: MessageMapper<TQueryRaw | TMutationRaw>;
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 useVoiceOptimisticChat<TQeuryRaw, TMutationRaw>({ queryKey, queryFn, mutationFn, map, voice, onError, staleTime, gcTime, }: Options<TQeuryRaw, TMutationRaw>): {
193
- messages: Message[];
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, useOptimisticChat, useVoiceOptimisticChat };
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
- useOptimisticChat: () => useOptimisticChat,
58
- useVoiceOptimisticChat: () => useVoiceOptimisticChat
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
- el.scrollTop = el.scrollHeight;
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.jsx)(
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: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
580
- ChatList,
581
- __spreadValues(__spreadValues(__spreadValues(__spreadValues({
582
- messages
583
- }, messageMapper && { messageMapper }), messageRenderer && { messageRenderer }), loadingRenderer && { loadingRenderer }), listClassName && { className: listClassName })
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/useOptimisticChat.ts
641
+ // src/hooks/useChat.ts
628
642
  var import_react_query = require("@tanstack/react-query");
629
643
  var import_react6 = require("react");
630
- function useOptimisticChat({
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: messages = [],
643
- isLoading: isInitialLoading
644
- } = (0, import_react_query.useQuery)({
670
+ data,
671
+ isLoading: isInitialLoading,
672
+ fetchNextPage,
673
+ hasNextPage,
674
+ isFetchingNextPage
675
+ } = (0, import_react_query.useInfiniteQuery)({
645
676
  queryKey,
646
- queryFn: async () => {
647
- const rawList = await queryFn();
648
- return rawList.map(map);
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
- const base = old != null ? old : [];
664
- return [
665
- ...base,
666
- // user 메시지 추가
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 aiMessage = map(rawAiResponse);
727
+ const mapped = map(rawAiResponse);
728
+ const aiMessage = splitRawToMessage(rawAiResponse, mapped);
685
729
  queryClient.setQueryData(queryKey, (old) => {
686
- if (!old || old.length === 0) {
687
- return [aiMessage];
688
- }
689
- const next = [...old];
690
- const lastIndex = next.length - 1;
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
- return next;
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/useVoiceOptimisticChat.ts
772
+ // src/hooks/useVoiceChat.ts
727
773
  var import_react_query2 = require("@tanstack/react-query");
728
774
  var import_react7 = require("react");
729
- function useVoiceOptimisticChat({
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: messages = [],
745
- isLoading: isInitialLoading
746
- } = (0, import_react_query2.useQuery)({
804
+ data,
805
+ isLoading: isInitialLoading,
806
+ fetchNextPage,
807
+ hasNextPage,
808
+ isFetchingNextPage
809
+ } = (0, import_react_query2.useInfiniteQuery)({
747
810
  queryKey,
748
- queryFn: async () => {
749
- const rawList = await queryFn();
750
- return rawList.map(map);
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
- const base = old != null ? old : [];
766
- return [
767
- ...base,
768
- // AI placeholder 추가
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 aiMessage = map(rawAiResponse);
855
+ const mapped = map(rawAiResponse);
856
+ const aiMessage = splitRawToMessage2(rawAiResponse, mapped);
781
857
  queryClient.setQueryData(queryKey, (old) => {
782
- if (!old || old.length === 0) {
783
- return [aiMessage];
784
- }
785
- const next = [...old];
786
- const lastIndex = next.length - 1;
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
- return next;
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
- ...old != null ? old : [],
815
- {
816
- id: crypto.randomUUID(),
817
- role: "USER",
818
- content: ""
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 next = [...old];
829
- const last = next.length - 1;
830
- if (((_a = next[last]) == null ? void 0 : _a.role) !== "USER") return old;
831
- next[last] = __spreadProps(__spreadValues({}, next[last]), {
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
- return next;
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
- useOptimisticChat,
874
- useVoiceOptimisticChat
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
- el.scrollTop = el.scrollHeight;
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__ */ jsx6(
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: /* @__PURE__ */ jsx6(
539
- ChatList,
540
- __spreadValues(__spreadValues(__spreadValues(__spreadValues({
541
- messages
542
- }, messageMapper && { messageMapper }), messageRenderer && { messageRenderer }), loadingRenderer && { loadingRenderer }), listClassName && { className: listClassName })
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/useOptimisticChat.ts
587
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
600
+ // src/hooks/useChat.ts
601
+ import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
588
602
  import { useState as useState4 } from "react";
589
- function useOptimisticChat({
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: messages = [],
602
- isLoading: isInitialLoading
603
- } = useQuery({
629
+ data,
630
+ isLoading: isInitialLoading,
631
+ fetchNextPage,
632
+ hasNextPage,
633
+ isFetchingNextPage
634
+ } = useInfiniteQuery({
604
635
  queryKey,
605
- queryFn: async () => {
606
- const rawList = await queryFn();
607
- return rawList.map(map);
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
- const base = old != null ? old : [];
623
- return [
624
- ...base,
625
- // user 메시지 추가
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 aiMessage = map(rawAiResponse);
686
+ const mapped = map(rawAiResponse);
687
+ const aiMessage = splitRawToMessage(rawAiResponse, mapped);
644
688
  queryClient.setQueryData(queryKey, (old) => {
645
- if (!old || old.length === 0) {
646
- return [aiMessage];
647
- }
648
- const next = [...old];
649
- const lastIndex = next.length - 1;
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
- return next;
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/useVoiceOptimisticChat.ts
686
- import { useMutation as useMutation2, useQuery as useQuery2, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
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 useVoiceOptimisticChat({
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: messages = [],
704
- isLoading: isInitialLoading
705
- } = useQuery2({
763
+ data,
764
+ isLoading: isInitialLoading,
765
+ fetchNextPage,
766
+ hasNextPage,
767
+ isFetchingNextPage
768
+ } = useInfiniteQuery2({
706
769
  queryKey,
707
- queryFn: async () => {
708
- const rawList = await queryFn();
709
- return rawList.map(map);
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
- const base = old != null ? old : [];
725
- return [
726
- ...base,
727
- // AI placeholder 추가
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 aiMessage = map(rawAiResponse);
814
+ const mapped = map(rawAiResponse);
815
+ const aiMessage = splitRawToMessage2(rawAiResponse, mapped);
740
816
  queryClient.setQueryData(queryKey, (old) => {
741
- if (!old || old.length === 0) {
742
- return [aiMessage];
743
- }
744
- const next = [...old];
745
- const lastIndex = next.length - 1;
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
- return next;
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
- ...old != null ? old : [],
774
- {
775
- id: crypto.randomUUID(),
776
- role: "USER",
777
- content: ""
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 next = [...old];
788
- const last = next.length - 1;
789
- if (((_a = next[last]) == null ? void 0 : _a.role) !== "USER") return old;
790
- next[last] = __spreadProps(__spreadValues({}, next[last]), {
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
- return next;
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
- useOptimisticChat,
832
- useVoiceOptimisticChat
923
+ useChat,
924
+ useVoiceChat
833
925
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-optimistic-chat",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",