/
KKleinikov
/
FastAPI-GigaChatAI
Обзор
Документация
Войти
/
KKleinikov
/
FastAPI-GigaChatAI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
templates/index.html
253 строки
9 KB
Konstantin-Kleinikov
1.05 Remove style from index.html and put it to /static/css/
06 дек 2025, 22:14
06 дек 2025, 22:14
622818a
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AI Bridge — GigaChat клиент</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&family=Manrope:wght@500;600&display=swap" rel="stylesheet"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"> <link rel="stylesheet" href="/static/css/style.css"> </head> <body> <div class="glow-blob"></div> <div class="glow-blob"></div> <div class="page"> <header class="header"> <div class="brand"> <div class="logo">AI</div> <div class="title"> <h1>AI Bridge</h1> <span>Frontend → FastAPI → GigaChat</span> </div> </div> <div class="status"> <div class="chip">API: <strong>http://localhost:8000</strong></div> <div class="badge" id="connBadge"><span class="status-dot"></span><span>Статус API</span></div> <button class="btn-ghost" id="pingBtn">Проверить</button> </div> </header> <div class="grid"> <section class="panel"> <h2>Задайте вопрос</h2> <p class="subtext">Отправьте промпт в GigaChat через API. Модель выбирается из списка ниже.</p> <!-- Поле ввода промпта --> <textarea id="promptInput" placeholder="Например: придумай стратегию запуска pet‑проекта с бюджетом 100$"></textarea> <!-- Выбор модели --> <div style="margin: 12px 0;"> <label for="modelSelect" style="display:block; margin-bottom:6px; color: var(--muted); font-size:14px;">Модель:</label> <select id="modelSelect" style="width:100%; padding:10px; border-radius:12px; border:1px solid var(--border); background:rgba(255,255,255,0.04); color:var(--text); font-size:14px;"> <option value="">Загрузка моделей...</option> </select> </div> <div class="controls"> <div class="hints"> <span class="muted">Сокращения: </span> <span class="badge">Ctrl/⌘ + Enter — отправить</span> </div> <div style="display:flex; gap:8px;"> <button type="button" class="btn-ghost" id="clearBtn">Очистить</button> <button type="button" class="btn-primary" id="sendBtn">Отправить</button> </div> </div> <div class="answer" id="answerBox">Ответ появится здесь.</div> </section> <aside class="panel"> <div style="display:flex; justify-content:space-between; align-items:center; gap:8px; margin-bottom:10px;"> <div> <h2 style="margin:0;">История запросов</h2> <p class="subtext">Ваши запросы сохраняются в базе.</p> </div> <button class="btn-ghost" id="refreshBtn">Обновить</button> </div> <div class="history" id="history"></div> </aside> </div> </div> <script> const promptInput = document.getElementById('promptInput'); const sendBtn = document.getElementById('sendBtn'); const clearBtn = document.getElementById('clearBtn'); const answerBox = document.getElementById('answerBox'); const historyEl = document.getElementById('history'); const refreshBtn = document.getElementById('refreshBtn'); const connBadge = document.getElementById('connBadge'); const pingBtn = document.getElementById('pingBtn'); const modelSelect = document.getElementById('modelSelect'); const API_BASE = 'http://localhost:8000'; const setLoading = (isLoading) => { sendBtn.disabled = isLoading; sendBtn.style.opacity = isLoading ? 0.65 : 1; answerBox.classList.toggle('loading', isLoading); }; const updateStatus = (ok) => { const dot = connBadge.querySelector('.status-dot'); connBadge.style.borderColor = ok ? 'rgba(47, 224, 162, 0.4)' : 'rgba(255, 86, 86, 0.4)'; connBadge.style.background = ok ? 'rgba(47, 224, 162, 0.12)' : 'rgba(255, 86, 86, 0.10)'; connBadge.querySelector('span:last-child').textContent = ok ? 'API доступен' : 'API недоступен'; dot.style.background = ok ? 'var(--accent)' : '#ff5656'; dot.style.boxShadow = ok ? '0 0 12px var(--glow)' : '0 0 12px rgba(255,86,86,0.4)'; connBadge.dataset.ok = ok ? '1' : '0'; }; const renderHistory = (items) => { historyEl.innerHTML = ''; if (!items || !items.length) { historyEl.innerHTML = '<div class="empty">История пуста. Отправьте первый запрос.</div>'; return; } items.forEach(({ id, prompt, response, model, timestamp }) => { const card = document.createElement('div'); card.className = 'card'; const label = document.createElement('div'); label.className = 'label'; label.textContent = `${new Date(timestamp).toLocaleString()} | Модель: ${model}`; const promptEl = document.createElement('p'); promptEl.className = 'prompt'; promptEl.textContent = prompt; const responseEl = document.createElement('p'); responseEl.className = 'response'; responseEl.textContent = response; card.appendChild(label); card.appendChild(promptEl); card.appendChild(responseEl); historyEl.appendChild(card); }); }; const fetchHistory = async () => { try { const res = await fetch(`${API_BASE}/chat_history`); if (!res.ok) throw new Error('History failed'); const data = await res.json(); renderHistory(data); updateStatus(true); } catch (e) { console.error(e); renderHistory([]); updateStatus(false); } }; const fetchModels = async () => { try { const res = await fetch(`${API_BASE}/models`); if (!res.ok) throw new Error('Models failed'); const models = await res.json(); modelSelect.innerHTML = ''; models.forEach(model => { const option = document.createElement('option'); option.value = model.model; option.textContent = `${model.model} (${model.capabilities[0] || '—'})`; modelSelect.appendChild(option); }); updateStatus(true); } catch (e) { console.error(e); modelSelect.innerHTML = '<option value="">Не удалось загрузить модели</option>'; updateStatus(false); } }; const sendPrompt = async () => { const prompt = promptInput.value.trim(); const model = modelSelect.value; if (!prompt) return; if (!model) { alert("Выберите модель"); return; } setLoading(true); try { const res = await fetch(`${API_BASE}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ prompt, model }) }); if (!res.ok) throw new Error('API error'); const data = await res.json(); setAnswer(data.response); // ← теперь с Markdown promptInput.value = ''; await fetchHistory(); } catch (e) { console.error(e); answerBox.textContent = 'Не удалось получить ответ. Проверьте, что backend запущен и разрешён CORS.'; updateStatus(false); } finally { setLoading(false); } }; // Загрузка данных при старте fetchModels(); fetchHistory(); // Обработчики событий sendBtn.addEventListener('click', sendPrompt); clearBtn.addEventListener('click', () => { promptInput.value = ''; promptInput.focus(); }); refreshBtn.addEventListener('click', fetchHistory); pingBtn.addEventListener('click', () => { fetchModels(); fetchHistory(); }); promptInput.addEventListener('keydown', (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { sendPrompt(); } }); </script> </body> <!-- Подсветка синтаксиса --> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"> <!-- Markdown рендеринг --> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script> // Настройка marked marked.setOptions({ gfm: true, breaks: true, sanitize: false, highlight: function(code, lang) { if (lang && window.hljs) { try { return window.hljs.highlight(lang, code).value; } catch (__) {} } return code; } }); // Функция для безопасного рендера Markdown const setAnswer = (text) => { answerBox.innerHTML = marked.parse(text || 'Пустой ответ'); }; // Вызов в sendPrompt: // setAnswer(data.response); </script> </html>