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);
+ }
+ },
+ }
)
);
};