| <!DOCTYPE html> |
| <html lang="ru"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>🤖 Puter AI Chat</title> |
| <style> |
| :root { --bg: #0f172a; --card: #1e293b; --text: #e2e8f0; --accent: #3b82f6; --user: #2563eb; --ai: #334155; } |
| body { margin: 0; font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); display: flex; flex-direction: column; height: 100vh; } |
| header { padding: 1rem; background: var(--card); display: flex; gap: 1rem; align-items: center; border-bottom: 1px solid #334155; flex-wrap: wrap; } |
| select, button { padding: 0.5rem 1rem; border-radius: 0.5rem; border: none; cursor: pointer; } |
| select { background: #334155; color: white; } |
| button { background: var(--accent); color: white; font-weight: 600; } |
| button:disabled { opacity: 0.5; cursor: not-allowed; } |
| #chat { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; } |
| .msg { max-width: 80%; padding: 0.75rem 1rem; border-radius: 1rem; line-height: 1.5; white-space: pre-wrap; } |
| .user { align-self: flex-end; background: var(--user); } |
| .ai { align-self: flex-start; background: var(--ai); } |
| footer { padding: 1rem; background: var(--card); display: flex; gap: 0.5rem; border-top: 1px solid #334155; } |
| input { flex: 1; padding: 0.75rem; border-radius: 0.5rem; border: none; background: #334155; color: white; } |
| input:focus { outline: 2px solid var(--accent); } |
| .loading { display: inline-block; width: 1rem; height: 1rem; border: 2px solid #fff; border-radius: 50%; border-top-color: transparent; animation: spin 1s linear infinite; } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| .error { color: #f87171; } |
| </style> |
| </head> |
| <body> |
| <header> |
| <span>🤖 Puter AI Chat</span> |
| <select id="model"> |
| <option value="claude-sonnet-4-5">Claude 4.5</option> |
| <option value="gemini-2.5-flash-lite">Gemini 2.5</option> |
| <option value="grok-4-1-fast">Grok 4.1</option> |
| <option value="gpt-4o">GPT-4o</option> |
| </select> |
| <button id="clear">🗑️ Очистить</button> |
| <button id="save">💾 Сохранить диалог</button> |
| <button id="load">📂 Загрузить диалог</button> |
| </header> |
| <div id="chat"></div> |
| <footer> |
| <input id="input" placeholder="Введите сообщение..." autocomplete="off"> |
| <button id="send">➤</button> |
| </footer> |
|
|
| <script> |
| const chat = document.getElementById('chat'); |
| const input = document.getElementById('input'); |
| const send = document.getElementById('send'); |
| const model = document.getElementById('model'); |
| const clear = document.getElementById('clear'); |
| const save = document.getElementById('save'); |
| const load = document.getElementById('load'); |
| let history = []; |
| |
| function addMsg(role, text, isError = false) { |
| const div = document.createElement('div'); |
| div.className = `msg ${role}`; |
| if (isError) div.style.backgroundColor = '#7f1a1a'; |
| div.textContent = text; |
| chat.appendChild(div); |
| chat.scrollTop = chat.scrollHeight; |
| } |
| |
| async function sendMessage() { |
| const text = input.value.trim(); |
| if (!text) return; |
| input.value = ''; |
| addMsg('user', text); |
| history.push({ role: 'user', content: text }); |
| |
| send.disabled = true; |
| const loadingDiv = document.createElement('div'); |
| loadingDiv.className = 'msg ai'; |
| loadingDiv.innerHTML = '<span class="loading"></span> Думаю...'; |
| chat.appendChild(loadingDiv); |
| chat.scrollTop = chat.scrollHeight; |
| |
| try { |
| const res = await fetch('/api/chat', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ model: model.value, messages: history }) |
| }); |
| if (!res.ok) { |
| const errData = await res.text(); |
| throw new Error(`Ошибка ${res.status}: ${errData}`); |
| } |
| const data = await res.json(); |
| chat.removeChild(loadingDiv); |
| addMsg('ai', data.reply); |
| history.push({ role: 'assistant', content: data.reply }); |
| } catch (e) { |
| chat.removeChild(loadingDiv); |
| addMsg('ai', `❌ ${e.message}`, true); |
| } finally { |
| send.disabled = false; |
| input.focus(); |
| } |
| } |
| |
| function clearChat() { |
| history = []; |
| chat.innerHTML = ''; |
| addMsg('ai', '🗑️ Чат очищен. Начинайте новый диалог.'); |
| } |
| |
| function saveDialog() { |
| const data = { model: model.value, history }; |
| localStorage.setItem('puter_chat_backup', JSON.stringify(data)); |
| addMsg('ai', '💾 Диалог сохранён в localStorage.', false); |
| } |
| |
| function loadDialog() { |
| const raw = localStorage.getItem('puter_chat_backup'); |
| if (!raw) { |
| addMsg('ai', '❌ Нет сохранённого диалога.', true); |
| return; |
| } |
| try { |
| const data = JSON.parse(raw); |
| model.value = data.model; |
| history = data.history; |
| chat.innerHTML = ''; |
| for (const msg of history) { |
| addMsg(msg.role, msg.content); |
| } |
| addMsg('ai', '📂 Диалог загружен.', false); |
| } catch(e) { |
| addMsg('ai', '❌ Ошибка загрузки.', true); |
| } |
| } |
| |
| send.onclick = sendMessage; |
| input.onkeydown = e => { if (e.key === 'Enter' && !e.shiftKey) sendMessage(); }; |
| clear.onclick = clearChat; |
| save.onclick = saveDialog; |
| load.onclick = loadDialog; |
| |
| addMsg('ai', '👋 Привет! Выбери модель и начни диалог. Диалог автоматически не сохраняется, но ты можешь сохранить его кнопкой.'); |
| </script> |
| </body> |
| </html> |