| <!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">×</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(); |
| } |
| }); |
| } |
| |
| function escapeHtml(unsafe) { |
| return unsafe |
| .replace(/&/g, "&") |
| .replace(/</g, "<") |
| .replace(/>/g, ">") |
| .replace(/"/g, """) |
| .replace(/'/g, "'"); |
| } |
| |
| 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; |
| } |
| |
| 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> |