/
yurezproj
/
kursovaya
Обзор
Документация
Войти
/
yurezproj
/
kursovaya
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
script.js
538 строк
25 KB
yurezproj
create: deepseek-api.js, index.html, ollama-api.js, script.js, server.js, style.css
11 май 2026, 14:38
Верифицирован
11 май 2026, 14:38
e05f98a
Код
Авторство
О чём код?
// script.js - версия с Ollama (универсальный режим) (function() { // ========== ТЕМА ========== const themeToggle = document.getElementById('theme-toggle'); const html = document.documentElement; if (localStorage.getItem('theme') === 'dark') { html.classList.add('dark'); if(themeToggle) themeToggle.innerHTML = '☀️'; } if(themeToggle) { themeToggle.addEventListener('click', () => { if (html.classList.contains('dark')) { html.classList.remove('dark'); localStorage.setItem('theme', 'light'); themeToggle.innerHTML = '🌙'; } else { html.classList.add('dark'); localStorage.setItem('theme', 'dark'); themeToggle.innerHTML = '☀️'; } }); } // ========== DOM ЭЛЕМЕНТЫ ========== const dropzone = document.getElementById('dropzone'); const fileInput = document.getElementById('file-input'); const fileInfo = document.getElementById('file-info'); const fileName = document.getElementById('file-name'); const removeFile = document.getElementById('remove-file'); const analyzeBtn = document.getElementById('analyze-btn'); const miniConsole = document.getElementById('mini-console'); const resultsList = document.getElementById('results-list'); const statErrors = document.getElementById('stat-errors'); const statWarnings = document.getElementById('stat-warnings'); const statOk = document.getElementById('stat-ok'); const stageUpload = document.getElementById('stage-upload'); const stageExtract = document.getElementById('stage-extract'); const stageSearch = document.getElementById('stage-search'); const stageAI = document.getElementById('stage-ai'); const stageResult = document.getElementById('stage-result'); const ollamaStatus = document.getElementById('ollama-status'); const universalQuestion = document.getElementById('universal-question'); let currentFile = null; let ollamaClient = null; // Функции консоли и статусов function addConsoleMessage(msg, type = 'info') { if(!miniConsole) return; const div = document.createElement('div'); div.className = 'text-xs'; const icon = type === 'error' ? '❌' : (type === 'success' ? '✅' : '⚡'); div.innerHTML = `${icon} ${msg}`; if (type === 'error') { div.style.color = '#ef4444'; } else if (type === 'success') { div.style.color = '#10b981'; } else { div.style.color = 'var(--text-secondary)'; } miniConsole.appendChild(div); miniConsole.scrollTop = miniConsole.scrollHeight; while (miniConsole.children.length > 30) { miniConsole.removeChild(miniConsole.firstChild); } } function updateStageStatus(stage, status) { if (!stage) return; const icon = stage.querySelector('i'); const text = stage.querySelector('span'); stage.classList.remove('opacity-50', 'text-green-500', 'text-blue-500', 'text-red-500'); if (status === 'active') { stage.classList.add('text-blue-500'); if (icon) { icon.classList.remove('fa-check-circle', 'fa-times-circle'); icon.classList.add('fa-spinner', 'fa-spin'); } if (text) text.classList.add('font-semibold'); } else if (status === 'completed') { stage.classList.add('text-green-500'); if (icon) { icon.classList.remove('fa-spinner', 'fa-spin', 'fa-times-circle'); icon.classList.add('fa-check-circle'); } if (text) text.classList.remove('font-semibold'); } else if (status === 'error') { stage.classList.add('text-red-500'); if (icon) { icon.classList.remove('fa-spinner', 'fa-spin', 'fa-check-circle'); icon.classList.add('fa-times-circle'); } } else { stage.classList.add('opacity-50'); if (icon) { icon.classList.remove('fa-spinner', 'fa-spin', 'fa-check-circle', 'fa-times-circle'); if (stage.id === 'stage-upload') icon.classList.add('fa-cloud-upload-alt'); if (stage.id === 'stage-extract') icon.classList.add('fa-robot'); if (stage.id === 'stage-search') icon.classList.add('fa-database'); if (stage.id === 'stage-ai') icon.classList.add('fa-brain'); if (stage.id === 'stage-result') icon.classList.add('fa-file-signature'); } } } function resetStages() { [stageUpload, stageExtract, stageSearch, stageAI, stageResult].forEach(stage => { if (stage) updateStageStatus(stage, 'reset'); }); } function completeStage(stage) { if (stage) updateStageStatus(stage, 'completed'); } function activateStage(stage) { if (stage) updateStageStatus(stage, 'active'); } function errorStage(stage) { if (stage) updateStageStatus(stage, 'error'); } // Проверка статуса Ollama async function checkOllamaStatus() { try { addConsoleMessage('🦙 Проверка подключения к Ollama...'); const response = await fetch('http://localhost:11434/api/tags'); if (response.ok) { const data = await response.json(); const hasModel = data.models && data.models.some(m => m.name.includes('qwen2:1.5b')); if (hasModel) { ollamaStatus.innerHTML = '<i class="fas fa-check-circle text-green-500"></i> Ollama: подключен, модель qwen2:1.5b готова'; ollamaStatus.classList.remove('bg-yellow-500/10', 'border-yellow-500/30'); ollamaStatus.classList.add('bg-green-500/10', 'border-green-500/30'); ollamaClient = new OllamaClient('qwen2:1.5b'); addConsoleMessage('Ollama подключен, модель qwen2:1.5b готова', 'success'); return true; } else { ollamaStatus.innerHTML = '<i class="fas fa-exclamation-triangle text-yellow-500"></i> Модель qwen2:1.5b не найдена. Выполните: ollama pull qwen2:1.5b'; addConsoleMessage('Модель qwen2:1.5b не найдена. Выполните ollama pull qwen2:1.5b', 'error'); return false; } } else { throw new Error('Ollama не отвечает'); } } catch (error) { ollamaStatus.innerHTML = '<i class="fas fa-times-circle text-red-500"></i> Ollama не подключен. Запустите терминал как администратор и выполните: ollama serve'; addConsoleMessage('Ollama не доступен. Убедитесь что Ollama запущен', 'error'); return false; } } // Drag & drop if(dropzone) { dropzone.addEventListener('click', () => fileInput?.click()); dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('dragover'); }); dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover')); dropzone.addEventListener('drop', (e) => { e.preventDefault(); dropzone.classList.remove('dragover'); if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); }); } if(fileInput) { fileInput.addEventListener('change', (e) => { if (e.target.files.length) handleFile(e.target.files[0]); }); } function handleFile(file) { currentFile = file; if(fileName) fileName.textContent = file.name; if(fileInfo) fileInfo.classList.remove('hidden'); if(analyzeBtn) { analyzeBtn.disabled = false; analyzeBtn.classList.remove('opacity-50', 'cursor-not-allowed'); } if (stageUpload) updateStageStatus(stageUpload, 'completed'); addConsoleMessage(`📎 Загружен: ${file.name} (${(file.size/1024).toFixed(1)} КБ)`, 'success'); } if(removeFile) { removeFile.addEventListener('click', () => { currentFile = null; if(fileInfo) fileInfo.classList.add('hidden'); if(fileInput) fileInput.value = ''; if(analyzeBtn) { analyzeBtn.disabled = true; analyzeBtn.classList.add('opacity-50', 'cursor-not-allowed'); } resetStages(); addConsoleMessage('Файл удален'); }); } // Извлечение текста из файла async function extractTextFromFile(file) { activateStage(stageExtract); addConsoleMessage(`📄 Извлечение текста из ${file.name}...`); if (file.name.endsWith('.txt')) { const text = await file.text(); addConsoleMessage(`Извлечено ${text.length} символов из TXT`, 'success'); completeStage(stageExtract); return text; } if (file.name.endsWith('.pdf')) { addConsoleMessage('Парсинг PDF файла...'); try { const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; let fullText = ''; const totalPages = pdf.numPages; addConsoleMessage(`Найдено страниц: ${totalPages}`); for (let i = 1; i <= totalPages; i++) { const page = await pdf.getPage(i); const textContent = await page.getTextContent(); const pageText = textContent.items.map(item => item.str).join(' '); fullText += `\n--- СТРАНИЦА ${i} ---\n${pageText}\n`; if (i % 5 === 0 || i === totalPages) { addConsoleMessage(`Обработано страниц: ${i}/${totalPages}`); } } addConsoleMessage(`Извлечено ${fullText.length} символов из PDF`, 'success'); completeStage(stageExtract); return fullText; } catch (error) { addConsoleMessage(`Ошибка PDF: ${error.message}`, 'error'); errorStage(stageExtract); return `[Ошибка: ${error.message}]`; } } addConsoleMessage(`Неподдерживаемый формат. Используйте TXT или PDF`, 'error'); errorStage(stageExtract); return `[Неподдерживаемый формат: ${file.name}]`; } // Отправка запроса в Ollama async function sendToOllama(projectText, userQuestion = null) { if (!ollamaClient) { throw new Error('Ollama не подключен. Проверьте статус в левой панели.'); } // Обрезаем текст, чтобы не перегружать модель const maxDocLength = 6000; const truncatedDoc = projectText.length > maxDocLength ? projectText.substring(0, maxDocLength) + "\n...[документ обрезан для экономии памяти]..." : projectText; let systemPrompt, userMessage; if (userQuestion && userQuestion.trim()) { // Режим вопроса/расчета systemPrompt = `Ты эксперт по дорожному строительству. Отвечай используя только загруженный нормативный документ. Отвечай на русском языке подробно и профессионально. Нормативный документ: ${truncatedDoc}`; userMessage = `Вопрос: ${userQuestion} Данные из документа: ${truncatedDoc.substring(0, 2000)}`; } else { // Режим автоматической проверки systemPrompt = `Ты эксперт по нормоконтролю дорожного строительства. Проверь проект на соответствие нормативам из загруженного документа. Отвечай ТОЛЬКО в формате JSON, без лишнего текста: { "errors": [{"norm": "ссылка на норматив", "description": "описание ошибки", "recommendation": "рекомендация"}], "warnings": [{"norm": "ссылка на норматив", "description": "описание замечания", "recommendation": "рекомендация"}], "compliant": [{"norm": "ссылка на норматив", "description": "что соответствует"}], "summary": "краткое заключение по всему документу" } Нормативный документ: ${truncatedDoc}`; userMessage = `Проверь этот проект на соответствие нормативам: ${truncatedDoc.substring(0, 3000)}`; } const messages = [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage } ]; addConsoleMessage('Отправка запроса в Ollama...'); addConsoleMessage('Это может занять 20-40 секунд...'); activateStage(stageAI); let responseText = ''; await ollamaClient.sendRequest(messages, (chunk) => { responseText += chunk; renderStreamingResponse(responseText, userQuestion); }); completeStage(stageAI); return responseText; } function renderStreamingResponse(text, isQuestion = false) { if(!resultsList) return; // Если это вопрос (не авто-проверка) или нет JSON if (isQuestion && isQuestion.trim()) { resultsList.innerHTML = ` <div class="bg-[var(--bg-primary)] rounded-xl p-4 border border-[var(--border-light)]"> <div class="flex items-center gap-2 mb-3"> <i class="fas fa-robot text-[var(--accent)]"></i> <span class="font-medium">Ответ ИИ:</span> </div> <div class="text-sm whitespace-pre-wrap leading-relaxed">${formatResponse(text)}</div> </div> `; return; } // Попытка распарсить JSON для проверки try { const jsonMatch = text.match(/\{[\s\S]*\}/); if (jsonMatch) { const result = JSON.parse(jsonMatch[0]); renderCheckResults(result); return; } } catch (e) { // Если JSON не полный, показываем как есть resultsList.innerHTML = ` <div class="bg-[var(--bg-primary)] rounded-xl p-4 border border-[var(--border-light)]"> <div class="flex items-center gap-2 mb-3"> <i class="fas fa-robot text-[var(--accent)]"></i> <span class="font-medium">Анализ в процессе:</span> </div> <div class="text-sm whitespace-pre-wrap leading-relaxed">${formatResponse(text)}</div> </div> `; } } function renderCheckResults(result) { if(!resultsList) return; const errors = result.errors || []; const warnings = result.warnings || []; const compliant = result.compliant || []; if(statErrors) statErrors.textContent = errors.length; if(statWarnings) statWarnings.textContent = warnings.length; if(statOk) statOk.textContent = compliant.length; let html = ''; if (errors.length > 0) { html += `<div class="mb-4"><h3 class="font-semibold text-[var(--error)] mb-2"><i class="fas fa-times-circle"></i> Ошибки (${errors.length})</h3>`; errors.forEach(err => { html += `<div class="violation-item error"> <div class="font-medium text-sm"> ${escapeHtml(err.norm || 'Неизвестный норматив')}</div> <div class="text-sm mt-1">${escapeHtml(err.description)}</div> <div class="text-xs mt-2 text-[var(--accent)]"> ${escapeHtml(err.recommendation || 'Нет рекомендации')}</div> </div>`; }); html += `</div>`; } if (warnings.length > 0) { html += `<div class="mb-4"><h3 class="font-semibold text-[var(--warning)] mb-2"><i class="fas fa-exclamation-triangle"></i> Замечания (${warnings.length})</h3>`; warnings.forEach(warn => { html += `<div class="violation-item warning"> <div class="font-medium text-sm">${escapeHtml(warn.norm || 'Неизвестный норматив')}</div> <div class="text-sm mt-1">${escapeHtml(warn.description)}</div> <div class="text-xs mt-2 text-[var(--accent)]">${escapeHtml(warn.recommendation || 'Нет рекомендации')}</div> </div>`; }); html += `</div>`; } if (compliant.length > 0) { html += `<div class="mb-4"><h3 class="font-semibold text-[var(--success)] mb-2"><i class="fas fa-check-circle"></i> Соответствует нормативам (${compliant.length})</h3>`; compliant.forEach(comp => { html += `<div class="violation-item success"> <div class="font-medium text-sm">${escapeHtml(comp.norm || 'Норматив')}</div> <div class="text-sm mt-1">${escapeHtml(comp.description)}</div> </div>`; }); html += `</div>`; } if (result.summary) { html += `<div class="mt-4 p-4 bg-[var(--accent-soft)] rounded-xl"> <div class="font-medium mb-2">Заключение</div> <div class="text-sm">${escapeHtml(result.summary)}</div> </div>`; } if (errors.length === 0 && warnings.length === 0 && compliant.length === 0 && !result.summary) { html = `<div class="text-center py-8"> <i class="fas fa-check-circle text-4xl text-[var(--success)] mb-3"></i> <p class="text-lg font-medium">Документ соответствует нормативам!</p> <p class="text-sm text-[var(--text-secondary)] mt-2">Все параметры в норме</p> </div>`; } resultsList.innerHTML = html; completeStage(stageResult); } function formatResponse(text) { let formatted = escapeHtml(text); formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); formatted = formatted.replace(/\*(.*?)\*/g, '<em>$1</em>'); formatted = formatted.replace(/`(.*?)`/g, '<code class="bg-[var(--bg-primary)] px-1 rounded">$1</code>'); formatted = formatted.replace(/\n/g, '<br>'); return formatted; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async function analyzeDocumentWithAI(file, userQuestion = null) { if (!file) { addConsoleMessage('Сначала загрузите файл', 'error'); return; } try { resetStages(); if (stageUpload) updateStageStatus(stageUpload, 'completed'); const documentText = await extractTextFromFile(file); addConsoleMessage(`Извлечено ${documentText.length} символов`); if (stageSearch) updateStageStatus(stageSearch, 'completed'); const response = await sendToOllama(documentText, userQuestion); addConsoleMessage('Анализ завершен', 'success'); completeStage(stageResult); } catch (error) { addConsoleMessage(`Ошибка: ${error.message}`, 'error'); errorStage(stageAI); if(resultsList) { resultsList.innerHTML = `<div class="text-center py-8 text-red-500">Ошибка: ${escapeHtml(error.message)}</div>`; } } } // Обработчик кнопки анализа if(analyzeBtn) { analyzeBtn.addEventListener('click', async () => { if (!currentFile) { alert('Загрузите файл с нормативной документацией (ГОСТ, СНиП, СП)'); return; } const isOllamaReady = await checkOllamaStatus(); if (!isOllamaReady) { alert('Ollama не подключен.\n\nЗапустите терминал как администратор и выполните:\nollama serve\n\nПосле запуска обновите страницу'); return; } analyzeBtn.disabled = true; analyzeBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Анализ...'; resetStages(); if(resultsList) { resultsList.innerHTML = '<div class="text-center py-8"><i class="fas fa-spinner fa-spin text-2xl"></i><p class="mt-2">Анализ через Ollama...</p><p class="text-xs text-[var(--text-secondary)] mt-1">Это может занять 20-40 секунд</p></div>'; } const userQuestion = universalQuestion ? universalQuestion.value : null; await analyzeDocumentWithAI(currentFile, userQuestion); analyzeBtn.disabled = false; analyzeBtn.innerHTML = '<i class="fas fa-search"></i> Запустить анализ'; }); } // Экспорт const exportBtn = document.getElementById('export-btn'); if(exportBtn) { exportBtn.addEventListener('click', () => { if(!resultsList) return; const resultsText = resultsList.innerText; if (resultsText && !resultsText.includes('Загрузите проект') && resultsText !== '') { const blob = new Blob([resultsText], { type: 'text/plain' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `analysis_${Date.now()}.txt`; a.click(); URL.revokeObjectURL(a.href); addConsoleMessage('Экспортировано', 'success'); } else { addConsoleMessage('Нет результатов для экспорта', 'error'); } }); } // Поделиться const shareBtn = document.getElementById('share-btn'); if(shareBtn) { shareBtn.addEventListener('click', async () => { if(!resultsList) return; const resultsText = resultsList.innerText; if (resultsText && !resultsText.includes('Загрузите проект') && resultsText !== '') { try { await navigator.clipboard.writeText(resultsText); addConsoleMessage('📋 Результаты скопированы в буфер обмена', 'success'); alert('Результаты скопированы в буфер обмена!'); } catch (err) { addConsoleMessage('Не удалось скопировать', 'error'); } } else { addConsoleMessage('Нет результатов для копирования', 'error'); } }); } // Запуск проверки Ollama setTimeout(() => checkOllamaStatus(), 1000); addConsoleMessage('Система готова. Загрузите документ и нажмите "Анализ"', 'success'); })();