Spaces:
Sleeping
Sleeping
| <html lang="pl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>KAI STUDIO</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| background: #0a0a0a; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; | |
| } | |
| header { | |
| padding: 12px 20px; border-bottom: 1px solid #222; display: flex; | |
| justify-content: space-between; align-items: center; background: #111; | |
| } | |
| .logo { font-weight: 700; font-size: 18px; color: #4ade80; } | |
| .model-badge { | |
| font-size: 12px; padding: 4px 8px; border-radius: 6px; | |
| background: #1f2937; color: #9ca3af; | |
| } | |
| .model-badge.qwen { background: #3b82f6; color: white; } | |
| .model-badge.mistral { background: #8b5cf6; color: white; } | |
| #chat { | |
| flex: 1; overflow-y: auto; padding: 20px; display: flex; | |
| flex-direction: column; gap: 16px; | |
| } | |
| .msg { max-width: 80%; padding: 12px 16px; border-radius: 12px; line-height: 1.5; } | |
| .msg.user { background: #1e40af; align-self: flex-end; } | |
| .msg.assistant { background: #1f2937; align-self: flex-start; } | |
| .msg pre { background: #0a0a0a; padding: 12px; border-radius: 8px; overflow-x: auto; margin: 8px 0; } | |
| .msg code { font-family: 'Courier New', monospace; } | |
| footer { padding: 16px; border-top: 1px solid #222; background: #111; } | |
| .input-wrap { display: flex; gap: 8px; } | |
| #input { | |
| flex: 1; padding: 12px; border-radius: 8px; border: 1px solid #333; | |
| background: #0a0a0a; color: #e0e0e0; font-size: 16px; resize: none; | |
| } | |
| #send { | |
| padding: 0 20px; border-radius: 8px; border: none; | |
| background: #4ade80; color: #0a0a0a; font-weight: 600; cursor: pointer; | |
| } | |
| #send:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .typing { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #9ca3af; animation: blink 1s infinite; } | |
| @keyframes blink { 50% { opacity: 0; } } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo">KAI STUDIO</div> | |
| <div id="modelBadge" class="model-badge">Auto</div> | |
| </header> | |
| <div id="chat"></div> | |
| <footer> | |
| <div class="input-wrap"> | |
| <textarea id="input" rows="1" placeholder="Napisz wiadomość... Shift+Enter = nowa linia"></textarea> | |
| <button id="send">Wyślij</button> | |
| </div> | |
| </footer> | |
| <script> | |
| const chat = document.getElementById('chat'); | |
| const input = document.getElementById('input'); | |
| const send = document.getElementById('send'); | |
| const modelBadge = document.getElementById('modelBadge'); | |
| let messages = []; | |
| let isStreaming = false; | |
| // Auto-resize textarea | |
| input.addEventListener('input', () => { | |
| input.style.height = 'auto'; | |
| input.style.height = input.scrollHeight + 'px'; | |
| }); | |
| // Send on Enter, new line on Shift+Enter | |
| input.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' &&!e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| send.onclick = sendMessage; | |
| function addMessage(role, content) { | |
| const div = document.createElement('div'); | |
| div.className = `msg ${role}`; | |
| div.innerHTML = content.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>'); | |
| chat.appendChild(div); | |
| chat.scrollTop = chat.scrollHeight; | |
| return div; | |
| } | |
| async function sendMessage() { | |
| const text = input.value.trim(); | |
| if (!text || isStreaming) return; | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| addMessage('user', text); | |
| messages.push({role: 'user', content: text}); | |
| isStreaming = true; | |
| send.disabled = true; | |
| const assistantDiv = addMessage('assistant', '<span class="typing"></span>'); | |
| let fullResponse = ''; | |
| let currentModel = 'auto'; | |
| try { | |
| const res = await fetch('/v1/chat/completions', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ | |
| messages: messages, | |
| stream: true, | |
| max_tokens: 600 | |
| }) | |
| }); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| while (true) { | |
| const {done, value} = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value); | |
| const lines = chunk.split('\n'); | |
| for (const line of lines) { | |
| if (line.startsWith(': x-kai-model-used ')) { | |
| currentModel = line.split(' ')[2]; | |
| modelBadge.textContent = currentModel.toUpperCase(); | |
| modelBadge.className = `model-badge ${currentModel}`; | |
| } | |
| if (line.startsWith('data: ')) { | |
| const data = line.slice(6); | |
| if (data === '[DONE]') continue; | |
| try { | |
| const json = JSON.parse(data); | |
| const delta = json.choices[0]?.delta?.content || ''; | |
| fullResponse += delta; | |
| assistantDiv.innerHTML = fullResponse.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>'); | |
| chat.scrollTop = chat.scrollHeight; | |
| } catch (e) {} | |
| } | |
| } | |
| } | |
| messages.push({role: 'assistant', content: fullResponse}); | |
| } catch (err) { | |
| assistantDiv.innerHTML = `Error: ${err.message}`; | |
| } finally { | |
| isStreaming = false; | |
| send.disabled = false; | |
| input.focus(); | |
| } | |
| } | |
| // Focus on load | |
| input.focus(); | |
| </script> | |
| </body> | |
| </html> |