| <!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> |