Добавлен field_label в результат переименования файлов, исправлена загрузка черновиков, обновлен формат пути S3 с project_name
This commit is contained in:
@@ -1,20 +1,24 @@
|
||||
/* AI Drawer - основные стили */
|
||||
.ai-drawer {
|
||||
position: fixed;
|
||||
right: -400px; /* Начально скрыт */
|
||||
right: 0; /* Всегда прижат к правому краю */
|
||||
top: 0;
|
||||
width: 400px;
|
||||
min-width: 300px; /* Минимальная ширина */
|
||||
max-width: 50vw; /* Максимальная ширина - половина экрана */
|
||||
height: 100vh;
|
||||
max-height: 100vh; /* Не превышаем высоту экрана */
|
||||
background: #ffffff; /* Чистый белый фон */
|
||||
box-shadow: -2px 0 15px rgba(0,0,0,0.1);
|
||||
transition: right 0.3s ease;
|
||||
transform: translateX(100%); /* Начально скрыт - сдвинут вправо на 100% своей ширины */
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 999999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px; /* Базовый размер шрифта */
|
||||
border-left: 1px solid #e9ecef;
|
||||
overflow: hidden; /* Предотвращаем выход элементов за пределы */
|
||||
box-sizing: border-box; /* Учитываем padding и border в ширине */
|
||||
}
|
||||
|
||||
/* Полоска для изменения ширины */
|
||||
@@ -44,13 +48,18 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Убираем transition при изменении размера, чтобы не было задержек */
|
||||
.ai-drawer.resizing.open {
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
|
||||
.ai-drawer.resizing .ai-drawer-resize-handle {
|
||||
background: #007bff;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.ai-drawer.open {
|
||||
right: 0;
|
||||
transform: translateX(0); /* Показываем - сдвигаем на место */
|
||||
}
|
||||
|
||||
/* Скрываем кнопку AI когда drawer открыт */
|
||||
@@ -91,6 +100,9 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #0056b3;
|
||||
flex-shrink: 0; /* Не сжимается при изменении размера */
|
||||
min-height: 50px; /* Минимальная высота для кнопки закрытия */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ai-drawer-close {
|
||||
@@ -436,12 +448,42 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
|
||||
.ai-message-content p {
|
||||
margin: 0 0 5px 0;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ai-message-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Стили для ссылок в сообщениях */
|
||||
.ai-message-link {
|
||||
color: #007bff;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
word-break: break-word;
|
||||
display: inline-block;
|
||||
margin: 2px 0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ai-message-link:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: none;
|
||||
background-color: #e7f3ff;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.ai-message-link:visited {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.ai-message-link:active {
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.ai-message-time {
|
||||
font-size: 11px;
|
||||
color: #6c757d; /* Серый цвет для времени */
|
||||
@@ -456,6 +498,8 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
border-top: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0; /* Не сжимается при изменении размера */
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -510,7 +554,8 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
@media (max-width: 768px) {
|
||||
.ai-drawer {
|
||||
width: 100%;
|
||||
right: -100%;
|
||||
right: 0;
|
||||
transform: translateX(100%); /* Начально скрыт на мобильных */
|
||||
height: 100vh;
|
||||
height: 100dvh; /* Динамическая высота viewport для мобильных */
|
||||
display: flex;
|
||||
@@ -731,7 +776,8 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
width: 400px;
|
||||
min-width: 300px;
|
||||
max-width: 50vw;
|
||||
right: -400px;
|
||||
right: 0;
|
||||
transform: translateX(100%); /* Начально скрыт на планшетах */
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -48,7 +48,11 @@ class AIDrawer {
|
||||
'<div class="ai-avatar assistant"></div>' +
|
||||
'<div class="ai-message-content">' +
|
||||
'<p>Привет! Я ваш AI ассистент. Чем могу помочь?</p>' +
|
||||
'<div class="ai-message-time">' + new Date().toLocaleTimeString() + '</div>' +
|
||||
'<div class="ai-message-time">' + new Date().toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false // 24-часовой формат
|
||||
}) + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
@@ -128,7 +132,7 @@ class AIDrawer {
|
||||
|
||||
// Обработчик изменения размера окна - ограничиваем ширину если нужно
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.drawerWidth > window.innerWidth / 2) {
|
||||
if (this.isOpen && this.drawerWidth > window.innerWidth / 2) {
|
||||
const maxWidth = window.innerWidth / 2;
|
||||
this.setDrawerWidth(maxWidth);
|
||||
}
|
||||
@@ -144,8 +148,18 @@ class AIDrawer {
|
||||
const savedWidth = localStorage.getItem('ai-drawer-width');
|
||||
if (savedWidth) {
|
||||
const width = parseInt(savedWidth, 10);
|
||||
if (width >= 300 && width <= window.innerWidth / 2) {
|
||||
const maxWidth = window.innerWidth / 2;
|
||||
// Проверяем что ширина в допустимых пределах для текущего экрана
|
||||
if (width >= 300 && width <= maxWidth) {
|
||||
this.setDrawerWidth(width);
|
||||
} else if (width > maxWidth) {
|
||||
// Если сохраненная ширина больше максимума - ограничиваем
|
||||
console.log('AI Drawer: Saved width', width, 'exceeds max', maxWidth, ', adjusting');
|
||||
this.setDrawerWidth(maxWidth);
|
||||
} else {
|
||||
// Если меньше минимума - устанавливаем минимум
|
||||
console.log('AI Drawer: Saved width', width, 'is less than minimum, setting to 300');
|
||||
this.setDrawerWidth(300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +234,23 @@ class AIDrawer {
|
||||
open() {
|
||||
console.log('AI Drawer: Opening drawer');
|
||||
if (this.drawer) {
|
||||
// Проверяем и корректируем ширину перед открытием
|
||||
const maxWidth = window.innerWidth / 2;
|
||||
if (this.drawerWidth > maxWidth) {
|
||||
console.log('AI Drawer: Adjusting width from', this.drawerWidth, 'to', maxWidth);
|
||||
this.setDrawerWidth(maxWidth);
|
||||
} else if (this.drawerWidth < 300) {
|
||||
console.log('AI Drawer: Adjusting width from', this.drawerWidth, 'to 300');
|
||||
this.setDrawerWidth(300);
|
||||
}
|
||||
|
||||
// Убеждаемся что ширина применена к drawer
|
||||
this.drawer.style.width = this.drawerWidth + 'px';
|
||||
|
||||
// Убеждаемся что drawer правильно позиционирован перед открытием
|
||||
this.drawer.style.right = '0';
|
||||
this.drawer.style.transform = 'translateX(0)';
|
||||
|
||||
this.drawer.classList.add('open');
|
||||
}
|
||||
|
||||
@@ -234,6 +265,33 @@ class AIDrawer {
|
||||
mainContainer.setAttribute('data-drawer-width', this.drawerWidth);
|
||||
}
|
||||
|
||||
// Прокручиваем вниз к последнему сообщению при открытии
|
||||
const scrollToBottomOnOpen = () => {
|
||||
const drawerContent = this.drawer?.querySelector('.ai-drawer-content');
|
||||
const chatMessages = this.drawer?.querySelector('.ai-chat-messages');
|
||||
|
||||
if (drawerContent) {
|
||||
const scroll = () => {
|
||||
drawerContent.scrollTop = drawerContent.scrollHeight;
|
||||
console.log('AI Drawer: Scrolled on open, scrollTop:', drawerContent.scrollTop, 'scrollHeight:', drawerContent.scrollHeight);
|
||||
};
|
||||
|
||||
// Прокручиваем последнее сообщение в видимую область
|
||||
if (chatMessages && chatMessages.lastElementChild) {
|
||||
chatMessages.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
scroll();
|
||||
requestAnimationFrame(scroll);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Прокручиваем после анимации открытия
|
||||
setTimeout(scrollToBottomOnOpen, 300);
|
||||
setTimeout(scrollToBottomOnOpen, 600);
|
||||
|
||||
// История уже загружена при инициализации страницы
|
||||
// Не нужно дополнительных запросов при открытии
|
||||
}
|
||||
@@ -242,6 +300,10 @@ class AIDrawer {
|
||||
console.log('AI Drawer: Closing drawer');
|
||||
if (this.drawer) {
|
||||
this.drawer.classList.remove('open');
|
||||
// Убеждаемся что drawer скрыт через transform
|
||||
if (!this.drawer.classList.contains('open')) {
|
||||
this.drawer.style.transform = 'translateX(100%)';
|
||||
}
|
||||
}
|
||||
|
||||
document.body.classList.remove('ai-drawer-open');
|
||||
@@ -295,6 +357,142 @@ class AIDrawer {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для определения читаемого текста ссылки на основе URL
|
||||
getLinkText(url) {
|
||||
const urlLower = url.toLowerCase();
|
||||
const maxLength = 60; // Максимальная длина ссылки до замены
|
||||
|
||||
// Если ссылка короткая, показываем её полностью
|
||||
if (url.length <= maxLength) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Анализируем URL и определяем тип действия
|
||||
if (urlLower.includes('download') || urlLower.includes('file') || urlLower.includes('скачать')) {
|
||||
return '📥 Скачать документ';
|
||||
}
|
||||
|
||||
if (urlLower.includes('edit') || urlLower.includes('редактир') || urlLower.includes('onlyoffice')) {
|
||||
return '✏️ Открыть для редактирования';
|
||||
}
|
||||
|
||||
if (urlLower.includes('view') || urlLower.includes('просмотр') || urlLower.includes('preview')) {
|
||||
return '👁️ Открыть для просмотра';
|
||||
}
|
||||
|
||||
if (urlLower.includes('document') || urlLower.includes('документ') || urlLower.includes('.docx') || urlLower.includes('.pdf')) {
|
||||
return '📄 Открыть документ';
|
||||
}
|
||||
|
||||
if (urlLower.includes('create') || urlLower.includes('создать')) {
|
||||
return '➕ Создать документ';
|
||||
}
|
||||
|
||||
// Общие варианты для длинных ссылок
|
||||
const linkTexts = [
|
||||
'🔗 Открыть ссылку',
|
||||
'👉 Смотреть здесь',
|
||||
'📋 Подробнее',
|
||||
'🔍 Перейти к документу',
|
||||
'📎 Открыть'
|
||||
];
|
||||
|
||||
// Выбираем случайный вариант для разнообразия
|
||||
return linkTexts[Math.floor(Math.random() * linkTexts.length)];
|
||||
}
|
||||
|
||||
// Функция для преобразования URL в кликабельные ссылки
|
||||
convertUrlsToLinks(text) {
|
||||
if (!text) return '';
|
||||
|
||||
let result = text;
|
||||
|
||||
// ШАГ 1: Обрабатываем Markdown ссылки [текст](url)
|
||||
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
result = result.replace(markdownLinkRegex, (match, linkText, url) => {
|
||||
// Проверяем, что это валидный URL
|
||||
if (url.match(/^https?:\/\//i)) {
|
||||
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="ai-message-link" title="${url}">${linkText}</a>`;
|
||||
}
|
||||
return match; // Если не URL, оставляем как есть
|
||||
});
|
||||
|
||||
// ШАГ 2: Временно заменяем уже существующие HTML-ссылки на плейсхолдеры
|
||||
const htmlLinks = [];
|
||||
const htmlLinkRegex = /<a\s+[^>]*href\s*=\s*["']([^"']+)["'][^>]*>([^<]*)<\/a>/gi;
|
||||
result = result.replace(htmlLinkRegex, (match, href, linkText) => {
|
||||
const placeholder = `__HTML_LINK_${htmlLinks.length}__`;
|
||||
htmlLinks.push({ href, linkText, match });
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// ШАГ 3: Экранируем оставшийся HTML для безопасности
|
||||
const escaped = result
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
// ШАГ 4: Восстанавливаем HTML-ссылки (плейсхолдеры не экранированы, т.к. не содержат < >)
|
||||
let finalResult = escaped;
|
||||
htmlLinks.forEach((link, index) => {
|
||||
const placeholder = `__HTML_LINK_${index}__`;
|
||||
// Используем оригинальную ссылку, но добавляем класс если его нет
|
||||
let htmlLink = link.match;
|
||||
if (!htmlLink.includes('class=')) {
|
||||
htmlLink = htmlLink.replace('<a', '<a class="ai-message-link"');
|
||||
} else if (!htmlLink.includes('ai-message-link')) {
|
||||
htmlLink = htmlLink.replace('class="', 'class="ai-message-link ');
|
||||
htmlLink = htmlLink.replace("class='", "class='ai-message-link ");
|
||||
}
|
||||
// Убеждаемся что есть target="_blank"
|
||||
if (!htmlLink.includes('target=')) {
|
||||
htmlLink = htmlLink.replace('<a', '<a target="_blank" rel="noopener noreferrer"');
|
||||
}
|
||||
// Заменяем плейсхолдер на правильный HTML (не экранированный)
|
||||
finalResult = finalResult.replace(placeholder, htmlLink);
|
||||
});
|
||||
|
||||
// ШАГ 5: Преобразуем обычные URL в ссылки (только те, что не внутри уже существующих ссылок)
|
||||
const urlRegex = /(https?:\/\/[^\s<>"{}|\\^`\[\]]+)/gi;
|
||||
let urlMatches = [];
|
||||
let match;
|
||||
|
||||
// Сначала находим все URL и проверяем их контекст
|
||||
while ((match = urlRegex.exec(finalResult)) !== null) {
|
||||
const url = match[0];
|
||||
const offset = match.index;
|
||||
const beforeMatch = finalResult.substring(0, offset);
|
||||
|
||||
// Проверяем, нет ли открывающего тега <a перед этим URL
|
||||
const lastOpenTag = beforeMatch.lastIndexOf('<a');
|
||||
const lastCloseTag = beforeMatch.lastIndexOf('</a>');
|
||||
|
||||
// Если есть открывающий тег <a и нет закрывающего после него - значит мы внутри ссылки
|
||||
if (lastOpenTag > lastCloseTag) {
|
||||
continue; // Пропускаем, это уже часть ссылки
|
||||
}
|
||||
|
||||
// Проверяем, не является ли это частью href атрибута
|
||||
if (beforeMatch.lastIndexOf('href=') > lastCloseTag) {
|
||||
continue; // Пропускаем, это часть href
|
||||
}
|
||||
|
||||
urlMatches.push({ url, offset });
|
||||
}
|
||||
|
||||
// Заменяем URL в обратном порядке (чтобы не сбить индексы)
|
||||
for (let i = urlMatches.length - 1; i >= 0; i--) {
|
||||
const { url, offset } = urlMatches[i];
|
||||
const linkText = this.getLinkText(url);
|
||||
const linkHtml = `<a href="${url}" target="_blank" rel="noopener noreferrer" class="ai-message-link" title="${url}">${linkText}</a>`;
|
||||
finalResult = finalResult.substring(0, offset) + linkHtml + finalResult.substring(offset + url.length);
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
|
||||
addMessage(text, isUser = false, customTime = null) {
|
||||
console.log('AI Drawer: addMessage called with:', {text: text.substring(0, 50), isUser, customTime});
|
||||
|
||||
@@ -331,17 +529,69 @@ class AIDrawer {
|
||||
contentDiv.className = 'ai-message-content';
|
||||
|
||||
const textDiv = document.createElement('p');
|
||||
textDiv.textContent = text;
|
||||
// Преобразуем URL в кликабельные ссылки
|
||||
textDiv.innerHTML = this.convertUrlsToLinks(text);
|
||||
contentDiv.appendChild(textDiv);
|
||||
|
||||
const timeDiv = document.createElement('div');
|
||||
timeDiv.className = 'ai-message-time';
|
||||
if (customTime) {
|
||||
// Если передано время из истории, используем его
|
||||
const historyTime = new Date(customTime);
|
||||
timeDiv.textContent = historyTime.toLocaleTimeString();
|
||||
try {
|
||||
// Логируем для отладки
|
||||
console.log('AI Drawer: Parsing timestamp:', customTime);
|
||||
const historyTime = new Date(customTime);
|
||||
// Проверяем что дата валидна
|
||||
if (isNaN(historyTime.getTime())) {
|
||||
// Если дата невалидна, пытаемся распарсить как строку времени (старый формат)
|
||||
console.warn('AI Drawer: Invalid timestamp format:', customTime, 'Parsed as:', historyTime);
|
||||
timeDiv.textContent = customTime; // Показываем как есть
|
||||
} else {
|
||||
// Определяем, нужно ли показывать дату
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const messageDate = new Date(historyTime.getFullYear(), historyTime.getMonth(), historyTime.getDate());
|
||||
const isToday = messageDate.getTime() === today.getTime();
|
||||
|
||||
let formattedTime;
|
||||
if (isToday) {
|
||||
// Если сообщение сегодня - показываем только время
|
||||
formattedTime = historyTime.toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
} else {
|
||||
// Если сообщение не сегодня - показываем дату и время
|
||||
const dateStr = historyTime.toLocaleDateString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit'
|
||||
});
|
||||
const timeStr = historyTime.toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
formattedTime = `${dateStr} ${timeStr}`;
|
||||
}
|
||||
|
||||
console.log('AI Drawer: Successfully formatted timestamp:', customTime, '->', formattedTime);
|
||||
timeDiv.textContent = formattedTime;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AI Drawer: Error parsing timestamp:', customTime, error);
|
||||
timeDiv.textContent = customTime || new Date().toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false // 24-часовой формат
|
||||
});
|
||||
}
|
||||
} else {
|
||||
timeDiv.textContent = new Date().toLocaleTimeString();
|
||||
timeDiv.textContent = new Date().toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false // 24-часовой формат
|
||||
});
|
||||
}
|
||||
contentDiv.appendChild(timeDiv);
|
||||
|
||||
@@ -388,7 +638,11 @@ class AIDrawer {
|
||||
|
||||
const timeDiv = document.createElement('div');
|
||||
timeDiv.className = 'ai-message-time';
|
||||
timeDiv.textContent = new Date().toLocaleTimeString();
|
||||
timeDiv.textContent = new Date().toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false // 24-часовой формат
|
||||
});
|
||||
contentDiv.appendChild(timeDiv);
|
||||
|
||||
messageDiv.appendChild(avatarDiv);
|
||||
@@ -404,9 +658,12 @@ class AIDrawer {
|
||||
|
||||
streamText(element, text, speed = 30) {
|
||||
let index = 0;
|
||||
let currentText = '';
|
||||
const interval = setInterval(() => {
|
||||
if (index < text.length) {
|
||||
element.textContent += text[index];
|
||||
currentText += text[index];
|
||||
// Преобразуем URL в кликабельные ссылки по мере добавления текста
|
||||
element.innerHTML = this.convertUrlsToLinks(currentText);
|
||||
index++;
|
||||
|
||||
const content = this.drawer.querySelector('.ai-drawer-content');
|
||||
@@ -880,6 +1137,41 @@ class AIDrawer {
|
||||
}
|
||||
});
|
||||
|
||||
// Прокручиваем вниз к последнему сообщению после загрузки истории
|
||||
const scrollToBottom = () => {
|
||||
const drawerContent = this.drawer?.querySelector('.ai-drawer-content');
|
||||
const chatMessages = this.drawer?.querySelector('.ai-chat-messages');
|
||||
|
||||
if (drawerContent) {
|
||||
// Способ 1: Прокручиваем контейнер
|
||||
const scroll = () => {
|
||||
drawerContent.scrollTop = drawerContent.scrollHeight;
|
||||
console.log('AI Drawer: Scrolled container, scrollTop:', drawerContent.scrollTop, 'scrollHeight:', drawerContent.scrollHeight);
|
||||
};
|
||||
|
||||
// Способ 2: Прокручиваем последнее сообщение в видимую область
|
||||
if (chatMessages && chatMessages.lastElementChild) {
|
||||
chatMessages.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
console.log('AI Drawer: Scrolled last message into view');
|
||||
}
|
||||
|
||||
// Используем requestAnimationFrame для более надежной прокрутки
|
||||
requestAnimationFrame(() => {
|
||||
scroll();
|
||||
requestAnimationFrame(() => {
|
||||
scroll();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.warn('AI Drawer: Drawer content not found for scrolling');
|
||||
}
|
||||
};
|
||||
|
||||
// Прокручиваем с несколькими задержками для надежности
|
||||
setTimeout(scrollToBottom, 100);
|
||||
setTimeout(scrollToBottom, 300);
|
||||
setTimeout(scrollToBottom, 600);
|
||||
|
||||
console.log('AI Drawer: Chat history restored -', data.history.length, 'messages');
|
||||
} else {
|
||||
console.log('AI Drawer: No chat history found. Response:', data);
|
||||
|
||||
Reference in New Issue
Block a user