fix floating support chat
All checks were successful
Deploy / CreateImage (push) Successful in 14m11s
Deploy / DeployService (push) Successful in 27s

This commit is contained in:
Nastya 2025-07-23 12:05:33 +03:00
parent ac692fafd3
commit 2fecf6ca14
7 changed files with 145 additions and 25 deletions

@ -6,7 +6,7 @@
"@craco/craco": "^7.0.0",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.110",
"@frontend/kitui": "1.0.110",
"@frontend/squzanswerer": "^1.0.57",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",

@ -1,4 +1,4 @@
import { clearAuthToken, getMessageFromFetchError, handleComponentError, UserAccount, useTicketsFetcher, useUserFetcher } from "@frontend/kitui";
import { clearAuthToken, createMakeRequestConfig, getMessageFromFetchError, handleComponentError, UserAccount, useTicketsFetcher, useUserFetcher } from "@frontend/kitui";
import type { OriginalUserAccount } from "@root/user";
import { clearUserData, setCustomerAccount, setUser, setUserAccount, useUserStore } from "@root/user";
import ContactFormModal from "@ui_kit/ContactForm";
@ -27,6 +27,7 @@ import Debug from "./pages/Debug";
import { setTicketData, setTickets, useTicketStore } from "./stores/ticket";
import { parseAxiosError } from "./utils/parse-error";
import { ErrorBoundary } from "react-error-boundary";
import { handleLogoutClick } from "./utils/HandleLogoutClick";
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery"));
@ -42,6 +43,12 @@ const PersonalizationAI = lazy(() => import("./pages/PersonalizationAI/Personali
let params = new URLSearchParams(document.location.search);
const isTest = Boolean(params.get("test"))
createMakeRequestConfig(
handleLogoutClick,
(error, info, getTickets) => handleComponentError(error, info, getTickets()),
() => useTicketStore.getState().tickets
);
const routeslink = [
{
path: "/edit",
@ -185,7 +192,7 @@ export default function App() {
return (
<ErrorBoundary
FallbackComponent={ApologyPage}
onError={(error, info) => handleComponentError(error, info, tickets)}
onError={(error, info) => handleComponentError(error, info, () => useTicketStore.getState().tickets)}
>
{amoAccount && <AmoTokenExpiredDialog isAmoTokenExpired={amoAccount.stale} />}

@ -30,7 +30,18 @@ interface Props {
sendFile: (a: File | undefined) => Promise<void>;
}
const greetingMessage = "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
const greetingMessage: TicketMessage = {
id: "greeting",
ticket_id: "",
user_id: "system",
session_id: "",
message: "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут",
files: [],
shown: {},
request_screenshot: "",
created_at: new Date().toISOString(),
system: false
};
export default function Chat({
open = false,
@ -197,10 +208,7 @@ export default function Chat({
>
{ticket.sessionData?.ticketId &&
messages.map((message) => {
const isSelf = useMemo(() =>
(ticket.sessionData?.sessionId || user) === message.user_id,
[ticket.sessionData?.sessionId, user, message.user_id]
);
const isSelf = (ticket.sessionData?.sessionId || user) === message.user_id;
return (
<ChatMessageRenderer
@ -213,10 +221,7 @@ export default function Chat({
{!ticket.sessionData?.ticketId && (
<ChatMessageRenderer
message={greetingMessage}
isSelf={useMemo(() =>
(ticket.sessionData?.sessionId || user) === greetingMessage.user_id,
[ticket.sessionData?.sessionId, user, greetingMessage.user_id]
)}
isSelf={false}
/>
)}
</Box>

@ -130,9 +130,15 @@ export default () => {
({ shown }) => shown?.me !== 1,
);
newMessages.forEach(({ id, user_id }) => {
if ((ticket.sessionData?.sessionId || user) === user_id) shownMessage(id);
});
// Находим последнее сообщение, которое не от пользователя
const lastNonUserMessage = newMessages
.filter(({ user_id }) => (ticket.sessionData?.sessionId || user) !== user_id)
.pop();
// Отправляем shown только на последнее сообщение, которое не от пользователя
if (lastNonUserMessage) {
shownMessage(lastNonUserMessage.id);
}
}
}, [isChatOpened, ticket.messages]);

@ -0,0 +1,109 @@
import { ErrorInfo } from "react";
import { Ticket, createTicket, getAuthToken, sendTicketMessage } from "..";
let errorsQueue: ComponentError[] = [];
let timeoutId: ReturnType<typeof setTimeout>;
interface ComponentError {
timestamp: number;
message: string;
callStack: string | undefined;
componentStack: string | null | undefined;
}
function isErrorReportingAllowed(error?: Error): boolean {
// Если ошибка помечена как debug-override — всегда отправлять
if (error && (error as any).__forceSend) return true;
// Проверяем домен
const currentDomain = window.location.hostname;
return currentDomain !== 'localhost';
}
// Новый API: getTickets — callback, возвращающий актуальные тикеты
export function handleComponentError(error: Error, info: ErrorInfo, getTickets: () => Ticket[]) {
//репортим только о авторизонышах
if (!getAuthToken()) return;
// Проверяем разрешение на отправку ошибок (по домену)
if (!isErrorReportingAllowed(error)) {
console.log('❌ Отправка ошибки заблокирована:', error.message);
return;
}
console.log(`✅ Обработка ошибки: ${error.message}`);
// Копируем __forceSend если есть
const componentError: ComponentError & { __forceSend?: boolean } = {
timestamp: Math.floor(Date.now() / 1000),
message: error.message,
callStack: error.stack,
componentStack: info.componentStack,
...(error && (error as any).__forceSend ? { __forceSend: true } : {})
};
queueErrorRequest(componentError, getTickets);
}
// Ставит ошибку в очередь для отправки, через 1 секунду вызывает sendErrorsToServer
export function queueErrorRequest(error: ComponentError, getTickets: () => Ticket[]) {
errorsQueue.push(error);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
sendErrorsToServer(getTickets);
}, 1000);
}
// Отправляет накопленные ошибки в тикеты, ищет существующий тикет с system: true или создает новый
export async function sendErrorsToServer(getTickets: () => Ticket[]) {
if (errorsQueue.length === 0) return;
// Проверяем разрешение на отправку ошибок (по домену и debug-override)
// Если хотя бы одна ошибка в очереди с __forceSend, отправляем всё
const forceSend = errorsQueue.some(e => (e as any).__forceSend);
if (!forceSend && !isErrorReportingAllowed()) {
console.log('❌ Отправка ошибок заблокирована, очищаем очередь');
errorsQueue = [];
return;
}
const tickets = getTickets();
try {
// Формируем сообщение об ошибке
const errorMessage = errorsQueue.map(error => {
return `[${new Date(error.timestamp * 1000).toISOString()}] ${error.message}\n\nCall Stack:\n${error.callStack || 'N/A'}\n\nComponent Stack:\n${error.componentStack || 'N/A'}`;
}).join('\n\n---\n\n');
// ВСЕГДА ищем тикет через API
const existingSystemTicket = await findSystemTicket(tickets);
if (existingSystemTicket) {
sendTicketMessage({
ticketId: existingSystemTicket,
message: errorMessage,
systemError: true,
});
} else {
// Создаем новый тикет для ошибки
createTicket({
message: errorMessage,
useToken: true,
systemError: true,
});
}
} catch (error) {
console.error('Error in sendErrorsToServer:', error);
} finally {
// Очищаем очередь ошибок
errorsQueue = [];
}
}
// Ищет существующий тикет с system: true
export async function findSystemTicket(tickets: Ticket[]) {
for (const ticket of tickets) {
console.log("[findSystemTicket] Проверяем тикет:", ticket);
if (!('messages' in ticket)) {
if (ticket.top_message && ticket.top_message.system === true) {
console.log("[findSystemTicket] Найден тикет по top_message.system:true:", ticket.id);
return ticket.id;
}
}
}
}
export function clearErrorHandlingConfig () {
clearTimeout(timeoutId);
errorsQueue = [];
}

@ -28,13 +28,6 @@ export const useAfterPay = () => {
let URLadditionalinformation = searchParams.get("additionalinformation");//его токен
useEffect(() => {
console.log("useAutoPay: Processing return from payment", {
URLaction,
URLuserId,
URLadditionalinformation,
userId,
wayback: searchParams.get("wayback")
});
setSearchParams({}, { replace: true });

@ -1443,10 +1443,10 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429"
integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
"@frontend/kitui@^1.0.110":
"@frontend/kitui@1.0.110":
version "1.0.110"
resolved "http://gitea.pena/api/packages/skeris/npm/%40frontend%2Fkitui/-/1.0.110/kitui-1.0.110.tgz#969f70636508e9efd6c8d81e62a6913b18a0c029"
integrity sha512-M+U9a4qylLb9ZOUn57v7lm/Rutqicm04vJsJrxeAY/6G4ma1bC29toOZwTt/uJUlF4gk4ojyWIIjiGTVM1/hKQ==
resolved "http://gitea.pena/api/packages/skeris/npm/%40frontend%2Fkitui/-/1.0.110/kitui-1.0.110.tgz#0c0a968293338537a2811e7761f8efe933893573"
integrity sha512-XOCev5zNtNZ8fu3IfK6oFNOqT8lE9jlmUX1kQ3OO+H30/LBpnBrww9nV/aHV2TEm0wYXdRMvaEtU6VOb72sDdg==
dependencies:
immer "^10.0.2"
reconnecting-eventsource "^1.6.2"