From ccffd32511bd5a79f83d3ddf6469692abc8bde1c Mon Sep 17 00:00:00 2001 From: gofnnp Date: Tue, 22 Jul 2025 23:17:28 +0400 Subject: [PATCH] AW-496-connect-chats edits --- .../(additional-purchases)/layout.module.scss | 2 +- src/app/[locale]/(chat)/chat/page.module.scss | 2 +- src/app/[locale]/(core)/layout.module.scss | 2 +- src/app/[locale]/(payment)/layout.module.scss | 2 +- src/app/[locale]/layout.module.scss | 1 + .../ChatMessagesWrapper.module.scss | 1 + .../ChatMessagesWrapper.tsx | 34 ++- .../MessageInputWrapper.module.scss | 3 +- .../MessageInputWrapper.tsx | 13 +- .../chat/Suggestions/Suggestions.module.scss | 22 ++ .../domains/chat/Suggestions/Suggestions.tsx | 47 ++++ src/components/domains/chat/index.ts | 1 + src/hooks/chats/useChatSocket.ts | 24 +- src/services/socket/events.ts | 16 +- src/services/socket/index.ts | 7 +- src/shared/api/httpClient.ts | 65 +++-- src/shared/utils/logger.ts | 231 +++++++++++------- src/stores/app-ui-store.ts | 1 - src/stores/chat-store.ts | 27 +- 19 files changed, 366 insertions(+), 135 deletions(-) create mode 100644 src/components/domains/chat/Suggestions/Suggestions.module.scss create mode 100644 src/components/domains/chat/Suggestions/Suggestions.tsx diff --git a/src/app/[locale]/(additional-purchases)/layout.module.scss b/src/app/[locale]/(additional-purchases)/layout.module.scss index 9f967e9..d44184e 100644 --- a/src/app/[locale]/(additional-purchases)/layout.module.scss +++ b/src/app/[locale]/(additional-purchases)/layout.module.scss @@ -1,7 +1,7 @@ .layout { position: relative; padding: 24px; - padding-bottom: 120px; + padding-bottom: 220px; min-height: 100dvh; height: fit-content; } diff --git a/src/app/[locale]/(chat)/chat/page.module.scss b/src/app/[locale]/(chat)/chat/page.module.scss index 1b1592a..d85fd4b 100644 --- a/src/app/[locale]/(chat)/chat/page.module.scss +++ b/src/app/[locale]/(chat)/chat/page.module.scss @@ -1,5 +1,5 @@ .container { - padding: 38px 16px 120px; + padding: 38px 16px 220px; } .categories { diff --git a/src/app/[locale]/(core)/layout.module.scss b/src/app/[locale]/(core)/layout.module.scss index 3577e50..06faeb5 100644 --- a/src/app/[locale]/(core)/layout.module.scss +++ b/src/app/[locale]/(core)/layout.module.scss @@ -1,6 +1,6 @@ .main { padding: 16px; - padding-bottom: 120px; + padding-bottom: 220px; } .navBar { diff --git a/src/app/[locale]/(payment)/layout.module.scss b/src/app/[locale]/(payment)/layout.module.scss index 3577e50..06faeb5 100644 --- a/src/app/[locale]/(payment)/layout.module.scss +++ b/src/app/[locale]/(payment)/layout.module.scss @@ -1,6 +1,6 @@ .main { padding: 16px; - padding-bottom: 120px; + padding-bottom: 220px; } .navBar { diff --git a/src/app/[locale]/layout.module.scss b/src/app/[locale]/layout.module.scss index 9061ae0..9c8bb1b 100644 --- a/src/app/[locale]/layout.module.scss +++ b/src/app/[locale]/layout.module.scss @@ -2,4 +2,5 @@ max-width: 560px; margin: 0 auto; position: relative; + min-height: 100dvh; } diff --git a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss index a2bfcf2..17d0054 100644 --- a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss +++ b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss @@ -2,6 +2,7 @@ flex: 1 1 0%; overflow-y: auto; scroll-behavior: smooth; + transition: padding-bottom 0.3s ease-in-out; } .loaderTop { diff --git a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx index de552f3..46cfbcb 100644 --- a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx +++ b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react"; import { 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 } from ".."; @@ -21,6 +22,8 @@ export default function ChatMessagesWrapper() { scrollToBottom, } = useChat(); + const { suggestionsHeight, _hasHydrated } = useChatStore(state => state); + const isInitialScrollDone = useRef(false); const handleScroll = useCallback(() => { @@ -45,20 +48,47 @@ export default function ChatMessagesWrapper() { }, [socketMessages]); useEffect(() => { - if (socketMessages.length > 0 && !isInitialScrollDone.current) { + if ( + socketMessages.length > 0 && + !isInitialScrollDone.current && + _hasHydrated + ) { scrollToBottom(); const timeout = setTimeout(() => { isInitialScrollDone.current = true; }, 1000); return () => clearTimeout(timeout); } - }, [socketMessages.length, scrollToBottom]); + }, [socketMessages.length, scrollToBottom, _hasHydrated]); + + // useEffect(() => { + // if (suggestionsHeight) { + // const timeout = setTimeout(() => { + // scrollToBottom(); + // console.log("scrollToBottom 2"); + // }, 0); + // return () => clearTimeout(timeout); + // } + // }, [suggestionsHeight, scrollToBottom]); + + useEffect(() => { + if (messagesWrapperRef.current?.style.paddingBottom) { + scrollToBottom(); + } + }, [ + scrollToBottom, + messagesWrapperRef.current?.style.paddingBottom, + messagesWrapperRef, + ]); return (
{isLoadingOlder && hasMoreOlderMessages && (
diff --git a/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.module.scss b/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.module.scss index 8aa4d1a..abb98de 100644 --- a/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.module.scss +++ b/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.module.scss @@ -1,3 +1,4 @@ -.inputWrapper { +.container { flex-shrink: 0; + position: relative; } diff --git a/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx b/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx index 3036f43..ac87bee 100644 --- a/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx +++ b/src/components/domains/chat/MessageInputWrapper/MessageInputWrapper.tsx @@ -2,7 +2,7 @@ import { useChat } from "@/providers/chat-provider"; -import { MessageInput } from ".."; +import { MessageInput, Suggestions } from ".."; import styles from "./MessageInputWrapper.module.scss"; @@ -10,8 +10,15 @@ export default function MessageInputWrapper() { const { send } = useChat(); return ( -
- +
+ { + send(suggestion); + }} + /> +
+ +
); } diff --git a/src/components/domains/chat/Suggestions/Suggestions.module.scss b/src/components/domains/chat/Suggestions/Suggestions.module.scss new file mode 100644 index 0000000..1e862b1 --- /dev/null +++ b/src/components/domains/chat/Suggestions/Suggestions.module.scss @@ -0,0 +1,22 @@ +.container { + position: absolute; + bottom: 100%; + display: flex; + flex-wrap: wrap; + gap: 12px; + padding: 12px 16px; + + & > .suggestion { + width: fit-content; + padding: 8px 16px; + border-radius: 9999px; + border: 2px solid rgba(229, 231, 235, 1); + background: var(--background); + cursor: pointer; + + & > .suggestionText { + color: #1f2937; + font-size: 15px; + } + } +} diff --git a/src/components/domains/chat/Suggestions/Suggestions.tsx b/src/components/domains/chat/Suggestions/Suggestions.tsx new file mode 100644 index 0000000..1ca884e --- /dev/null +++ b/src/components/domains/chat/Suggestions/Suggestions.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +import { Typography } from "@/components/ui"; +import { useChatStore } from "@/providers/chat-store-provider"; + +import styles from "./Suggestions.module.scss"; + +interface SuggestionsProps { + onSuggestionClick: (suggestion: string) => void; +} + +export default function Suggestions({ onSuggestionClick }: SuggestionsProps) { + const { suggestions, setSuggestionsHeight } = useChatStore(state => state); + const suggestionsRef = useRef(null); + + useEffect(() => { + setSuggestionsHeight(suggestionsRef.current?.clientHeight ?? 0); + }, [setSuggestionsHeight, suggestions]); + + return ( + <> + {!!suggestions?.length && ( +
+ {suggestions?.map((suggestion, index) => ( +
{ + onSuggestionClick(suggestion); + }} + > + + {suggestion} + +
+ ))} +
+ )} + + ); +} diff --git a/src/components/domains/chat/index.ts b/src/components/domains/chat/index.ts index e27d64c..81bece5 100644 --- a/src/components/domains/chat/index.ts +++ b/src/components/domains/chat/index.ts @@ -35,4 +35,5 @@ export { default as NewMessagesWrapper, NewMessagesWrapperSkeleton, } from "./NewMessagesWrapper/NewMessagesWrapper"; +export { default as Suggestions } from "./Suggestions/Suggestions"; export { default as ViewAll, type ViewAllProps } from "./ViewAll/ViewAll"; diff --git a/src/hooks/chats/useChatSocket.ts b/src/hooks/chats/useChatSocket.ts index 94e3aaf..6d35e2f 100644 --- a/src/hooks/chats/useChatSocket.ts +++ b/src/hooks/chats/useChatSocket.ts @@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { fetchChatMessages } from "@/entities/chats/actions"; import type { IChatMessage } from "@/entities/chats/types"; import { useSocketEvent } from "@/hooks/socket/useSocketEvent"; +import { useChatStore } from "@/providers/chat-store-provider"; import { ESocketStatus, useSocketEmit, @@ -13,7 +14,6 @@ import { } from "@/services/socket"; import type { ICurrentBalance, - IMessage, IRefillModals, ISessionStarted, } from "@/services/socket/events"; @@ -21,8 +21,8 @@ import type { const PAGE_LIMIT = 50; type UIMessage = Pick< - IMessage, - "id" | "role" | "text" | "createdDate" | "isRead" + IChatMessage, + "id" | "role" | "text" | "createdDate" | "isRead" | "suggestions" >; interface UseChatSocketOptions { @@ -45,6 +45,7 @@ export const useChatSocket = ( text: m.text, createdDate: m.createdDate, isRead: m.isRead, + suggestions: m.suggestions, }); const [messages, setMessages] = useState(() => @@ -61,6 +62,9 @@ export const useChatSocket = ( // const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false); const [isSessionExpired, setIsSessionExpired] = useState(false); const [refillModals, setRefillModals] = useState(null); + const { suggestions, setSuggestions } = useChatStore(state => state); + const [isSuggestionsInitialized, setIsSuggestionsInitialized] = + useState(false); const isLoadingAdvisorMessage = useMemo(() => { return messages.length > 0 && messages[0].role !== "assistant"; @@ -162,6 +166,8 @@ export const useChatSocket = ( if (data[0].role === "user") setIsLoadingSelfMessage(false); // if (data[0].role === "assistant") setIsLoadingAdvisorMessage(false); + setSuggestions(data[0].suggestions); + setMessages(prev => { const map = new Map(); @@ -223,6 +229,16 @@ export const useChatSocket = ( }; }, [socket, status, joinChat, leaveChat, fetchBalance]); + useEffect(() => { + if (!isSuggestionsInitialized) { + setSuggestions([]); + } + if (messages[0]?.suggestions && !isSuggestionsInitialized) { + setSuggestions(messages[0].suggestions); + setIsSuggestionsInitialized(true); + } + }, [messages, setSuggestions, isSuggestionsInitialized]); + useEffect(() => { if (session && status === ESocketStatus.CONNECTED) { startBalancePolling(); @@ -261,6 +277,7 @@ export const useChatSocket = ( balance, session, refillModals, + suggestions, send, read, @@ -281,6 +298,7 @@ export const useChatSocket = ( balance, session, refillModals, + suggestions, isLoadingSelfMessage, isLoadingAdvisorMessage, isAvailableChatting, diff --git a/src/services/socket/events.ts b/src/services/socket/events.ts index cce2f31..1fc5f2e 100644 --- a/src/services/socket/events.ts +++ b/src/services/socket/events.ts @@ -1,18 +1,6 @@ -import { IGetChatsListResponse } from "@/entities/chats/types"; +import { IChatMessage, IGetChatsListResponse } from "@/entities/chats/types"; import { Currency } from "@/types"; -export interface IMessage { - id: string; - role: string; - text?: string; - userId: string; - assistantId: string; - threadId: string; - chatId: string; - createdDate: string; - isRead: boolean; -} - export interface ICurrentBalance { chatId: string; cost: number; @@ -79,7 +67,7 @@ export interface ServerToClientEventsBaseData { export interface ServerToClientEvents { chat_joined: (data: ServerToClientEventsBaseData) => void; chat_left: (data: ServerToClientEventsBaseData) => void; - receive_message: (data: IMessage[]) => void; + receive_message: (data: IChatMessage[]) => void; current_balance: ( data: ServerToClientEventsBaseData ) => void; diff --git a/src/services/socket/index.ts b/src/services/socket/index.ts index d01c46c..b5035a0 100644 --- a/src/services/socket/index.ts +++ b/src/services/socket/index.ts @@ -95,7 +95,12 @@ export const useSocketStore = createStore()( // Universal incoming event logger socket.onAny((event: string, ...args: unknown[]) => { // Skip logging built-in socket.io events to avoid noise - const systemEvents = ['connect', 'disconnect', 'connect_error', 'reconnect']; + const systemEvents = [ + "connect", + "disconnect", + "connect_error", + "reconnect", + ]; if (!systemEvents.includes(event)) { devLogger.socketIncoming(event, ...args); } diff --git a/src/shared/api/httpClient.ts b/src/shared/api/httpClient.ts index 5b04334..ba91e84 100644 --- a/src/shared/api/httpClient.ts +++ b/src/shared/api/httpClient.ts @@ -61,19 +61,19 @@ class HttpClient { const startTime = Date.now(); // Log API request (both client and server with ENV control) - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { // Client-side logging devLogger.apiRequest(fullUrl, method, body); } else { // Server-side logging (requires ENV variable) - if (typeof devLogger.serverApiRequest === 'function') { + if (typeof devLogger.serverApiRequest === "function") { devLogger.serverApiRequest(fullUrl, method, body); } else { // Fallback server logging - if (process.env.DEV_LOGGER_SERVER_ENABLED === 'true') { + if (process.env.DEV_LOGGER_SERVER_ENABLED === "true") { console.group(`\nπŸš€ [SERVER] API REQUEST: ${method} ${fullUrl}`); if (body !== undefined) { - console.log('πŸ“¦ Request Body:', JSON.stringify(body, null, 2)); + console.log("πŸ“¦ Request Body:", JSON.stringify(body, null, 2)); } console.groupEnd(); } @@ -98,20 +98,28 @@ class HttpClient { if (!res.ok) { // Log API error response (both client and server) - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { devLogger.apiResponse(fullUrl, method, res.status, payload, duration); } else { - if (typeof devLogger.serverApiResponse === 'function') { - devLogger.serverApiResponse(fullUrl, method, res.status, payload, duration); + if (typeof devLogger.serverApiResponse === "function") { + devLogger.serverApiResponse( + fullUrl, + method, + res.status, + payload, + duration + ); } else { // Fallback server logging - if (process.env.DEV_LOGGER_SERVER_ENABLED === 'true') { - const emoji = res.status >= 200 && res.status < 300 ? 'βœ…' : '❌'; - console.group(`\n${emoji} [SERVER] API ERROR: ${method} ${fullUrl}`); + if (process.env.DEV_LOGGER_SERVER_ENABLED === "true") { + const emoji = res.status >= 200 && res.status < 300 ? "βœ…" : "❌"; + console.group( + `\n${emoji} [SERVER] API ERROR: ${method} ${fullUrl}` + ); console.log(`πŸ“Š Status: ${res.status}`); console.log(`⏱️ Duration: ${duration}ms`); if (payload !== undefined) { - console.log('πŸ“¦ Error Response:', payload); + console.log("πŸ“¦ Error Response:", payload); } console.groupEnd(); } @@ -128,24 +136,37 @@ class HttpClient { const validatedData = schema ? schema.parse(data) : data; // Log successful API response (both client and server) - if (typeof window !== 'undefined') { - devLogger.apiResponse(fullUrl, method, res.status, validatedData, duration); + if (typeof window !== "undefined") { + devLogger.apiResponse( + fullUrl, + method, + res.status, + validatedData, + duration + ); } else { - if (typeof devLogger.serverApiResponse === 'function') { - devLogger.serverApiResponse(fullUrl, method, res.status, validatedData, duration); + if (typeof devLogger.serverApiResponse === "function") { + devLogger.serverApiResponse( + fullUrl, + method, + res.status, + validatedData, + duration + ); } else { // Fallback server logging - if (process.env.DEV_LOGGER_SERVER_ENABLED === 'true') { + if (process.env.DEV_LOGGER_SERVER_ENABLED === "true") { console.group(`\nβœ… [SERVER] API SUCCESS: ${method} ${fullUrl}`); console.log(`πŸ“Š Status: ${res.status}`); console.log(`⏱️ Duration: ${duration}ms`); if (validatedData !== undefined) { - const responsePreview = typeof validatedData === 'object' && validatedData !== null - ? Array.isArray(validatedData) - ? `Array[${validatedData.length}]` - : `Object{${Object.keys(validatedData).slice(0, 5).join(', ')}${Object.keys(validatedData).length > 5 ? '...' : ''}}` - : validatedData; - console.log('πŸ“¦ Response Preview:', responsePreview); + const responsePreview = + typeof validatedData === "object" && validatedData !== null + ? Array.isArray(validatedData) + ? `Array[${validatedData.length}]` + : `Object{${Object.keys(validatedData).slice(0, 5).join(", ")}${Object.keys(validatedData).length > 5 ? "..." : ""}}` + : validatedData; + console.log("πŸ“¦ Response Preview:", responsePreview); } console.groupEnd(); } diff --git a/src/shared/utils/logger.ts b/src/shared/utils/logger.ts index 0b60cf9..9b9e314 100644 --- a/src/shared/utils/logger.ts +++ b/src/shared/utils/logger.ts @@ -3,17 +3,17 @@ /* eslint-disable no-console */ export enum LogType { - API = 'API', - SOCKET = 'SOCKET', - ERROR = 'ERROR', - INFO = 'INFO' + API = "API", + SOCKET = "SOCKET", + ERROR = "ERROR", + INFO = "INFO", } export enum LogDirection { - REQUEST = 'REQUEST', - RESPONSE = 'RESPONSE', - INCOMING = 'INCOMING', - OUTGOING = 'OUTGOING' + REQUEST = "REQUEST", + RESPONSE = "RESPONSE", + INCOMING = "INCOMING", + OUTGOING = "OUTGOING", } interface LogEntry { @@ -36,20 +36,21 @@ class DevLogger { constructor() { // Check ENV variables first - if (typeof window !== 'undefined') { - this.envEnabled = process.env.NEXT_PUBLIC_DEV_LOGGER_ENABLED !== 'false'; + if (typeof window !== "undefined") { + this.envEnabled = process.env.NEXT_PUBLIC_DEV_LOGGER_ENABLED !== "false"; } else { // Server side - check server env - this.serverLoggingEnabled = process.env.DEV_LOGGER_SERVER_ENABLED === 'true'; - this.envEnabled = process.env.DEV_LOGGER_ENABLED !== 'false'; + this.serverLoggingEnabled = + process.env.DEV_LOGGER_SERVER_ENABLED === "true"; + this.envEnabled = process.env.DEV_LOGGER_ENABLED !== "false"; } // Check localStorage for logging preferences (client-side only) - if (typeof window !== 'undefined') { - const stored = localStorage.getItem('dev-logger-enabled'); + if (typeof window !== "undefined") { + const stored = localStorage.getItem("dev-logger-enabled"); this.enabled = stored ? JSON.parse(stored) : this.envEnabled; - const storedTypes = localStorage.getItem('dev-logger-types'); + const storedTypes = localStorage.getItem("dev-logger-types"); if (storedTypes) { this.enabledTypes = new Set(JSON.parse(storedTypes)); } @@ -65,81 +66,114 @@ class DevLogger { private shouldLogServer(type: LogType): boolean { // Server logging requires explicit ENV enable - return this.serverLoggingEnabled && this.envEnabled && this.enabled && this.enabledTypes.has(type); + return ( + this.serverLoggingEnabled && + this.envEnabled && + this.enabled && + this.enabledTypes.has(type) + ); } - private getLogStyle(type: LogType, direction?: LogDirection): { emoji: string; color: string; bgColor?: string } { + private getLogStyle( + type: LogType, + direction?: LogDirection + ): { emoji: string; color: string; bgColor?: string } { const styles: Record = { [LogType.API]: { - [LogDirection.REQUEST]: { emoji: 'πŸš€', color: '#3b82f6', bgColor: '#eff6ff' }, - [LogDirection.RESPONSE]: { emoji: 'πŸ“¨', color: '#10b981', bgColor: '#f0fdf4' }, + [LogDirection.REQUEST]: { + emoji: "πŸš€", + color: "#3b82f6", + bgColor: "#eff6ff", + }, + [LogDirection.RESPONSE]: { + emoji: "πŸ“¨", + color: "#10b981", + bgColor: "#f0fdf4", + }, }, [LogType.SOCKET]: { - [LogDirection.OUTGOING]: { emoji: '🟒', color: '#16a34a' }, - [LogDirection.INCOMING]: { emoji: 'πŸ”΅', color: '#2563eb' }, + [LogDirection.OUTGOING]: { emoji: "🟒", color: "#16a34a" }, + [LogDirection.INCOMING]: { emoji: "πŸ”΅", color: "#2563eb" }, }, - [LogType.ERROR]: { emoji: '❌', color: '#ef4444' }, - [LogType.INFO]: { emoji: 'ℹ️', color: '#6366f1' } + [LogType.ERROR]: { emoji: "❌", color: "#ef4444" }, + [LogType.INFO]: { emoji: "ℹ️", color: "#6366f1" }, }; const typeStyles = styles[type]; - if (direction && typeof typeStyles === 'object' && direction in typeStyles) { + if ( + direction && + typeof typeStyles === "object" && + direction in typeStyles + ) { return typeStyles[direction]; } - return typeof typeStyles === 'object' ? { emoji: 'πŸ“', color: '#6b7280' } : typeStyles; + return typeof typeStyles === "object" + ? { emoji: "πŸ“", color: "#6b7280" } + : typeStyles; } private formatTime(date: Date): string { - return date.toLocaleTimeString('en-US', { + return date.toLocaleTimeString("en-US", { hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - fractionalSecondDigits: 3 + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + fractionalSecondDigits: 3, }); } - log(entry: Omit) { + log(entry: Omit) { if (!this.shouldLog(entry.type)) return; const timestamp = new Date(); - const { emoji, color, bgColor } = this.getLogStyle(entry.type, entry.direction); + const { emoji, color, bgColor } = this.getLogStyle( + entry.type, + entry.direction + ); const timeStr = this.formatTime(timestamp); const baseStyle = `color: ${color}; font-weight: bold;`; - const groupStyle = bgColor ? `${baseStyle} background: ${bgColor}; padding: 2px 6px; border-radius: 3px;` : baseStyle; + const groupStyle = bgColor + ? `${baseStyle} background: ${bgColor}; padding: 2px 6px; border-radius: 3px;` + : baseStyle; // Create compact collapsible group - const groupTitle = `${emoji} ${entry.type}${entry.direction ? ` ${entry.direction}` : ''}: ${entry.event}`; + const groupTitle = `${emoji} ${entry.type}${entry.direction ? ` ${entry.direction}` : ""}: ${entry.event}`; // Always use groupCollapsed for cleaner output - console.groupCollapsed( - `%c${groupTitle} [${timeStr}]`, - groupStyle - ); + console.groupCollapsed(`%c${groupTitle} [${timeStr}]`, groupStyle); // Compact one-line summary with key info const summaryParts = []; if (entry.method) summaryParts.push(`${entry.method}`); if (entry.status) { - const statusColor = entry.status >= 200 && entry.status < 300 ? 'βœ…' : '❌'; + const statusColor = + entry.status >= 200 && entry.status < 300 ? "βœ…" : "❌"; summaryParts.push(`${statusColor} ${entry.status}`); } - if (entry.duration !== undefined) summaryParts.push(`⏱️ ${entry.duration}ms`); + if (entry.duration !== undefined) + summaryParts.push(`⏱️ ${entry.duration}ms`); if (summaryParts.length > 0) { - console.log(`%c${summaryParts.join(' β€’ ')}`, 'color: #6b7280; font-size: 11px;'); + console.log( + `%c${summaryParts.join(" β€’ ")}`, + "color: #6b7280; font-size: 11px;" + ); } if (entry.data !== undefined) { // Show preview for objects/arrays, full value for primitives - if (typeof entry.data === 'object' && entry.data !== null) { + if (typeof entry.data === "object" && entry.data !== null) { const preview = Array.isArray(entry.data) ? `Array[${entry.data.length}]` - : `Object{${Object.keys(entry.data).slice(0, 3).join(', ')}${Object.keys(entry.data).length > 3 ? '...' : ''}}`; - console.log(`%cπŸ“¦ Data:`, 'color: #6b7280; font-size: 11px;', preview); + : `Object{${Object.keys(entry.data).slice(0, 3).join(", ")}${Object.keys(entry.data).length > 3 ? "..." : ""}}`; + console.log(`%cπŸ“¦ Data:`, "color: #6b7280; font-size: 11px;", preview); console.log(entry.data); } else { - console.log(`%cπŸ“¦ Data:`, 'color: #6b7280; font-size: 11px;', entry.data); + console.log( + `%cπŸ“¦ Data:`, + "color: #6b7280; font-size: 11px;", + entry.data + ); } } @@ -151,23 +185,29 @@ class DevLogger { this.log({ type: LogType.API, direction: LogDirection.REQUEST, - event: `${method.toUpperCase()} ${url.split('?')[0]}`, + event: `${method.toUpperCase()} ${url.split("?")[0]}`, url, method, - data + data, }); } - apiResponse(url: string, method: string, status: number, data?: unknown, duration?: number) { + apiResponse( + url: string, + method: string, + status: number, + data?: unknown, + duration?: number + ) { this.log({ type: LogType.API, direction: LogDirection.RESPONSE, - event: `${method.toUpperCase()} ${url.split('?')[0]}`, + event: `${method.toUpperCase()} ${url.split("?")[0]}`, url, method, status, data, - duration + duration, }); } @@ -177,7 +217,7 @@ class DevLogger { type: LogType.SOCKET, direction: LogDirection.OUTGOING, event, - data + data, }); } @@ -186,7 +226,7 @@ class DevLogger { type: LogType.SOCKET, direction: LogDirection.INCOMING, event, - data + data, }); } @@ -194,23 +234,23 @@ class DevLogger { socketConnected() { console.log( `%cβœ… SOCKET CONNECTED`, - 'color: #10b981; font-weight: bold; background: #f0fdf4; padding: 2px 6px; border-radius: 3px;' + "color: #10b981; font-weight: bold; background: #f0fdf4; padding: 2px 6px; border-radius: 3px;" ); } socketDisconnected(reason?: string) { console.log( `%c❌ SOCKET DISCONNECTED`, - 'color: #ef4444; font-weight: bold; background: #fef2f2; padding: 2px 6px; border-radius: 3px;', - reason || '' + "color: #ef4444; font-weight: bold; background: #fef2f2; padding: 2px 6px; border-radius: 3px;", + reason || "" ); } socketError(error?: unknown) { console.log( `%c⚠️ SOCKET ERROR`, - 'color: #f59e0b; font-weight: bold; background: #fffbeb; padding: 2px 6px; border-radius: 3px;', - error || '' + "color: #f59e0b; font-weight: bold; background: #fffbeb; padding: 2px 6px; border-radius: 3px;", + error || "" ); } @@ -220,30 +260,39 @@ class DevLogger { console.group(`\nπŸš€ [SERVER] API REQUEST: ${method} ${url}`); if (body !== undefined) { - console.log('πŸ“¦ Request Body:', JSON.stringify(body, null, 2)); + console.log("πŸ“¦ Request Body:", JSON.stringify(body, null, 2)); } console.groupEnd(); } - serverApiResponse(url: string, method: string, status: number, data?: unknown, duration?: number) { + serverApiResponse( + url: string, + method: string, + status: number, + data?: unknown, + duration?: number + ) { if (!this.shouldLogServer(LogType.API)) return; - const emoji = status >= 200 && status < 300 ? 'βœ…' : '❌'; - console.group(`\n${emoji} [SERVER] API ${status >= 200 && status < 300 ? 'SUCCESS' : 'ERROR'}: ${method} ${url}`); + const emoji = status >= 200 && status < 300 ? "βœ…" : "❌"; + console.group( + `\n${emoji} [SERVER] API ${status >= 200 && status < 300 ? "SUCCESS" : "ERROR"}: ${method} ${url}` + ); console.log(`πŸ“Š Status: ${status}`); if (duration !== undefined) { console.log(`⏱️ Duration: ${duration}ms`); } if (data !== undefined) { // Limit response data display to avoid overwhelming logs - const responsePreview = typeof data === 'object' && data !== null - ? Array.isArray(data) - ? `Array[${data.length}]` - : `Object{${Object.keys(data).slice(0, 5).join(', ')}${Object.keys(data).length > 5 ? '...' : ''}}` - : data; - console.log('πŸ“¦ Response Preview:', responsePreview); + const responsePreview = + typeof data === "object" && data !== null + ? Array.isArray(data) + ? `Array[${data.length}]` + : `Object{${Object.keys(data).slice(0, 5).join(", ")}${Object.keys(data).length > 5 ? "..." : ""}}` + : data; + console.log("πŸ“¦ Response Preview:", responsePreview); // Full response data (collapsed) - console.groupCollapsed('πŸ“„ Full Response Data:'); + console.groupCollapsed("πŸ“„ Full Response Data:"); console.log(data); console.groupEnd(); } @@ -253,43 +302,61 @@ class DevLogger { // Control methods enable() { this.enabled = true; - if (typeof window !== 'undefined') { - localStorage.setItem('dev-logger-enabled', 'true'); + if (typeof window !== "undefined") { + localStorage.setItem("dev-logger-enabled", "true"); } - console.log('%cπŸ“ Dev Logger ENABLED', 'color: #10b981; font-weight: bold;'); + console.log( + "%cπŸ“ Dev Logger ENABLED", + "color: #10b981; font-weight: bold;" + ); } disable() { this.enabled = false; - if (typeof window !== 'undefined') { - localStorage.setItem('dev-logger-enabled', 'false'); + if (typeof window !== "undefined") { + localStorage.setItem("dev-logger-enabled", "false"); } - console.log('%cπŸ“ Dev Logger DISABLED', 'color: #ef4444; font-weight: bold;'); + console.log( + "%cπŸ“ Dev Logger DISABLED", + "color: #ef4444; font-weight: bold;" + ); } enableType(type: LogType) { this.enabledTypes.add(type); this.saveEnabledTypes(); - console.log(`%cπŸ“ ${type} logging ENABLED`, 'color: #10b981; font-weight: bold;'); + console.log( + `%cπŸ“ ${type} logging ENABLED`, + "color: #10b981; font-weight: bold;" + ); } disableType(type: LogType) { this.enabledTypes.delete(type); this.saveEnabledTypes(); - console.log(`%cπŸ“ ${type} logging DISABLED`, 'color: #ef4444; font-weight: bold;'); + console.log( + `%cπŸ“ ${type} logging DISABLED`, + "color: #ef4444; font-weight: bold;" + ); } private saveEnabledTypes() { - if (typeof window !== 'undefined') { - localStorage.setItem('dev-logger-types', JSON.stringify(Array.from(this.enabledTypes))); + if (typeof window !== "undefined") { + localStorage.setItem( + "dev-logger-types", + JSON.stringify(Array.from(this.enabledTypes)) + ); } } // Helper method to show current settings status() { - console.group('%cπŸ”§ Dev Logger Status', 'color: #6366f1; font-weight: bold;'); - console.log('Enabled:', this.enabled); - console.log('Active Types:', Array.from(this.enabledTypes)); + console.group( + "%cπŸ”§ Dev Logger Status", + "color: #6366f1; font-weight: bold;" + ); + console.log("Enabled:", this.enabled); + console.log("Active Types:", Array.from(this.enabledTypes)); console.groupEnd(); } } @@ -298,7 +365,7 @@ class DevLogger { export const devLogger = new DevLogger(); // Make it available globally for easy console access -if (typeof window !== 'undefined') { +if (typeof window !== "undefined") { (window as any).devLogger = devLogger; } diff --git a/src/stores/app-ui-store.ts b/src/stores/app-ui-store.ts index a3a63ce..bee01ba 100644 --- a/src/stores/app-ui-store.ts +++ b/src/stores/app-ui-store.ts @@ -91,7 +91,6 @@ export const createAppUiStore = (initState: AppUiState = initialState) => { { name: "app-ui-storage", onRehydrateStorage: () => state => { - // ВызываСтся послС Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· localStorage if (state) { state.setHasHydrated(true); } diff --git a/src/stores/chat-store.ts b/src/stores/chat-store.ts index b1382e3..2d4b9cb 100644 --- a/src/stores/chat-store.ts +++ b/src/stores/chat-store.ts @@ -3,17 +3,23 @@ import { createStore } from "zustand"; import { persist } from "zustand/middleware"; -import { IChat } from "@/entities/chats/types"; +import { IChat, IChatMessage } from "@/entities/chats/types"; interface ChatState { currentChat: IChat | null; isAutoTopUp: boolean; + suggestions: IChatMessage["suggestions"]; + suggestionsHeight: number; + _hasHydrated: boolean; } export type ChatActions = { setCurrentChat: (chat: IChat) => void; setIsAutoTopUp: (isAutoTopUp: boolean) => void; + setSuggestions: (suggestions: IChatMessage["suggestions"]) => void; + setSuggestionsHeight: (height: number) => void; clearChatData: () => void; + setHasHydrated: (hasHydrated: boolean) => void; }; export type ChatStore = ChatState & ChatActions; @@ -21,6 +27,9 @@ export type ChatStore = ChatState & ChatActions; const initialState: ChatState = { currentChat: null, isAutoTopUp: false, + suggestions: [], + suggestionsHeight: 0, + _hasHydrated: false, }; export const createChatStore = (initState: ChatState = initialState) => { @@ -30,9 +39,23 @@ export const createChatStore = (initState: ChatState = initialState) => { ...initState, setCurrentChat: (chat: IChat) => set({ currentChat: chat }), setIsAutoTopUp: (isAutoTopUp: boolean) => set({ isAutoTopUp }), + setSuggestions: (suggestions: IChatMessage["suggestions"]) => + set({ suggestions }), + setSuggestionsHeight: (height: number) => + set({ suggestionsHeight: height }), clearChatData: () => set(initialState), + + setHasHydrated: (hasHydrated: boolean) => + set({ _hasHydrated: hasHydrated }), }), - { name: "chat-storage" } + { + name: "chat-storage", + onRehydrateStorage: () => state => { + if (state) { + state.setHasHydrated(true); + } + }, + } ) ); };