Merge pull request #31 from pennyteenycat/AW-496-chat-improvement
AW-496-chat-improvement
This commit is contained in:
commit
d135674e46
42
package-lock.json
generated
42
package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@lottiefiles/dotlottie-react": "^0.14.1",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"client-only": "^0.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
"idb": "^8.0.3",
|
||||
@ -200,13 +201,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz",
|
||||
"integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==",
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
|
||||
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.15.0",
|
||||
"@eslint/core": "^0.15.1",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -214,9 +215,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz",
|
||||
"integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==",
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
||||
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@ -1308,6 +1309,33 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@lottiefiles/dotlottie-react": "^0.14.1",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"client-only": "^0.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
"idb": "^8.0.3",
|
||||
|
||||
@ -2,4 +2,5 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@ -8,5 +8,6 @@
|
||||
&.own {
|
||||
align-items: flex-end;
|
||||
align-self: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
import { useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { IChatMessage } from "@/entities/chats/types";
|
||||
import { useChat } from "@/providers/chat-provider";
|
||||
import { formatTime } from "@/shared/utils/date";
|
||||
|
||||
import MessageAudio from "./MessageAudio/MessageAudio";
|
||||
import MessageBubble from "./MessageBubble/MessageBubble";
|
||||
import MessageImage from "./MessageImage/MessageImage";
|
||||
import MessageMeta from "./MessageMeta/MessageMeta";
|
||||
import MessageStatus from "./MessageStatus/MessageStatus";
|
||||
import MessageText from "./MessageText/MessageText";
|
||||
@ -16,21 +16,23 @@ import MessageTyping from "./MessageTyping/MessageTyping";
|
||||
import styles from "./ChatMessage.module.scss";
|
||||
|
||||
export interface ChatMessageProps {
|
||||
message: {
|
||||
id: string;
|
||||
type: "text" | "image" | "audio" | "typing";
|
||||
content?: string;
|
||||
imageUrl?: string;
|
||||
audioUrl?: string;
|
||||
duration?: number;
|
||||
time: string | null;
|
||||
isOwn: boolean;
|
||||
isRead?: boolean;
|
||||
};
|
||||
// message: {
|
||||
// id: string;
|
||||
// type: "text" | "image" | "voice" | "typing";
|
||||
// text?: string;
|
||||
// imageUrl?: string;
|
||||
// audioUrl?: string;
|
||||
// duration?: number;
|
||||
// time: string | null;
|
||||
// isOwn: boolean;
|
||||
// isRead?: boolean;
|
||||
// };
|
||||
message: IChatMessage;
|
||||
}
|
||||
|
||||
export default function ChatMessage({ message }: ChatMessageProps) {
|
||||
const { isConnected, read } = useChat();
|
||||
const isOwn = message.role === "user";
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -45,37 +47,33 @@ export default function ChatMessage({ message }: ChatMessageProps) {
|
||||
}, [message.id, message.isRead, read, isConnected]);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.message, message.isOwn && styles.own)}>
|
||||
<MessageBubble isOwn={message.isOwn}>
|
||||
{message.type === "text" && (
|
||||
<MessageText text={message.content} isOwn={message.isOwn} />
|
||||
<div className={clsx(styles.message, isOwn && styles.own)}>
|
||||
<MessageBubble isOwn={isOwn}>
|
||||
{message.type === "text" && message.id !== "typing" && (
|
||||
<MessageText text={message.text} isOwn={isOwn} />
|
||||
)}
|
||||
|
||||
{message.type === "typing" && <MessageTyping />}
|
||||
{message.id === "typing" && <MessageTyping />}
|
||||
|
||||
{message.type === "image" && (
|
||||
{/* {message.type === "image" && (
|
||||
<>
|
||||
<MessageImage src={message.imageUrl || ""} />
|
||||
{message.content && (
|
||||
<MessageText text={message.content} isOwn={message.isOwn} />
|
||||
)}
|
||||
{message.text && <MessageText text={message.text} isOwn={isOwn} />}
|
||||
</>
|
||||
)}
|
||||
|
||||
{message.type === "audio" && (
|
||||
{message.type === "voice" && (
|
||||
<>
|
||||
<MessageAudio
|
||||
src={message.audioUrl || ""}
|
||||
duration={message.duration}
|
||||
/>
|
||||
{message.content && (
|
||||
<MessageText text={message.content} isOwn={message.isOwn} />
|
||||
)}
|
||||
{message.text && <MessageText text={message.text} isOwn={isOwn} />}
|
||||
</>
|
||||
)}
|
||||
)} */}
|
||||
</MessageBubble>
|
||||
<MessageMeta time={message.time}>
|
||||
{message.isOwn && <MessageStatus isRead={message.isRead} />}
|
||||
<MessageMeta time={formatTime(message.createdDate)}>
|
||||
{isOwn && <MessageStatus isRead={message.isRead} />}
|
||||
</MessageMeta>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -17,11 +17,11 @@ export default function ChatMessages({
|
||||
<ChatMessage
|
||||
message={{
|
||||
id: "typing",
|
||||
type: "typing",
|
||||
content: "…",
|
||||
isOwn: false,
|
||||
type: "text",
|
||||
text: "…",
|
||||
role: "assistant",
|
||||
isRead: false,
|
||||
time: "",
|
||||
createdDate: new Date().toISOString(),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,18 +1,45 @@
|
||||
.messagesWrapper {
|
||||
flex: 1 1 0%;
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
transition: padding-bottom 0.3s ease-in-out;
|
||||
position: relative;
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
.loaderTop {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.suggestions.suggestions {
|
||||
// position: sticky;
|
||||
// bottom: 0;
|
||||
padding: 0 16px 36px;
|
||||
margin-bottom: 0;
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
.scrollToBottomButton.scrollToBottomButton {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 444;
|
||||
padding: 8px;
|
||||
width: fit-content;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 6px #00000017;
|
||||
|
||||
& > .badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background-color: #fbbf24;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
max-width: 28px;
|
||||
max-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,93 +1,174 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
|
||||
import { Spinner } from "@/components/ui";
|
||||
import { Button, Icon, IconName, Spinner } from "@/components/ui";
|
||||
import { useChat } from "@/providers/chat-provider";
|
||||
import { useChatStore } from "@/providers/chat-store-provider";
|
||||
import { formatTime } from "@/shared/utils/date";
|
||||
|
||||
import { ChatMessages, Suggestions } from "..";
|
||||
import { ChatMessage, Suggestions } from "..";
|
||||
|
||||
import styles from "./ChatMessagesWrapper.module.scss";
|
||||
|
||||
export default function ChatMessagesWrapper() {
|
||||
const messagesWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isScrolledUp, setIsScrolledUp] = useState(false);
|
||||
|
||||
const {
|
||||
messages: socketMessages,
|
||||
isLoadingAdvisorMessage,
|
||||
hasMoreOlderMessages,
|
||||
isLoadingOlder,
|
||||
messagesWrapperRef,
|
||||
// unreadMessagesCount,
|
||||
loadOlder,
|
||||
scrollToBottom,
|
||||
send,
|
||||
} = useChat();
|
||||
|
||||
const { _hasHydrated } = useChatStore(state => state);
|
||||
const messages = useMemo(() => {
|
||||
const msgs = [...socketMessages];
|
||||
if (isLoadingAdvisorMessage) {
|
||||
msgs.unshift({
|
||||
id: "typing",
|
||||
type: "text",
|
||||
text: "…",
|
||||
role: "assistant",
|
||||
isRead: false,
|
||||
createdDate: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
return msgs;
|
||||
}, [isLoadingAdvisorMessage, socketMessages]);
|
||||
|
||||
const [isLoadOlder, setIsLoadOlder] = useState(false);
|
||||
const virtualizer = useVirtualizer({
|
||||
enabled: messages.length > 0,
|
||||
count: hasMoreOlderMessages ? messages.length + 1 : messages.length,
|
||||
getScrollElement: () => messagesWrapperRef.current,
|
||||
measureElement: el => el.getBoundingClientRect().height,
|
||||
getItemKey: idx => messages[idx]?.id ?? idx,
|
||||
estimateSize: _i => 100,
|
||||
overscan: 5,
|
||||
paddingStart: 36,
|
||||
paddingEnd: 36,
|
||||
gap: 8,
|
||||
});
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
const el = messagesWrapperRef.current;
|
||||
if (!el) return;
|
||||
const items = virtualizer.getVirtualItems();
|
||||
|
||||
if (el.scrollTop < 100) {
|
||||
setIsLoadOlder(true);
|
||||
const scrollToBottom = useCallback(() => {
|
||||
virtualizer.scrollToOffset(0);
|
||||
}, [virtualizer]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
const currentTarget = e.currentTarget as HTMLElement;
|
||||
|
||||
if (currentTarget) {
|
||||
currentTarget.scrollTop -= e.deltaY;
|
||||
}
|
||||
};
|
||||
messagesWrapperRef.current?.addEventListener("wheel", handleScroll, {
|
||||
passive: false,
|
||||
});
|
||||
return () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
messagesWrapperRef.current?.removeEventListener("wheel", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const [lastItem] = [...items].reverse();
|
||||
|
||||
if (!lastItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
lastItem.index >= messages.length - 1 &&
|
||||
hasMoreOlderMessages &&
|
||||
!isLoadingOlder
|
||||
) {
|
||||
loadOlder();
|
||||
}
|
||||
}, [loadOlder, messagesWrapperRef]);
|
||||
|
||||
const mappedMessages = useMemo(() => {
|
||||
const msgs = socketMessages.map(m => ({
|
||||
id: m.id,
|
||||
type: "text" as const,
|
||||
content: m.text,
|
||||
isOwn: m.role === "user",
|
||||
isRead: m.isRead,
|
||||
time: formatTime(m.createdDate),
|
||||
}));
|
||||
return msgs;
|
||||
}, [socketMessages]);
|
||||
}, [hasMoreOlderMessages, loadOlder, messages.length, isLoadingOlder, items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadOlder) {
|
||||
setIsLoadOlder(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [socketMessages]);
|
||||
if (!messagesWrapperRef.current || messages.length === 0) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (socketMessages.length > 0 && _hasHydrated && !isLoadOlder) {
|
||||
const timeout = setTimeout(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [socketMessages.length, scrollToBottom, _hasHydrated]);
|
||||
setIsScrolledUp((virtualizer.scrollOffset || 0) > 600);
|
||||
}, [virtualizer.scrollOffset, messages.length, messagesWrapperRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.messagesWrapper}
|
||||
ref={messagesWrapperRef}
|
||||
onScroll={handleScroll}
|
||||
<>
|
||||
{isScrolledUp && (
|
||||
<Button
|
||||
className={styles.scrollToBottomButton}
|
||||
onClick={scrollToBottom}
|
||||
aria-label="Scroll to bottom"
|
||||
style={{
|
||||
top: `${messagesWrapperRef.current?.clientHeight}px`,
|
||||
}}
|
||||
>
|
||||
{isLoadingOlder && hasMoreOlderMessages && (
|
||||
<div className={styles.loaderTop}>
|
||||
<Spinner size={16} />
|
||||
</div>
|
||||
)}
|
||||
<ChatMessages
|
||||
messages={mappedMessages}
|
||||
isLoadingAdvisorMessage={isLoadingAdvisorMessage}
|
||||
<Icon
|
||||
name={IconName.Chevron}
|
||||
style={{ transform: "rotate(-90deg)" }}
|
||||
/>
|
||||
{/* {!!unreadMessagesCount && (
|
||||
<Badge className={styles.badge}>
|
||||
<Typography weight="semiBold" size="xs" color="black">
|
||||
{unreadMessagesCount > 99 ? "99+" : unreadMessagesCount}
|
||||
</Typography>
|
||||
</Badge>
|
||||
)} */}
|
||||
</Button>
|
||||
)}
|
||||
<div ref={messagesWrapperRef} className={styles.messagesWrapper}>
|
||||
<Suggestions
|
||||
className={styles.suggestions}
|
||||
onSuggestionClick={suggestion => {
|
||||
send(suggestion);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
height: virtualizer.getTotalSize(),
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{items.map(virtualRow => {
|
||||
const message = messages[virtualRow.index];
|
||||
const isLoaderRow = virtualRow.index > messages.length - 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
data-index={virtualRow.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
paddingInline: "16px",
|
||||
transform: `translateY(${virtualRow.start}px) scaleY(-1)`,
|
||||
}}
|
||||
>
|
||||
{!isLoaderRow && (
|
||||
<ChatMessage key={message.id} message={message} />
|
||||
)}
|
||||
{isLoaderRow && (
|
||||
<div className={styles.loaderTop}>
|
||||
<Spinner size={16} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -30,10 +30,10 @@ const PartnerSchema = z
|
||||
address: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
birthdate: z.string(),
|
||||
gender: z.string(),
|
||||
age: z.number(),
|
||||
sign: z.string(),
|
||||
birthdate: z.string().optional(),
|
||||
gender: z.string().optional(),
|
||||
age: z.number().optional(),
|
||||
sign: z.string().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
|
||||
@ -20,15 +20,10 @@ import type {
|
||||
|
||||
const PAGE_LIMIT = 50;
|
||||
|
||||
type UIMessage = Pick<
|
||||
IChatMessage,
|
||||
"id" | "role" | "text" | "createdDate" | "isRead" | "suggestions" | "isLast"
|
||||
>;
|
||||
|
||||
interface UseChatSocketOptions {
|
||||
initialMessages?: IChatMessage[];
|
||||
initialTotal?: number;
|
||||
onNewMessage?: (message: UIMessage) => void;
|
||||
onNewMessage?: (message: IChatMessage) => void;
|
||||
}
|
||||
|
||||
export const useChatSocket = (
|
||||
@ -39,18 +34,8 @@ export const useChatSocket = (
|
||||
const status = useSocketStatus();
|
||||
const emit = useSocketEmit();
|
||||
|
||||
const mapApiMessage = (m: IChatMessage): UIMessage => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
text: m.text,
|
||||
createdDate: m.createdDate,
|
||||
isRead: m.isRead,
|
||||
suggestions: m.suggestions,
|
||||
isLast: m.isLast,
|
||||
});
|
||||
|
||||
const [messages, setMessages] = useState<UIMessage[]>(() =>
|
||||
options.initialMessages ? options.initialMessages.map(mapApiMessage) : []
|
||||
const [messages, setMessages] = useState<IChatMessage[]>(
|
||||
() => options.initialMessages || []
|
||||
);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalCount, _setTotalCount] = useState<number | null>(
|
||||
@ -60,7 +45,6 @@ export const useChatSocket = (
|
||||
const [balance, setBalance] = useState<ICurrentBalance | null>(null);
|
||||
const [session, setSession] = useState<ISessionStarted | null>(null);
|
||||
const [isLoadingSelfMessage, setIsLoadingSelfMessage] = useState(false);
|
||||
// const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false);
|
||||
const [isSessionExpired, setIsSessionExpired] = useState(false);
|
||||
const [refillModals, setRefillModals] = useState<IRefillModals | null>(null);
|
||||
const { suggestions, setSuggestions } = useChatStore(state => state);
|
||||
@ -73,6 +57,10 @@ export const useChatSocket = (
|
||||
);
|
||||
}, [messages]);
|
||||
|
||||
const unreadMessagesCount = useMemo(() => {
|
||||
return messages.filter(m => !m.isRead && m.role === "assistant").length;
|
||||
}, [messages]);
|
||||
|
||||
const joinChat = useCallback(
|
||||
() => emit("join_chat", { chatId }),
|
||||
[emit, chatId]
|
||||
@ -84,12 +72,13 @@ export const useChatSocket = (
|
||||
|
||||
const send = useCallback(
|
||||
(text: string) => {
|
||||
const sendingMessage = {
|
||||
const sendingMessage: IChatMessage = {
|
||||
id: `sending-message-${Date.now()}`,
|
||||
role: "user",
|
||||
text,
|
||||
createdDate: new Date().toISOString(),
|
||||
isRead: false,
|
||||
type: "text",
|
||||
};
|
||||
setMessages(prev => [sendingMessage, ...prev]);
|
||||
if (options.onNewMessage) {
|
||||
@ -97,14 +86,18 @@ export const useChatSocket = (
|
||||
}
|
||||
|
||||
setIsLoadingSelfMessage(true);
|
||||
// setIsLoadingAdvisorMessage(true);
|
||||
emit("send_message", { chatId, message: text });
|
||||
},
|
||||
[options, emit, chatId]
|
||||
);
|
||||
|
||||
const read = useCallback(
|
||||
(ids: string[]) => emit("read_message", { messages: ids }),
|
||||
(ids: string[]) => {
|
||||
emit("read_message", { messages: ids });
|
||||
// setMessages(prev =>
|
||||
// prev.map(m => (ids.includes(m.id) ? { ...m, isRead: true } : m))
|
||||
// );
|
||||
},
|
||||
[emit]
|
||||
);
|
||||
const startSession = useCallback(
|
||||
@ -149,10 +142,7 @@ export const useChatSocket = (
|
||||
const { messages: msgs } = data;
|
||||
setMessages(prev => {
|
||||
const ids = new Set(prev.map(m => m.id));
|
||||
return [
|
||||
...prev,
|
||||
...msgs.map(mapApiMessage).filter(m => !ids.has(m.id)),
|
||||
];
|
||||
return [...prev, ...msgs.filter(m => !ids.has(m.id))];
|
||||
});
|
||||
setPage(nextPage);
|
||||
} catch (e) {
|
||||
@ -167,26 +157,16 @@ export const useChatSocket = (
|
||||
if (!data?.length) return;
|
||||
|
||||
if (data[0].role === "user") setIsLoadingSelfMessage(false);
|
||||
// if (data[0].role === "assistant") setIsLoadingAdvisorMessage(false);
|
||||
|
||||
setMessages(prev => {
|
||||
const map = new Map<string, UIMessage>();
|
||||
const map = new Map<string, IChatMessage>();
|
||||
|
||||
prev
|
||||
.filter(m => !m.id.startsWith("sending-message-"))
|
||||
.forEach(m => map.set(m.id, m));
|
||||
|
||||
data.forEach(d =>
|
||||
map.set(d.id, {
|
||||
id: d.id,
|
||||
role: d.role,
|
||||
text: d.text,
|
||||
createdDate: d.createdDate,
|
||||
isRead: d.isRead,
|
||||
suggestions: d.suggestions,
|
||||
isLast: d.isLast,
|
||||
})
|
||||
);
|
||||
data.forEach(d => map.set(d.id, d));
|
||||
|
||||
return Array.from(map.values()).sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime()
|
||||
@ -275,6 +255,7 @@ export const useChatSocket = (
|
||||
session,
|
||||
refillModals,
|
||||
suggestions,
|
||||
unreadMessagesCount,
|
||||
|
||||
send,
|
||||
read,
|
||||
@ -296,6 +277,7 @@ export const useChatSocket = (
|
||||
session,
|
||||
refillModals,
|
||||
suggestions,
|
||||
unreadMessagesCount,
|
||||
isLoadingSelfMessage,
|
||||
isLoadingAdvisorMessage,
|
||||
isAvailableChatting,
|
||||
|
||||
@ -1,20 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { createContext, ReactNode, useContext } from "react";
|
||||
|
||||
import type { IChatMessage } from "@/entities/chats/types";
|
||||
import { useChatSocket } from "@/hooks/chats/useChatSocket";
|
||||
|
||||
interface ChatContextValue extends ReturnType<typeof useChatSocket> {
|
||||
messagesWrapperRef: React.RefObject<HTMLDivElement | null>;
|
||||
scrollToBottom: (behavior?: ScrollBehavior) => void;
|
||||
}
|
||||
type ChatContextValue = ReturnType<typeof useChatSocket>;
|
||||
|
||||
const ChatContext = createContext<ChatContextValue | null>(null);
|
||||
|
||||
@ -44,22 +35,5 @@ export function ChatProvider({
|
||||
initialTotal,
|
||||
});
|
||||
|
||||
const messagesWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
||||
if (messagesWrapperRef.current) {
|
||||
messagesWrapperRef.current.scrollTo({
|
||||
top: messagesWrapperRef.current.scrollHeight,
|
||||
behavior,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ChatContext.Provider
|
||||
value={{ ...value, messagesWrapperRef, scrollToBottom }}
|
||||
>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
);
|
||||
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user