AW-496-connect-chats

edits
This commit is contained in:
gofnnp 2025-07-22 23:17:28 +04:00
parent e424cc7f23
commit ccffd32511
19 changed files with 366 additions and 135 deletions

View File

@ -1,7 +1,7 @@
.layout { .layout {
position: relative; position: relative;
padding: 24px; padding: 24px;
padding-bottom: 120px; padding-bottom: 220px;
min-height: 100dvh; min-height: 100dvh;
height: fit-content; height: fit-content;
} }

View File

@ -1,5 +1,5 @@
.container { .container {
padding: 38px 16px 120px; padding: 38px 16px 220px;
} }
.categories { .categories {

View File

@ -1,6 +1,6 @@
.main { .main {
padding: 16px; padding: 16px;
padding-bottom: 120px; padding-bottom: 220px;
} }
.navBar { .navBar {

View File

@ -1,6 +1,6 @@
.main { .main {
padding: 16px; padding: 16px;
padding-bottom: 120px; padding-bottom: 220px;
} }
.navBar { .navBar {

View File

@ -2,4 +2,5 @@
max-width: 560px; max-width: 560px;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
min-height: 100dvh;
} }

View File

@ -2,6 +2,7 @@
flex: 1 1 0%; flex: 1 1 0%;
overflow-y: auto; overflow-y: auto;
scroll-behavior: smooth; scroll-behavior: smooth;
transition: padding-bottom 0.3s ease-in-out;
} }
.loaderTop { .loaderTop {

View File

@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";
import { Spinner } from "@/components/ui"; import { Spinner } from "@/components/ui";
import { useChat } from "@/providers/chat-provider"; import { useChat } from "@/providers/chat-provider";
import { useChatStore } from "@/providers/chat-store-provider";
import { formatTime } from "@/shared/utils/date"; import { formatTime } from "@/shared/utils/date";
import { ChatMessages } from ".."; import { ChatMessages } from "..";
@ -21,6 +22,8 @@ export default function ChatMessagesWrapper() {
scrollToBottom, scrollToBottom,
} = useChat(); } = useChat();
const { suggestionsHeight, _hasHydrated } = useChatStore(state => state);
const isInitialScrollDone = useRef(false); const isInitialScrollDone = useRef(false);
const handleScroll = useCallback(() => { const handleScroll = useCallback(() => {
@ -45,20 +48,47 @@ export default function ChatMessagesWrapper() {
}, [socketMessages]); }, [socketMessages]);
useEffect(() => { useEffect(() => {
if (socketMessages.length > 0 && !isInitialScrollDone.current) { if (
socketMessages.length > 0 &&
!isInitialScrollDone.current &&
_hasHydrated
) {
scrollToBottom(); scrollToBottom();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
isInitialScrollDone.current = true; isInitialScrollDone.current = true;
}, 1000); }, 1000);
return () => clearTimeout(timeout); 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 ( return (
<div <div
className={styles.messagesWrapper} className={styles.messagesWrapper}
ref={messagesWrapperRef} ref={messagesWrapperRef}
onScroll={handleScroll} onScroll={handleScroll}
style={{
paddingBottom: suggestionsHeight,
}}
> >
{isLoadingOlder && hasMoreOlderMessages && ( {isLoadingOlder && hasMoreOlderMessages && (
<div className={styles.loaderTop}> <div className={styles.loaderTop}>

View File

@ -1,3 +1,4 @@
.inputWrapper { .container {
flex-shrink: 0; flex-shrink: 0;
position: relative;
} }

View File

@ -2,7 +2,7 @@
import { useChat } from "@/providers/chat-provider"; import { useChat } from "@/providers/chat-provider";
import { MessageInput } from ".."; import { MessageInput, Suggestions } from "..";
import styles from "./MessageInputWrapper.module.scss"; import styles from "./MessageInputWrapper.module.scss";
@ -10,8 +10,15 @@ export default function MessageInputWrapper() {
const { send } = useChat(); const { send } = useChat();
return ( return (
<div className={styles.inputWrapper}> <div className={styles.container}>
<MessageInput onSend={send} /> <Suggestions
onSuggestionClick={suggestion => {
send(suggestion);
}}
/>
<div className={styles.inputWrapper}>
<MessageInput onSend={send} />
</div>
</div> </div>
); );
} }

View File

@ -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;
}
}
}

View File

@ -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<HTMLDivElement>(null);
useEffect(() => {
setSuggestionsHeight(suggestionsRef.current?.clientHeight ?? 0);
}, [setSuggestionsHeight, suggestions]);
return (
<>
{!!suggestions?.length && (
<div className={styles.container} ref={suggestionsRef}>
{suggestions?.map((suggestion, index) => (
<div
key={`suggestion-${index}`}
className={styles.suggestion}
onClick={() => {
onSuggestionClick(suggestion);
}}
>
<Typography
as="p"
weight="medium"
className={styles.suggestionText}
>
{suggestion}
</Typography>
</div>
))}
</div>
)}
</>
);
}

View File

@ -35,4 +35,5 @@ export {
default as NewMessagesWrapper, default as NewMessagesWrapper,
NewMessagesWrapperSkeleton, NewMessagesWrapperSkeleton,
} from "./NewMessagesWrapper/NewMessagesWrapper"; } from "./NewMessagesWrapper/NewMessagesWrapper";
export { default as Suggestions } from "./Suggestions/Suggestions";
export { default as ViewAll, type ViewAllProps } from "./ViewAll/ViewAll"; export { default as ViewAll, type ViewAllProps } from "./ViewAll/ViewAll";

View File

@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { fetchChatMessages } from "@/entities/chats/actions"; import { fetchChatMessages } from "@/entities/chats/actions";
import type { IChatMessage } from "@/entities/chats/types"; import type { IChatMessage } from "@/entities/chats/types";
import { useSocketEvent } from "@/hooks/socket/useSocketEvent"; import { useSocketEvent } from "@/hooks/socket/useSocketEvent";
import { useChatStore } from "@/providers/chat-store-provider";
import { import {
ESocketStatus, ESocketStatus,
useSocketEmit, useSocketEmit,
@ -13,7 +14,6 @@ import {
} from "@/services/socket"; } from "@/services/socket";
import type { import type {
ICurrentBalance, ICurrentBalance,
IMessage,
IRefillModals, IRefillModals,
ISessionStarted, ISessionStarted,
} from "@/services/socket/events"; } from "@/services/socket/events";
@ -21,8 +21,8 @@ import type {
const PAGE_LIMIT = 50; const PAGE_LIMIT = 50;
type UIMessage = Pick< type UIMessage = Pick<
IMessage, IChatMessage,
"id" | "role" | "text" | "createdDate" | "isRead" "id" | "role" | "text" | "createdDate" | "isRead" | "suggestions"
>; >;
interface UseChatSocketOptions { interface UseChatSocketOptions {
@ -45,6 +45,7 @@ export const useChatSocket = (
text: m.text, text: m.text,
createdDate: m.createdDate, createdDate: m.createdDate,
isRead: m.isRead, isRead: m.isRead,
suggestions: m.suggestions,
}); });
const [messages, setMessages] = useState<UIMessage[]>(() => const [messages, setMessages] = useState<UIMessage[]>(() =>
@ -61,6 +62,9 @@ export const useChatSocket = (
// const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false); // const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false);
const [isSessionExpired, setIsSessionExpired] = useState(false); const [isSessionExpired, setIsSessionExpired] = useState(false);
const [refillModals, setRefillModals] = useState<IRefillModals | null>(null); const [refillModals, setRefillModals] = useState<IRefillModals | null>(null);
const { suggestions, setSuggestions } = useChatStore(state => state);
const [isSuggestionsInitialized, setIsSuggestionsInitialized] =
useState(false);
const isLoadingAdvisorMessage = useMemo(() => { const isLoadingAdvisorMessage = useMemo(() => {
return messages.length > 0 && messages[0].role !== "assistant"; 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 === "user") setIsLoadingSelfMessage(false);
// if (data[0].role === "assistant") setIsLoadingAdvisorMessage(false); // if (data[0].role === "assistant") setIsLoadingAdvisorMessage(false);
setSuggestions(data[0].suggestions);
setMessages(prev => { setMessages(prev => {
const map = new Map<string, UIMessage>(); const map = new Map<string, UIMessage>();
@ -223,6 +229,16 @@ export const useChatSocket = (
}; };
}, [socket, status, joinChat, leaveChat, fetchBalance]); }, [socket, status, joinChat, leaveChat, fetchBalance]);
useEffect(() => {
if (!isSuggestionsInitialized) {
setSuggestions([]);
}
if (messages[0]?.suggestions && !isSuggestionsInitialized) {
setSuggestions(messages[0].suggestions);
setIsSuggestionsInitialized(true);
}
}, [messages, setSuggestions, isSuggestionsInitialized]);
useEffect(() => { useEffect(() => {
if (session && status === ESocketStatus.CONNECTED) { if (session && status === ESocketStatus.CONNECTED) {
startBalancePolling(); startBalancePolling();
@ -261,6 +277,7 @@ export const useChatSocket = (
balance, balance,
session, session,
refillModals, refillModals,
suggestions,
send, send,
read, read,
@ -281,6 +298,7 @@ export const useChatSocket = (
balance, balance,
session, session,
refillModals, refillModals,
suggestions,
isLoadingSelfMessage, isLoadingSelfMessage,
isLoadingAdvisorMessage, isLoadingAdvisorMessage,
isAvailableChatting, isAvailableChatting,

View File

@ -1,18 +1,6 @@
import { IGetChatsListResponse } from "@/entities/chats/types"; import { IChatMessage, IGetChatsListResponse } from "@/entities/chats/types";
import { Currency } from "@/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 { export interface ICurrentBalance {
chatId: string; chatId: string;
cost: number; cost: number;
@ -79,7 +67,7 @@ export interface ServerToClientEventsBaseData<T> {
export interface ServerToClientEvents { export interface ServerToClientEvents {
chat_joined: (data: ServerToClientEventsBaseData<null>) => void; chat_joined: (data: ServerToClientEventsBaseData<null>) => void;
chat_left: (data: ServerToClientEventsBaseData<boolean>) => void; chat_left: (data: ServerToClientEventsBaseData<boolean>) => void;
receive_message: (data: IMessage[]) => void; receive_message: (data: IChatMessage[]) => void;
current_balance: ( current_balance: (
data: ServerToClientEventsBaseData<ICurrentBalance> data: ServerToClientEventsBaseData<ICurrentBalance>
) => void; ) => void;

View File

@ -95,7 +95,12 @@ export const useSocketStore = createStore<SocketStore>()(
// Universal incoming event logger // Universal incoming event logger
socket.onAny((event: string, ...args: unknown[]) => { socket.onAny((event: string, ...args: unknown[]) => {
// Skip logging built-in socket.io events to avoid noise // 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)) { if (!systemEvents.includes(event)) {
devLogger.socketIncoming(event, ...args); devLogger.socketIncoming(event, ...args);
} }

View File

@ -61,19 +61,19 @@ class HttpClient {
const startTime = Date.now(); const startTime = Date.now();
// Log API request (both client and server with ENV control) // Log API request (both client and server with ENV control)
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
// Client-side logging // Client-side logging
devLogger.apiRequest(fullUrl, method, body); devLogger.apiRequest(fullUrl, method, body);
} else { } else {
// Server-side logging (requires ENV variable) // Server-side logging (requires ENV variable)
if (typeof devLogger.serverApiRequest === 'function') { if (typeof devLogger.serverApiRequest === "function") {
devLogger.serverApiRequest(fullUrl, method, body); devLogger.serverApiRequest(fullUrl, method, body);
} else { } else {
// Fallback server logging // 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}`); console.group(`\n🚀 [SERVER] API REQUEST: ${method} ${fullUrl}`);
if (body !== undefined) { if (body !== undefined) {
console.log('📦 Request Body:', JSON.stringify(body, null, 2)); console.log("📦 Request Body:", JSON.stringify(body, null, 2));
} }
console.groupEnd(); console.groupEnd();
} }
@ -98,20 +98,28 @@ class HttpClient {
if (!res.ok) { if (!res.ok) {
// Log API error response (both client and server) // Log API error response (both client and server)
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
devLogger.apiResponse(fullUrl, method, res.status, payload, duration); devLogger.apiResponse(fullUrl, method, res.status, payload, duration);
} else { } else {
if (typeof devLogger.serverApiResponse === 'function') { if (typeof devLogger.serverApiResponse === "function") {
devLogger.serverApiResponse(fullUrl, method, res.status, payload, duration); devLogger.serverApiResponse(
fullUrl,
method,
res.status,
payload,
duration
);
} else { } else {
// Fallback server logging // Fallback server logging
if (process.env.DEV_LOGGER_SERVER_ENABLED === 'true') { if (process.env.DEV_LOGGER_SERVER_ENABLED === "true") {
const emoji = res.status >= 200 && res.status < 300 ? '✅' : '❌'; const emoji = res.status >= 200 && res.status < 300 ? "✅" : "❌";
console.group(`\n${emoji} [SERVER] API ERROR: ${method} ${fullUrl}`); console.group(
`\n${emoji} [SERVER] API ERROR: ${method} ${fullUrl}`
);
console.log(`📊 Status: ${res.status}`); console.log(`📊 Status: ${res.status}`);
console.log(`⏱️ Duration: ${duration}ms`); console.log(`⏱️ Duration: ${duration}ms`);
if (payload !== undefined) { if (payload !== undefined) {
console.log('📦 Error Response:', payload); console.log("📦 Error Response:", payload);
} }
console.groupEnd(); console.groupEnd();
} }
@ -128,24 +136,37 @@ class HttpClient {
const validatedData = schema ? schema.parse(data) : data; const validatedData = schema ? schema.parse(data) : data;
// Log successful API response (both client and server) // Log successful API response (both client and server)
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
devLogger.apiResponse(fullUrl, method, res.status, validatedData, duration); devLogger.apiResponse(
fullUrl,
method,
res.status,
validatedData,
duration
);
} else { } else {
if (typeof devLogger.serverApiResponse === 'function') { if (typeof devLogger.serverApiResponse === "function") {
devLogger.serverApiResponse(fullUrl, method, res.status, validatedData, duration); devLogger.serverApiResponse(
fullUrl,
method,
res.status,
validatedData,
duration
);
} else { } else {
// Fallback server logging // 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.group(`\n✅ [SERVER] API SUCCESS: ${method} ${fullUrl}`);
console.log(`📊 Status: ${res.status}`); console.log(`📊 Status: ${res.status}`);
console.log(`⏱️ Duration: ${duration}ms`); console.log(`⏱️ Duration: ${duration}ms`);
if (validatedData !== undefined) { if (validatedData !== undefined) {
const responsePreview = typeof validatedData === 'object' && validatedData !== null const responsePreview =
? Array.isArray(validatedData) typeof validatedData === "object" && validatedData !== null
? `Array[${validatedData.length}]` ? Array.isArray(validatedData)
: `Object{${Object.keys(validatedData).slice(0, 5).join(', ')}${Object.keys(validatedData).length > 5 ? '...' : ''}}` ? `Array[${validatedData.length}]`
: validatedData; : `Object{${Object.keys(validatedData).slice(0, 5).join(", ")}${Object.keys(validatedData).length > 5 ? "..." : ""}}`
console.log('📦 Response Preview:', responsePreview); : validatedData;
console.log("📦 Response Preview:", responsePreview);
} }
console.groupEnd(); console.groupEnd();
} }

View File

@ -3,17 +3,17 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
export enum LogType { export enum LogType {
API = 'API', API = "API",
SOCKET = 'SOCKET', SOCKET = "SOCKET",
ERROR = 'ERROR', ERROR = "ERROR",
INFO = 'INFO' INFO = "INFO",
} }
export enum LogDirection { export enum LogDirection {
REQUEST = 'REQUEST', REQUEST = "REQUEST",
RESPONSE = 'RESPONSE', RESPONSE = "RESPONSE",
INCOMING = 'INCOMING', INCOMING = "INCOMING",
OUTGOING = 'OUTGOING' OUTGOING = "OUTGOING",
} }
interface LogEntry { interface LogEntry {
@ -36,20 +36,21 @@ class DevLogger {
constructor() { constructor() {
// Check ENV variables first // Check ENV variables first
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
this.envEnabled = process.env.NEXT_PUBLIC_DEV_LOGGER_ENABLED !== 'false'; this.envEnabled = process.env.NEXT_PUBLIC_DEV_LOGGER_ENABLED !== "false";
} else { } else {
// Server side - check server env // Server side - check server env
this.serverLoggingEnabled = process.env.DEV_LOGGER_SERVER_ENABLED === 'true'; this.serverLoggingEnabled =
this.envEnabled = process.env.DEV_LOGGER_ENABLED !== 'false'; process.env.DEV_LOGGER_SERVER_ENABLED === "true";
this.envEnabled = process.env.DEV_LOGGER_ENABLED !== "false";
} }
// Check localStorage for logging preferences (client-side only) // Check localStorage for logging preferences (client-side only)
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
const stored = localStorage.getItem('dev-logger-enabled'); const stored = localStorage.getItem("dev-logger-enabled");
this.enabled = stored ? JSON.parse(stored) : this.envEnabled; this.enabled = stored ? JSON.parse(stored) : this.envEnabled;
const storedTypes = localStorage.getItem('dev-logger-types'); const storedTypes = localStorage.getItem("dev-logger-types");
if (storedTypes) { if (storedTypes) {
this.enabledTypes = new Set(JSON.parse(storedTypes)); this.enabledTypes = new Set(JSON.parse(storedTypes));
} }
@ -65,81 +66,114 @@ class DevLogger {
private shouldLogServer(type: LogType): boolean { private shouldLogServer(type: LogType): boolean {
// Server logging requires explicit ENV enable // 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, any> = { const styles: Record<LogType, any> = {
[LogType.API]: { [LogType.API]: {
[LogDirection.REQUEST]: { emoji: '🚀', color: '#3b82f6', bgColor: '#eff6ff' }, [LogDirection.REQUEST]: {
[LogDirection.RESPONSE]: { emoji: '📨', color: '#10b981', bgColor: '#f0fdf4' }, emoji: "🚀",
color: "#3b82f6",
bgColor: "#eff6ff",
},
[LogDirection.RESPONSE]: {
emoji: "📨",
color: "#10b981",
bgColor: "#f0fdf4",
},
}, },
[LogType.SOCKET]: { [LogType.SOCKET]: {
[LogDirection.OUTGOING]: { emoji: '🟢', color: '#16a34a' }, [LogDirection.OUTGOING]: { emoji: "🟢", color: "#16a34a" },
[LogDirection.INCOMING]: { emoji: '🔵', color: '#2563eb' }, [LogDirection.INCOMING]: { emoji: "🔵", color: "#2563eb" },
}, },
[LogType.ERROR]: { emoji: '❌', color: '#ef4444' }, [LogType.ERROR]: { emoji: "❌", color: "#ef4444" },
[LogType.INFO]: { emoji: '', color: '#6366f1' } [LogType.INFO]: { emoji: "", color: "#6366f1" },
}; };
const typeStyles = styles[type]; const typeStyles = styles[type];
if (direction && typeof typeStyles === 'object' && direction in typeStyles) { if (
direction &&
typeof typeStyles === "object" &&
direction in typeStyles
) {
return typeStyles[direction]; return typeStyles[direction];
} }
return typeof typeStyles === 'object' ? { emoji: '📝', color: '#6b7280' } : typeStyles; return typeof typeStyles === "object"
? { emoji: "📝", color: "#6b7280" }
: typeStyles;
} }
private formatTime(date: Date): string { private formatTime(date: Date): string {
return date.toLocaleTimeString('en-US', { return date.toLocaleTimeString("en-US", {
hour12: false, hour12: false,
hour: '2-digit', hour: "2-digit",
minute: '2-digit', minute: "2-digit",
second: '2-digit', second: "2-digit",
fractionalSecondDigits: 3 fractionalSecondDigits: 3,
}); });
} }
log(entry: Omit<LogEntry, 'timestamp'>) { log(entry: Omit<LogEntry, "timestamp">) {
if (!this.shouldLog(entry.type)) return; if (!this.shouldLog(entry.type)) return;
const timestamp = new Date(); 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 timeStr = this.formatTime(timestamp);
const baseStyle = `color: ${color}; font-weight: bold;`; 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 // 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 // Always use groupCollapsed for cleaner output
console.groupCollapsed( console.groupCollapsed(`%c${groupTitle} [${timeStr}]`, groupStyle);
`%c${groupTitle} [${timeStr}]`,
groupStyle
);
// Compact one-line summary with key info // Compact one-line summary with key info
const summaryParts = []; const summaryParts = [];
if (entry.method) summaryParts.push(`${entry.method}`); if (entry.method) summaryParts.push(`${entry.method}`);
if (entry.status) { if (entry.status) {
const statusColor = entry.status >= 200 && entry.status < 300 ? '✅' : '❌'; const statusColor =
entry.status >= 200 && entry.status < 300 ? "✅" : "❌";
summaryParts.push(`${statusColor} ${entry.status}`); 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) { 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) { if (entry.data !== undefined) {
// Show preview for objects/arrays, full value for primitives // 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) const preview = Array.isArray(entry.data)
? `Array[${entry.data.length}]` ? `Array[${entry.data.length}]`
: `Object{${Object.keys(entry.data).slice(0, 3).join(', ')}${Object.keys(entry.data).length > 3 ? '...' : ''}}`; : `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(`%c📦 Data:`, "color: #6b7280; font-size: 11px;", preview);
console.log(entry.data); console.log(entry.data);
} else { } 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({ this.log({
type: LogType.API, type: LogType.API,
direction: LogDirection.REQUEST, direction: LogDirection.REQUEST,
event: `${method.toUpperCase()} ${url.split('?')[0]}`, event: `${method.toUpperCase()} ${url.split("?")[0]}`,
url, url,
method, 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({ this.log({
type: LogType.API, type: LogType.API,
direction: LogDirection.RESPONSE, direction: LogDirection.RESPONSE,
event: `${method.toUpperCase()} ${url.split('?')[0]}`, event: `${method.toUpperCase()} ${url.split("?")[0]}`,
url, url,
method, method,
status, status,
data, data,
duration duration,
}); });
} }
@ -177,7 +217,7 @@ class DevLogger {
type: LogType.SOCKET, type: LogType.SOCKET,
direction: LogDirection.OUTGOING, direction: LogDirection.OUTGOING,
event, event,
data data,
}); });
} }
@ -186,7 +226,7 @@ class DevLogger {
type: LogType.SOCKET, type: LogType.SOCKET,
direction: LogDirection.INCOMING, direction: LogDirection.INCOMING,
event, event,
data data,
}); });
} }
@ -194,23 +234,23 @@ class DevLogger {
socketConnected() { socketConnected() {
console.log( console.log(
`%c✅ SOCKET CONNECTED`, `%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) { socketDisconnected(reason?: string) {
console.log( console.log(
`%c❌ SOCKET DISCONNECTED`, `%c❌ SOCKET DISCONNECTED`,
'color: #ef4444; font-weight: bold; background: #fef2f2; padding: 2px 6px; border-radius: 3px;', "color: #ef4444; font-weight: bold; background: #fef2f2; padding: 2px 6px; border-radius: 3px;",
reason || '' reason || ""
); );
} }
socketError(error?: unknown) { socketError(error?: unknown) {
console.log( console.log(
`%c⚠ SOCKET ERROR`, `%c⚠ SOCKET ERROR`,
'color: #f59e0b; font-weight: bold; background: #fffbeb; padding: 2px 6px; border-radius: 3px;', "color: #f59e0b; font-weight: bold; background: #fffbeb; padding: 2px 6px; border-radius: 3px;",
error || '' error || ""
); );
} }
@ -220,30 +260,39 @@ class DevLogger {
console.group(`\n🚀 [SERVER] API REQUEST: ${method} ${url}`); console.group(`\n🚀 [SERVER] API REQUEST: ${method} ${url}`);
if (body !== undefined) { if (body !== undefined) {
console.log('📦 Request Body:', JSON.stringify(body, null, 2)); console.log("📦 Request Body:", JSON.stringify(body, null, 2));
} }
console.groupEnd(); 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; if (!this.shouldLogServer(LogType.API)) return;
const emoji = status >= 200 && status < 300 ? '✅' : '❌'; const emoji = status >= 200 && status < 300 ? "✅" : "❌";
console.group(`\n${emoji} [SERVER] API ${status >= 200 && status < 300 ? 'SUCCESS' : 'ERROR'}: ${method} ${url}`); console.group(
`\n${emoji} [SERVER] API ${status >= 200 && status < 300 ? "SUCCESS" : "ERROR"}: ${method} ${url}`
);
console.log(`📊 Status: ${status}`); console.log(`📊 Status: ${status}`);
if (duration !== undefined) { if (duration !== undefined) {
console.log(`⏱️ Duration: ${duration}ms`); console.log(`⏱️ Duration: ${duration}ms`);
} }
if (data !== undefined) { if (data !== undefined) {
// Limit response data display to avoid overwhelming logs // Limit response data display to avoid overwhelming logs
const responsePreview = typeof data === 'object' && data !== null const responsePreview =
? Array.isArray(data) typeof data === "object" && data !== null
? `Array[${data.length}]` ? Array.isArray(data)
: `Object{${Object.keys(data).slice(0, 5).join(', ')}${Object.keys(data).length > 5 ? '...' : ''}}` ? `Array[${data.length}]`
: data; : `Object{${Object.keys(data).slice(0, 5).join(", ")}${Object.keys(data).length > 5 ? "..." : ""}}`
console.log('📦 Response Preview:', responsePreview); : data;
console.log("📦 Response Preview:", responsePreview);
// Full response data (collapsed) // Full response data (collapsed)
console.groupCollapsed('📄 Full Response Data:'); console.groupCollapsed("📄 Full Response Data:");
console.log(data); console.log(data);
console.groupEnd(); console.groupEnd();
} }
@ -253,43 +302,61 @@ class DevLogger {
// Control methods // Control methods
enable() { enable() {
this.enabled = true; this.enabled = true;
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
localStorage.setItem('dev-logger-enabled', 'true'); 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() { disable() {
this.enabled = false; this.enabled = false;
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
localStorage.setItem('dev-logger-enabled', 'false'); 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) { enableType(type: LogType) {
this.enabledTypes.add(type); this.enabledTypes.add(type);
this.saveEnabledTypes(); 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) { disableType(type: LogType) {
this.enabledTypes.delete(type); this.enabledTypes.delete(type);
this.saveEnabledTypes(); 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() { private saveEnabledTypes() {
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
localStorage.setItem('dev-logger-types', JSON.stringify(Array.from(this.enabledTypes))); localStorage.setItem(
"dev-logger-types",
JSON.stringify(Array.from(this.enabledTypes))
);
} }
} }
// Helper method to show current settings // Helper method to show current settings
status() { status() {
console.group('%c🔧 Dev Logger Status', 'color: #6366f1; font-weight: bold;'); console.group(
console.log('Enabled:', this.enabled); "%c🔧 Dev Logger Status",
console.log('Active Types:', Array.from(this.enabledTypes)); "color: #6366f1; font-weight: bold;"
);
console.log("Enabled:", this.enabled);
console.log("Active Types:", Array.from(this.enabledTypes));
console.groupEnd(); console.groupEnd();
} }
} }
@ -298,7 +365,7 @@ class DevLogger {
export const devLogger = new DevLogger(); export const devLogger = new DevLogger();
// Make it available globally for easy console access // Make it available globally for easy console access
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
(window as any).devLogger = devLogger; (window as any).devLogger = devLogger;
} }

View File

@ -91,7 +91,6 @@ export const createAppUiStore = (initState: AppUiState = initialState) => {
{ {
name: "app-ui-storage", name: "app-ui-storage",
onRehydrateStorage: () => state => { onRehydrateStorage: () => state => {
// Вызывается после загрузки данных из localStorage
if (state) { if (state) {
state.setHasHydrated(true); state.setHasHydrated(true);
} }

View File

@ -3,17 +3,23 @@
import { createStore } from "zustand"; import { createStore } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { IChat } from "@/entities/chats/types"; import { IChat, IChatMessage } from "@/entities/chats/types";
interface ChatState { interface ChatState {
currentChat: IChat | null; currentChat: IChat | null;
isAutoTopUp: boolean; isAutoTopUp: boolean;
suggestions: IChatMessage["suggestions"];
suggestionsHeight: number;
_hasHydrated: boolean;
} }
export type ChatActions = { export type ChatActions = {
setCurrentChat: (chat: IChat) => void; setCurrentChat: (chat: IChat) => void;
setIsAutoTopUp: (isAutoTopUp: boolean) => void; setIsAutoTopUp: (isAutoTopUp: boolean) => void;
setSuggestions: (suggestions: IChatMessage["suggestions"]) => void;
setSuggestionsHeight: (height: number) => void;
clearChatData: () => void; clearChatData: () => void;
setHasHydrated: (hasHydrated: boolean) => void;
}; };
export type ChatStore = ChatState & ChatActions; export type ChatStore = ChatState & ChatActions;
@ -21,6 +27,9 @@ export type ChatStore = ChatState & ChatActions;
const initialState: ChatState = { const initialState: ChatState = {
currentChat: null, currentChat: null,
isAutoTopUp: false, isAutoTopUp: false,
suggestions: [],
suggestionsHeight: 0,
_hasHydrated: false,
}; };
export const createChatStore = (initState: ChatState = initialState) => { export const createChatStore = (initState: ChatState = initialState) => {
@ -30,9 +39,23 @@ export const createChatStore = (initState: ChatState = initialState) => {
...initState, ...initState,
setCurrentChat: (chat: IChat) => set({ currentChat: chat }), setCurrentChat: (chat: IChat) => set({ currentChat: chat }),
setIsAutoTopUp: (isAutoTopUp: boolean) => set({ isAutoTopUp }), setIsAutoTopUp: (isAutoTopUp: boolean) => set({ isAutoTopUp }),
setSuggestions: (suggestions: IChatMessage["suggestions"]) =>
set({ suggestions }),
setSuggestionsHeight: (height: number) =>
set({ suggestionsHeight: height }),
clearChatData: () => set(initialState), clearChatData: () => set(initialState),
setHasHydrated: (hasHydrated: boolean) =>
set({ _hasHydrated: hasHydrated }),
}), }),
{ name: "chat-storage" } {
name: "chat-storage",
onRehydrateStorage: () => state => {
if (state) {
state.setHasHydrated(true);
}
},
}
) )
); );
}; };