| | <!DOCTYPE html> |
| | <html lang="ru"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>Qwen Turbo AI</title> |
| | |
| | <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
| | <style> |
| | :root { |
| | --bg-color: #212121; |
| | --chat-bg: #2f2f2f; |
| | --user-msg-bg: #303030; |
| | --ai-msg-bg: #212121; |
| | --accent: #10a37f; |
| | --text: #ececec; |
| | } |
| | body { margin: 0; font-family: 'Segoe UI', Roboto, sans-serif; background: var(--bg-color); color: var(--text); display: flex; flex-direction: column; height: 100vh; } |
| | |
| | |
| | header { padding: 15px; background: #171717; border-bottom: 1px solid #333; display: flex; align-items: center; justify-content: space-between; } |
| | h1 { margin: 0; font-size: 1.2rem; } |
| | |
| | |
| | #chat-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 20px; scroll-behavior: smooth; } |
| | |
| | .message { display: flex; gap: 15px; max-width: 800px; margin: 0 auto; width: 100%; } |
| | .avatar { width: 30px; height: 30px; border-radius: 5px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 0.9rem; flex-shrink: 0; } |
| | .user-avatar { background: #555; } |
| | .ai-avatar { background: var(--accent); } |
| | |
| | .content { line-height: 1.6; font-size: 1rem; padding-top: 4px; overflow-wrap: break-word; width: 100%; } |
| | .content p { margin-top: 0; } |
| | .content pre { background: #000; padding: 10px; border-radius: 5px; overflow-x: auto; } |
| | |
| | |
| | #input-area { background: #171717; padding: 20px; border-top: 1px solid #333; } |
| | .input-wrapper { max-width: 800px; margin: 0 auto; position: relative; } |
| | |
| | textarea { width: 100%; background: #40414f; border: 1px solid #555; color: white; padding: 12px 45px 12px 15px; border-radius: 10px; resize: none; outline: none; height: 50px; font-family: inherit; font-size: 1rem; box-sizing: border-box; } |
| | textarea:focus { border-color: var(--accent); } |
| | |
| | button#send-btn { position: absolute; right: 10px; bottom: 10px; background: transparent; border: none; cursor: pointer; color: #ccc; } |
| | button#send-btn:hover { color: white; } |
| | |
| | |
| | .controls { max-width: 800px; margin: 0 auto 10px; display: flex; gap: 15px; font-size: 0.9rem; color: #aaa; } |
| | .checkbox-wrapper { display: flex; align-items: center; gap: 5px; cursor: pointer; } |
| | .checkbox-wrapper input { cursor: pointer; accent-color: var(--accent); } |
| | |
| | |
| | .typing::after { content: '▋'; animation: blink 1s infinite; margin-left: 2px; } |
| | @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } |
| | </style> |
| | </head> |
| | <body> |
| |
|
| | <header> |
| | <h1>🤖 Qwen Turbo AI</h1> |
| | <div style="font-size: 0.8rem; color: #777;">Powered by HuggingFace</div> |
| | </header> |
| |
|
| | <div id="chat-container"> |
| | |
| | <div class="message"> |
| | <div class="avatar ai-avatar">AI</div> |
| | <div class="content">Привет! Я быстрый ИИ на базе Qwen 2.5. Могу искать информацию в интернете. Чем помочь?</div> |
| | </div> |
| | </div> |
| |
|
| | <div id="input-area"> |
| | <div class="controls"> |
| | <label class="checkbox-wrapper"> |
| | <input type="checkbox" id="web-search"> 🌐 Поиск в интернете (Tavily) |
| | </label> |
| | </div> |
| | <div class="input-wrapper"> |
| | <textarea id="user-input" placeholder="Введите сообщение..." onkeydown="handleKey(event)"></textarea> |
| | <button id="send-btn" onclick="sendMessage()">➤</button> |
| | </div> |
| | </div> |
| |
|
| | <script> |
| | const chatContainer = document.getElementById('chat-container'); |
| | const userInput = document.getElementById('user-input'); |
| | const webSearch = document.getElementById('web-search'); |
| | |
| | let history = []; |
| | |
| | function handleKey(e) { |
| | if (e.key === 'Enter' && !e.shiftKey) { |
| | e.preventDefault(); |
| | sendMessage(); |
| | } |
| | } |
| | |
| | async function sendMessage() { |
| | const text = userInput.value.trim(); |
| | if (!text) return; |
| | |
| | |
| | appendMessage('user', text); |
| | userInput.value = ''; |
| | |
| | |
| | const aiContentDiv = appendMessage('ai', ''); |
| | aiContentDiv.classList.add('typing'); |
| | |
| | |
| | const messagesToSend = [...history, { role: "user", content: text }]; |
| | |
| | try { |
| | |
| | const response = await fetch('/v1/chat/completions', { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ |
| | messages: messagesToSend, |
| | stream: true, |
| | use_search: webSearch.checked |
| | }) |
| | }); |
| | |
| | |
| | const reader = response.body.getReader(); |
| | const decoder = new TextDecoder(); |
| | let fullText = ""; |
| | |
| | while (true) { |
| | const { done, value } = await reader.read(); |
| | if (done) break; |
| | |
| | const chunk = decoder.decode(value, { stream: true }); |
| | const lines = chunk.split('\n'); |
| | |
| | for (const line of lines) { |
| | if (line.startsWith('data: ')) { |
| | const jsonStr = line.slice(6); |
| | if (jsonStr === '[DONE]') break; |
| | |
| | try { |
| | const json = JSON.parse(jsonStr); |
| | const delta = json.choices[0].delta.content; |
| | if (delta) { |
| | fullText += delta; |
| | |
| | aiContentDiv.innerHTML = marked.parse(fullText); |
| | chatContainer.scrollTop = chatContainer.scrollHeight; |
| | } |
| | } catch (e) { } |
| | } |
| | } |
| | } |
| | |
| | |
| | history.push({ role: "user", content: text }); |
| | history.push({ role: "assistant", content: fullText }); |
| | |
| | aiContentDiv.classList.remove('typing'); |
| | |
| | } catch (error) { |
| | aiContentDiv.innerHTML = `<span style="color:red">Ошибка: ${error.message}</span>`; |
| | } |
| | } |
| | |
| | function appendMessage(role, text) { |
| | const msgDiv = document.createElement('div'); |
| | msgDiv.className = 'message'; |
| | |
| | const avatar = document.createElement('div'); |
| | avatar.className = `avatar ${role === 'user' ? 'user-avatar' : 'ai-avatar'}`; |
| | avatar.textContent = role === 'user' ? 'Вы' : 'AI'; |
| | |
| | const content = document.createElement('div'); |
| | content.className = 'content'; |
| | content.innerHTML = role === 'user' ? text : ''; |
| | |
| | msgDiv.appendChild(avatar); |
| | msgDiv.appendChild(content); |
| | chatContainer.appendChild(msgDiv); |
| | chatContainer.scrollTop = chatContainer.scrollHeight; |
| | |
| | return content; |
| | } |
| | </script> |
| |
|
| | </body> |
| | </html> |