PrintAI / templates /index.html
KennyOry's picture
Update templates/index.html
47e40d1 verified
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>InkORA AI | Сервисный ассистент (beta)</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #4361ee;
--primary-light: #4895ef;
--secondary: #3f37c9;
--dark: #0e1424;
--light: #f8f9fa;
--gray: #e9ecef;
--success: #2ecc71;
--warning: #f39c12;
--danger: #e74c3c;
--transition: all 0.3s ease;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7f1 100%);
color: var(--dark);
min-height: 100vh;
padding: 20px;
}
.app-container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 300px;
gap: 20px;
}
.card {
background: rgba(255, 255, 255, 0.92);
border-radius: 16px;
border: none;
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.1);
backdrop-filter: blur(4px);
overflow: hidden;
transition: var(--transition);
}
.card:hover {
box-shadow: 0 12px 40px rgba(31, 38, 135, 0.15);
}
.card-header {
background: white;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
padding: 20px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.card-body {
padding: 0;
}
.chat-container {
height: calc(100vh - 200px);
display: flex;
flex-direction: column;
padding: 20px;
overflow-y: auto;
}
.message {
max-width: 80%;
padding: 16px 20px;
margin-bottom: 16px;
border-radius: 18px;
line-height: 1.5;
position: relative;
animation: fadeIn 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03);
will-change: transform, opacity;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.user-message {
background: var(--primary);
color: white;
align-self: flex-end;
border-bottom-right-radius: 4px;
}
.bot-message {
background: white;
border: 1px solid var(--gray);
align-self: flex-start;
border-bottom-left-radius: 4px;
}
.message-header {
font-size: 12px;
opacity: 0.8;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.input-area {
padding: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.05);
background: white;
transition: var(--transition);
}
.input-group {
display: flex;
gap: 10px;
}
#user-input {
flex: 1;
border: 1px solid var(--gray);
border-radius: 12px;
padding: 14px 20px;
font-size: 16px;
transition: var(--transition);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
}
#user-input:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.15);
outline: none;
}
#send-btn {
background: var(--primary);
color: white;
border: none;
border-radius: 12px;
padding: 0 24px;
font-weight: 600;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 6px rgba(67, 97, 238, 0.2);
}
#send-btn:hover {
background: var(--secondary);
transform: translateY(-1px);
}
#send-btn:active {
transform: translateY(1px);
}
.typing-indicator {
display: flex;
gap: 6px;
padding: 20px;
align-items: center;
}
.typing-text {
margin-left: 10px;
font-style: italic;
color: #6c757d;
}
.typing-dot {
width: 10px;
height: 10px;
background: var(--primary-light);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.typing-dot:nth-child(1) {
animation-delay: -0.32s;
}
.typing-dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
.log-entry {
padding: 14px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
font-size: 14px;
line-height: 1.5;
animation: fadeIn 0.3s ease;
}
.log-entry:last-child {
border-bottom: none;
}
.log-timestamp {
font-size: 11px;
opacity: 0.6;
margin-right: 8px;
}
.log-info {
color: var(--primary);
}
.log-success {
color: var(--success);
}
.log-warning {
color: var(--warning);
}
.log-error {
color: var(--danger);
}
.problem-text {
font-weight: 600;
color: var(--primary);
margin-bottom: 8px;
}
.solution-text {
font-weight: 600;
color: var(--secondary);
margin-bottom: 8px;
}
.solution-step {
padding-left: 20px;
position: relative;
margin-bottom: 6px;
}
.solution-step:before {
content: "•";
position: absolute;
left: 8px;
color: var(--primary);
font-weight: bold;
}
.sources-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(67, 97, 238, 0.1);
color: var(--primary);
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
margin-top: 15px;
cursor: pointer;
transition: var(--transition);
}
.sources-badge:hover {
background: rgba(67, 97, 238, 0.15);
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
padding: 4px 12px;
border-radius: 20px;
background: rgba(46, 204, 113, 0.1);
color: var(--success);
}
.status-indicator.offline {
background: rgba(231, 76, 60, 0.1);
color: var(--danger);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
}
.status-indicator.offline .status-dot {
background: var(--danger);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 20px;
color: var(--dark);
}
.logo-icon {
width: 36px;
height: 36px;
background: var(--primary);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.source-item {
padding: 12px 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
transition: var(--transition);
}
.source-item:hover {
background: rgba(67, 97, 238, 0.03);
}
.source-title {
font-weight: 500;
margin-bottom: 4px;
display: block;
}
.source-url {
font-size: 13px;
color: #6c757d;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 40px 20px;
color: #6c757d;
}
.empty-state i {
font-size: 48px;
margin-bottom: 20px;
color: #dee2e6;
}
.empty-state h4 {
font-weight: 500;
margin-bottom: 10px;
color: #495057;
}
.example-badge {
display: inline-block;
background: white;
border: 1px solid var(--gray);
border-radius: 8px;
padding: 8px 16px;
margin: 6px;
font-size: 14px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
}
.log-container {
height: calc(100vh - 200px);
overflow-y: auto;
}
.highlight {
background-color: #fff9c4;
padding: 2px 4px;
border-radius: 4px;
font-weight: 600;
}
.message-content {
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.6;
}
.source-link {
color: #4361ee;
text-decoration: underline;
cursor: pointer;
margin-left: 5px;
font-size: 0.9em;
}
.source-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.source-modal-content {
background: white;
border-radius: 16px;
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.source-modal-header {
padding: 20px;
background: var(--primary);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.source-modal-body {
padding: 20px;
overflow-y: auto;
flex-grow: 1;
}
.source-modal-footer {
padding: 15px 20px;
background: var(--gray);
display: flex;
justify-content: flex-end;
gap: 10px;
}
.source-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
}
.source-content {
line-height: 1.6;
max-height: 50vh;
overflow-y: auto;
padding: 10px;
border: 1px solid #eee;
border-radius: 8px;
background: #fafafa;
}
.source-original-link {
display: inline-block;
margin-top: 15px;
color: var(--primary);
}
.notes-text {
font-weight: 600;
color: #f39c12;
margin-top: 15px;
margin-bottom: 8px;
border-left: 3px solid #f39c12;
padding-left: 10px;
}
.sources-title {
font-weight: 600;
color: #3f37c9;
margin-top: 15px;
margin-bottom: 8px;
}
.note-item {
padding-left: 20px;
position: relative;
margin-bottom: 6px;
font-style: italic;
}
.note-item:before {
content: "•";
position: absolute;
left: 8px;
color: #f39c12;
}
.source-reference {
display: inline-block;
background: rgba(67, 97, 238, 0.1);
color: var(--primary);
padding: 2px 8px;
border-radius: 4px;
font-size: 0.9em;
margin-right: 5px;
}
/* Новые стили */
.d-none {
display: none !important;
}
.log-text {
line-height: 1.6;
padding: 10px 0;
white-space: pre-wrap;
}
.processing-steps {
padding: 10px 0;
font-size: 0.95em;
}
.step-item {
margin-bottom: 8px;
display: flex;
align-items: flex-start;
}
.step-icon {
margin-right: 10px;
color: var(--primary);
min-width: 20px;
}
.step-text {
flex: 1;
}
.step-active {
font-weight: 600;
color: var(--secondary);
}
/* Новые стили для анимированного лоадера */
.step-loader {
display: flex;
gap: 4px;
height: 20px;
align-items: center;
}
.step-loader .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--primary);
animation: loader-bounce 1.4s infinite ease-in-out both;
}
.step-loader .dot:nth-child(1) {
animation-delay: -0.32s;
}
.step-loader .dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes loader-bounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
@media (max-width: 768px) {
.app-container {
grid-template-columns: 1fr;
}
.chat-container {
height: 60vh;
}
.log-container {
height: 30vh;
}
}
</style>
</head>
<body>
<div class="app-container">
<div class="card main-card">
<div class="card-header">
<div class="logo">
<div class="logo-icon">
<i class="fas fa-print"></i>
</div>
<span>InkORA AI (beta)</span>
</div>
<div class="ms-auto status-indicator" id="status-indicator">
<div class="status-dot"></div>
<span>Подключено</span>
</div>
</div>
<div class="card-body">
<div class="chat-container" id="chat-container">
<div class="empty-state">
<i class="fas fa-comments"></i>
<h4>Добро пожаловать в InkORA AI</h4>
<p>Опишите проблему с принтером или МФУ, и я постараюсь найти решение</p>
<div class="mt-3">
<div class="example-badge">HP LaserJet 1020 не печатает</div>
<div class="example-badge">Konica Minolta C258 ошибка C5611</div>
<div class="example-badge">Canon i-SENSYS MF644Cdw зажевывает бумагу</div>
</div>
</div>
</div>
<div class="input-area">
<div class="input-group">
<input type="text" id="user-input" class="form-control"
placeholder="Опишите проблему с принтером..." autocomplete="off">
<button id="send-btn" class="btn">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card logs-card">
<div class="card-header">
<i class="fas fa-terminal"></i>
<span>Журнал обработки</span>
</div>
<div class="card-body">
<div class="log-container" id="logs-container">
<div class="log-entry log-info">
Система инициализирована. Ожидание запроса...
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const chatContainer = document.getElementById('chat-container');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const logsContainer = document.getElementById('logs-container');
const statusIndicator = document.getElementById('status-indicator');
const statusDot = statusIndicator.querySelector('.status-dot');
const statusText = statusIndicator.querySelector('span');
const welcomeState = document.querySelector('.empty-state');
let currentSources = [];
let isProcessing = false;
let eventSource = null;
let currentBotMessage = null;
let processingLog = [];
let responseBuffer = '';
// Новые переменные
let currentLogMessage = null;
let processingStep = 0;
const processingSteps = [
"Запрос отправлен на сервер",
"Извлекаю параметры из входящего запроса",
"Провожу поиск по запросу",
"Анализирую источники",
"Определяю проблему",
"Генерирую ответ на основе источников"
];
// Функция добавления временной метки
function getTimestamp() {
const now = new Date();
return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
// Функция добавления сообщения в журнал
function addLog(message, type = 'info') {
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${type}`;
logEntry.innerHTML = `
<span class="log-timestamp">${getTimestamp()}</span>
${message}
`;
logsContainer.appendChild(logEntry);
logsContainer.scrollTop = logsContainer.scrollHeight;
processingLog.push(`[${getTimestamp()}] ${message}`);
}
// Добавление сообщения пользователя
function addUserMessage(message) {
if (welcomeState && welcomeState.parentElement === chatContainer) {
chatContainer.innerHTML = '';
}
const messageDiv = document.createElement('div');
messageDiv.className = 'message user-message';
messageDiv.innerHTML = `
<div class="message-header">
<i class="fas fa-user"></i>
<span>Вы</span>
<span class="ms-auto">${getTimestamp()}</span>
</div>
<div>${escapeHtml(message)}</div>
`;
chatContainer.appendChild(messageDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// Создание сообщения бота с логами процесса
function createBotLogMessage() {
if (welcomeState && welcomeState.parentElement === chatContainer) {
chatContainer.innerHTML = '';
}
const messageDiv = document.createElement('div');
messageDiv.className = 'message bot-message';
messageDiv.id = 'bot-log-message';
messageDiv.innerHTML = `
<div class="message-header">
<i class="fas fa-robot"></i>
<span>PrintMaster</span>
<span class="ms-auto">${getTimestamp()}</span>
</div>
<div class="processing-steps">
${processingSteps.map((step, index) => `
<div class="step-item" id="step-${index}">
<div class="step-icon">
<div class="step-loader ${index === 0 ? '' : 'd-none'}">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<i class="far fa-circle ${index !== 0 ? 'd-none' : ''}"></i>
<i class="fas fa-check-circle step-done d-none"></i>
</div>
<div class="step-text ${index === 0 ? 'step-active' : ''}">${step}${index === 0 ? '...' : ''}</div>
</div>
`).join('')}
</div>
`;
chatContainer.appendChild(messageDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
return messageDiv;
}
// Обновление шагов процесса
function updateProcessingSteps(stepIndex) {
for (let i = 0; i < processingSteps.length; i++) {
const stepElement = document.getElementById(`step-${i}`);
if (!stepElement) continue;
const loader = stepElement.querySelector('.step-loader');
const iconCircle = stepElement.querySelector('.fa-circle');
const iconCheck = stepElement.querySelector('.fa-check-circle');
const text = stepElement.querySelector('.step-text');
// Если элементы не найдены, пропускаем шаг
if (!loader || !iconCircle || !text) continue;
if (i < stepIndex) {
// Завершенные шаги
loader.classList.add('d-none');
iconCircle.classList.add('d-none');
if (iconCheck) iconCheck.classList.remove('d-none');
text.classList.remove('step-active');
text.textContent = processingSteps[i];
} else if (i === stepIndex) {
// Текущий шаг
loader.classList.remove('d-none');
iconCircle.classList.add('d-none');
if (iconCheck) iconCheck.classList.add('d-none');
text.classList.add('step-active');
text.textContent = processingSteps[i] + '...';
} else {
// Будущие шаги
loader.classList.add('d-none');
iconCircle.classList.remove('d-none');
if (iconCheck) iconCheck.classList.add('d-none');
text.classList.remove('step-active');
text.textContent = processingSteps[i];
}
}
}
// Показ следующего шага обработки
function showNextProcessingStep() {
processingStep++;
if (processingStep < processingSteps.length) {
updateProcessingSteps(processingStep);
return true;
}
return false;
}
// Имитация прогресса обработки
function simulateProcessingProgress() {
if (processingStep < processingSteps.length - 1) {
setTimeout(() => {
if (showNextProcessingStep()) {
simulateProcessingProgress();
}
}, 1000 + Math.random() * 1500);
}
}
// Скрытие/показа поля ввода
function toggleInputArea(show) {
const inputArea = document.querySelector('.input-area');
if (show) {
inputArea.classList.remove('d-none');
} else {
inputArea.classList.add('d-none');
}
}
// Преобразование индикатора в полноценное сообщение
function convertToMessage(content) {
const logMessageDiv = document.getElementById('bot-log-message');
if (logMessageDiv) {
logMessageDiv.innerHTML = `
<div class="message-header">
<i class="fas fa-robot"></i>
<span>PrintMaster</span>
<span class="ms-auto">${getTimestamp()}</span>
</div>
<div class="message-content">${formatContent(content)}</div>
`;
logMessageDiv.id = 'bot-message-' + Date.now();
return logMessageDiv;
}
// Если индикатора нет, создаем новое сообщение
const messageDiv = document.createElement('div');
messageDiv.className = 'message bot-message';
messageDiv.id = 'bot-message-' + Date.now();
messageDiv.innerHTML = `
<div class="message-header">
<i class="fas fa-robot"></i>
<span>PrintMaster</span>
<span class="ms-auto">${getTimestamp()}</span>
</div>
<div class="message-content">${formatContent(content)}</div>
`;
chatContainer.appendChild(messageDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
return messageDiv;
}
// Форматирование контента
function formatContent(content) {
// Заменяем разделы на стилизованные блоки
content = content.replace(/\*\*Проблема:\*\*/g,
'<div class="problem-text">Проблема:</div>');
content = content.replace(/\*\*Решение:\*\*/g,
'<div class="solution-text">Решение:</div>');
content = content.replace(/\*\*Примечания:\*\*/g,
'<div class="notes-text">Примечания:</div>');
content = content.replace(/\*\*Источники:\*\*/g,
'<div class="sources-title">Источники:</div>');
content = content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Обработка ссылок на источники
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
'<a href="$2" target="_blank" class="source-link">$1</a>');
return content;
}
// Добавление ссылок на источники
function addSourceLinks() {
if (!currentBotMessage || currentSources.length === 0) return;
const messageDiv = currentBotMessage;
const existingSources = messageDiv.querySelector('.sources-container');
if (existingSources) existingSources.remove();
if (currentSources.length > 0) {
const sourcesContainer = document.createElement('div');
sourcesContainer.className = 'sources-container mt-3';
sourcesContainer.innerHTML = '<div class="font-bold text-gray-700 mb-2 flex items-center"><i class="fas fa-link mr-2"></i> Источники информации</div>';
const linksContainer = document.createElement('div');
linksContainer.className = 'flex flex-wrap gap-2';
currentSources.slice(0, 5).forEach((source, index) => {
const link = document.createElement('span');
link.className = 'sources-badge';
link.innerHTML = `<i class="fas fa-external-link-alt mr-1"></i> Источник ${index + 1}`;
link.dataset.sourceIndex = index;
link.addEventListener('click', function (e) {
e.preventDefault();
const sourceIndex = parseInt(this.dataset.sourceIndex);
showSourceDetails(sourceIndex);
});
linksContainer.appendChild(link);
});
sourcesContainer.appendChild(linksContainer);
messageDiv.appendChild(sourcesContainer);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}
// Показ деталей источника
function showSourceDetails(sourceIndex) {
const source = currentSources[sourceIndex];
if (!source) return;
// Создаем модальное окно
const modal = document.createElement('div');
modal.className = 'source-modal';
modal.innerHTML = `
<div class="source-modal-content">
<div class="source-modal-header">
<h3>${source.title || 'Источник информации'}</h3>
<button class="source-close">&times;</button>
</div>
<div class="source-modal-body">
<p><strong>URL:</strong> <a href="${source.url}" target="_blank">${source.url}</a></p>
<div class="mt-3">
<h4>Содержимое:</h4>
<div class="source-content">${source.content ? escapeHtml(source.content) : 'Содержимое недоступно'}</div>
</div>
</div>
<div class="source-modal-footer">
<button id="open-source" class="btn btn-primary">Открыть источник</button>
<button id="close-modal" class="btn btn-secondary">Закрыть</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Обработчики событий
modal.querySelector('.source-close').addEventListener('click', () => modal.remove());
modal.querySelector('#close-modal').addEventListener('click', () => modal.remove());
modal.querySelector('#open-source').addEventListener('click', () => {
window.open(source.url, '_blank');
});
// Закрытие по клику вне окна
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
}
// Функция для экранирования HTML
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Подключение потока данных
function connectStream() {
if (eventSource) eventSource.close();
// Обновляем статус
statusText.textContent = 'Обработка запроса...';
statusDot.style.background = 'var(--warning)';
statusIndicator.classList.add('warning');
statusIndicator.classList.remove('success');
eventSource = new EventSource('/stream');
eventSource.onmessage = function (event) {
try {
const data = JSON.parse(event.data);
const { type, content } = data;
if (type === 'log' || type === 'processing_log') {
addLog(content, 'info');
}
else if (type === 'response_end') {
// ТОЛЬКО ЗДЕСЬ ОБРАБАТЫВАЕМ ОТВЕТ
responseBuffer = content; // Перезаписываем буфер
currentBotMessage = convertToMessage(responseBuffer);
addSourceLinks();
toggleInputArea(true);
}
else if (type === 'sources') {
try {
currentSources = JSON.parse(content);
} catch (e) {
console.error('Error parsing sources:', e);
addLog('Ошибка обработки источников', 'error');
}
}
else if (type === 'done') {
isProcessing = false;
statusText.textContent = 'Подключено';
statusDot.style.background = 'var(--success)';
statusIndicator.classList.add('success');
statusIndicator.classList.remove('warning');
if (eventSource) {
eventSource.close();
eventSource = null;
}
}
} catch (e) {
console.error('Error processing event:', e);
addLog(`Ошибка обработки данных: ${e.message}`, 'error');
}
};
eventSource.onerror = function () {
addLog("⚠️ Поток данных прерван", 'error');
statusText.textContent = 'Ошибка соединения';
statusDot.style.background = 'var(--danger)';
statusIndicator.classList.add('danger');
statusIndicator.classList.remove('success', 'warning');
toggleInputArea(true); // Показываем поле ввода
if (eventSource) {
eventSource.close();
eventSource = null;
}
// Попытка восстановления через 2 секунды
setTimeout(() => {
if (!isProcessing) {
statusText.textContent = 'Подключено';
statusDot.style.background = 'var(--success)';
statusIndicator.classList.add('success');
statusIndicator.classList.remove('danger');
}
}, 2000);
};
}
// Отправка сообщения
function sendMessage() {
const message = userInput.value.trim();
if (!message || isProcessing) return;
// Добавляем сообщение пользователя
addUserMessage(message);
userInput.value = '';
// Скрываем поле ввода
toggleInputArea(false);
// Сбрасываем состояние обработки
isProcessing = true;
processingStep = 0;
currentSources = [];
responseBuffer = '';
currentBotMessage = null;
// Создаем сообщение бота с логами
currentLogMessage = createBotLogMessage();
// Обновляем статус
statusText.textContent = 'Обработка запроса...';
statusDot.style.background = 'var(--warning)';
statusIndicator.classList.add('warning');
statusIndicator.classList.remove('success');
// Запускаем имитацию прогресса
simulateProcessingProgress();
// Подключаемся к потоку данных
connectStream();
// Отправляем запрос на сервер
fetch('/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `message=${encodeURIComponent(message)}`
})
.then(res => res.json())
.then(data => {
if (data.status === 'processing') {
addLog("⚙️ Запрос отправлен на сервер...", 'info');
}
})
.catch(err => {
addLog(`❌ Ошибка запроса: ${err.message}`, 'error');
isProcessing = false;
statusText.textContent = 'Ошибка соединения';
statusDot.style.background = 'var(--danger)';
statusIndicator.classList.add('danger');
statusIndicator.classList.remove('warning');
toggleInputArea(true); // Показываем поле ввода
// Обновляем сообщение бота
const errorMessage = "Ошибка обработки запроса. Пожалуйста, попробуйте снова.";
if (currentLogMessage) {
currentLogMessage.innerHTML = `
<div class="message-header">
<i class="fas fa-robot"></i>
<span>PrintMaster</span>
<span class="ms-auto">${getTimestamp()}</span>
</div>
<div class="message-content text-danger">${errorMessage}</div>
`;
}
// Восстанавливаем статус
setTimeout(() => {
statusText.textContent = 'Подключено';
statusDot.style.background = 'var(--success)';
statusIndicator.classList.add('success');
statusIndicator.classList.remove('danger');
}, 2000);
});
}
// Обработчики событий
sendBtn.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Закрытие соединений при уходе со страницы
window.addEventListener('beforeunload', () => {
if (eventSource) {
eventSource.close();
eventSource = null;
}
});
});
</script>
</body>
</html>