Spaces:
Running
Running
Download index.html from LessThanThreeAI/Humanlike-Chat: direct link, hf CLI and curl.
- Browser
- Download file 11.4 kB
-
https://huggingface.co/spaces/LessThanThreeAI/Humanlike-Chat/resolve/main/index.html
- Command line
-
hf download hf://spaces/LessThanThreeAI/Humanlike-Chat/index.html
-
curl -L -o index.html https://huggingface.co/spaces/LessThanThreeAI/Humanlike-Chat/resolve/main/index.html
11.4 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Humanlike Chat</title> | |
| <style> | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| color: #111827; | |
| background: #fff; | |
| font: 15px/1.5 Inter, ui-sans-serif, system-ui, sans-serif; | |
| } | |
| main { | |
| display: grid; | |
| grid-template-rows: auto 1fr auto auto; | |
| width: min(880px, 100%); | |
| height: 100dvh; | |
| margin: auto; | |
| } | |
| header { padding: 14px 16px; border-bottom: 1px solid #e5e7eb; } | |
| h1 { margin: 0; font-size: 15px; font-weight: 600; } | |
| header p { margin: 4px 0 0; color: #6b7280; font-size: 12px; } | |
| header a { color: #4f46e5; } | |
| code { color: #374151; overflow-wrap: anywhere; } | |
| details { margin-top: 10px; } | |
| summary { color: #6b7280; font-size: 12px; cursor: pointer; } | |
| summary:hover { color: #4f46e5; } | |
| .field { margin-top: 10px; } | |
| .field label { display: block; margin-bottom: 4px; color: #6b7280; font-size: 12px; } | |
| #system { | |
| width: 100%; | |
| min-height: 76px; | |
| padding: 8px 10px; | |
| border: 1px solid #d1d5db; | |
| border-radius: 8px; | |
| font: inherit; | |
| font-size: 13px; | |
| resize: vertical; | |
| } | |
| #reasoning { | |
| padding: 6px 8px; | |
| border: 1px solid #d1d5db; | |
| border-radius: 8px; | |
| background: #fff; | |
| font: inherit; | |
| font-size: 13px; | |
| } | |
| #chat { padding: 20px 16px; overflow-y: auto; } | |
| .message { | |
| max-width: 80%; | |
| margin: 0 0 14px; | |
| padding: 10px 14px; | |
| border-radius: 12px; | |
| line-height: 1.55; | |
| white-space: pre-wrap; | |
| overflow-wrap: anywhere; | |
| } | |
| .assistant { background: #f3f4f6; } | |
| .user { margin-left: auto; color: #fff; background: #4f46e5; } | |
| .error { color: #991b1b; background: #fee2e2; } | |
| .pending { color: #6b7280; font-style: italic; } | |
| .reasoning { | |
| margin-bottom: 8px; | |
| padding-left: 9px; | |
| border-left: 2px solid #d1d5db; | |
| color: #6b7280; | |
| font-size: 13px; | |
| font-style: italic; | |
| } | |
| form { | |
| display: grid; | |
| grid-template-columns: 1fr auto; | |
| gap: 10px; | |
| padding: 14px 16px; | |
| border-top: 1px solid #e5e7eb; | |
| } | |
| textarea { | |
| min-height: 60px; | |
| padding: 10px 12px; | |
| border: 1px solid #d1d5db; | |
| border-radius: 8px; | |
| font: inherit; | |
| resize: vertical; | |
| } | |
| button { | |
| padding: 10px 20px; | |
| color: #fff; | |
| background: #4f46e5; | |
| border: 0; | |
| border-radius: 8px; | |
| font: inherit; | |
| font-weight: 600; | |
| cursor: pointer; | |
| align-self: end; | |
| } | |
| button:disabled { opacity: 0.5; cursor: wait; } | |
| textarea:focus, button:focus-visible, select:focus-visible, summary:focus-visible { | |
| outline: 2px solid #6366f1; | |
| outline-offset: 2px; | |
| } | |
| #status { min-height: 32px; margin: 0; padding: 0 16px 12px; color: #6b7280; font-size: 12px; } | |
| #status.busy::before { | |
| content: ""; | |
| display: inline-block; | |
| width: 10px; | |
| height: 10px; | |
| margin-right: 8px; | |
| vertical-align: -1px; | |
| border: 2px solid #d1d5db; | |
| border-top-color: #4f46e5; | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| @media (prefers-color-scheme: dark) { | |
| :root { color-scheme: dark; } | |
| body { color: #f3f4f6; background: #111827; } | |
| header, form { border-color: #374151; } | |
| header p, #status, .pending, summary, .field label, .reasoning { color: #9ca3af; } | |
| summary:hover { color: #818cf8; } | |
| code { color: #d1d5db; } | |
| #system, textarea, #reasoning { color: #f3f4f6; background: #1f2937; border-color: #4b5563; } | |
| .assistant { background: #1f2937; } | |
| .error { color: #fecaca; background: #450a0a; } | |
| .reasoning { border-color: #4b5563; } | |
| #status.busy::before { border-color: #4b5563; border-top-color: #818cf8; } | |
| } | |
| @media (max-width: 620px) { | |
| .message { max-width: 92%; } | |
| form { grid-template-columns: 1fr; } | |
| button { width: 100%; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <header> | |
| <h1>Humanlike Chat</h1> | |
| <p> | |
| Live demo of the hosted endpoint <code>https://api.lessthanthreeai.com/v1</code>. | |
| </p> | |
| <details> | |
| <summary>Settings</summary> | |
| <div class="field"> | |
| <label for="system">System prompt</label> | |
| <textarea id="system" | |
| placeholder="Optional. Sets identity, setting, or relationship for the conversation."></textarea> | |
| </div> | |
| <div class="field"> | |
| <label for="reasoning">Thinking</label> | |
| <select id="reasoning"> | |
| <option value="off">Off (fastest, the default)</option> | |
| <option value="low">Low</option> | |
| <option value="medium">Medium</option> | |
| <option value="high">High</option> | |
| </select> | |
| </div> | |
| </details> | |
| </header> | |
| <div id="chat" aria-live="polite"></div> | |
| <form id="form"> | |
| <textarea id="prompt" required aria-label="Message" placeholder="Type a message"></textarea> | |
| <button id="send" type="submit">Send</button> | |
| </form> | |
| <p id="status" aria-live="polite"></p> | |
| </main> | |
| <script> | |
| const API = "https://api.lessthanthreeai.com/v1"; | |
| const MODEL = "gemma-4-31b-humanlike-chat"; | |
| const INTRO = "Say something. The first reply can take a few minutes while the inference worker starts."; | |
| const MAX_TOKENS = { off: 512, low: 4096, medium: 4096, high: 8192 }; | |
| const chat = document.querySelector("#chat"); | |
| const form = document.querySelector("#form"); | |
| const prompt = document.querySelector("#prompt"); | |
| const send = document.querySelector("#send"); | |
| const status = document.querySelector("#status"); | |
| const system = document.querySelector("#system"); | |
| const reasoning = document.querySelector("#reasoning"); | |
| // One conversation per page load. Reloading starts a fresh session. | |
| const messages = []; | |
| system.value = localStorage.getItem("systemPrompt") || ""; | |
| reasoning.value = localStorage.getItem("thinkingLevel") in MAX_TOKENS ? localStorage.getItem("thinkingLevel") : "off"; | |
| system.addEventListener("input", () => localStorage.setItem("systemPrompt", system.value)); | |
| reasoning.addEventListener("change", () => localStorage.setItem("thinkingLevel", reasoning.value)); | |
| function addMessage(role, text) { | |
| const element = document.createElement("div"); | |
| element.className = `message ${role}`; | |
| element.textContent = text; | |
| chat.append(element); | |
| chat.scrollTop = chat.scrollHeight; | |
| return element; | |
| } | |
| function addAssistantMessage() { | |
| const element = document.createElement("div"); | |
| element.className = "message assistant"; | |
| const thoughts = document.createElement("div"); | |
| thoughts.className = "reasoning"; | |
| thoughts.hidden = true; | |
| const content = document.createElement("div"); | |
| content.className = "pending"; | |
| content.textContent = "waiting for the first token"; | |
| element.append(thoughts, content); | |
| chat.append(element); | |
| chat.scrollTop = chat.scrollHeight; | |
| return { element, thoughts, content }; | |
| } | |
| // Server-sent events arrive as "data: {...}" chunks. The endpoint may also send | |
| // ": status={...}" comment lines while a cold worker starts. | |
| function applyEvent(line, target) { | |
| if (line.startsWith(":")) { | |
| const payload = line.slice(1).trim().replace(/^status=/, ""); | |
| try { | |
| const state = JSON.parse(payload); | |
| if (state.message) status.textContent = state.message; | |
| } catch {} | |
| return; | |
| } | |
| if (!line.startsWith("data:")) return; | |
| const payload = line.slice(5).trim(); | |
| if (!payload || payload === "[DONE]") return; | |
| const delta = (JSON.parse(payload).choices || [{}])[0].delta || {}; | |
| if (delta.reasoning) { | |
| target.thoughts.hidden = false; | |
| target.thoughts.textContent += delta.reasoning; | |
| chat.scrollTop = chat.scrollHeight; | |
| } | |
| if (delta.content) { | |
| if (target.content.classList.contains("pending")) { | |
| target.content.classList.remove("pending"); | |
| target.content.textContent = ""; | |
| status.textContent = ""; | |
| } | |
| target.content.textContent += delta.content; | |
| chat.scrollTop = chat.scrollHeight; | |
| } | |
| } | |
| form.addEventListener("submit", async (event) => { | |
| event.preventDefault(); | |
| const text = prompt.value.trim(); | |
| if (!text) return; | |
| messages.push({ role: "user", content: text }); | |
| addMessage("user", text); | |
| prompt.value = ""; | |
| send.disabled = true; | |
| status.classList.add("busy"); | |
| status.textContent = "Starting the model. A cold start can take a few minutes."; | |
| const target = addAssistantMessage(); | |
| const effort = reasoning.value; | |
| const systemPrompt = system.value.trim(); | |
| const request = systemPrompt | |
| ? [{ role: "system", content: systemPrompt }, ...messages] | |
| : messages; | |
| const chatTemplateKwargs = effort === "off" | |
| ? { enable_thinking: false, preserve_thinking: false } | |
| : { enable_thinking: true, reasoning_effort: effort, preserve_thinking: false }; | |
| try { | |
| const response = await fetch(`${API}/chat/completions`, { | |
| method: "POST", | |
| headers: { "content-type": "application/json" }, | |
| body: JSON.stringify({ | |
| model: MODEL, | |
| messages: request, | |
| stream: true, | |
| max_tokens: MAX_TOKENS[effort], | |
| chat_template_kwargs: chatTemplateKwargs, | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const body = await response.text(); | |
| let message = body; | |
| try { message = JSON.parse(body).error?.message || body; } catch {} | |
| throw new Error(message || `HTTP ${response.status}`); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ""; | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| buffer += decoder.decode(value || new Uint8Array(), { stream: !done }); | |
| const lines = buffer.split("\n"); | |
| buffer = lines.pop() || ""; | |
| for (const line of lines) applyEvent(line.trim(), target); | |
| if (done) break; | |
| } | |
| if (buffer.trim()) applyEvent(buffer.trim(), target); | |
| target.content.classList.remove("pending"); | |
| if (!target.content.textContent) target.content.textContent = "(no final answer returned)"; | |
| messages.push({ role: "assistant", content: target.content.textContent }); | |
| status.textContent = "Complete. Later replies are fast while the worker stays warm."; | |
| } catch (error) { | |
| target.content.classList.remove("pending"); | |
| target.element.classList.add("error"); | |
| target.content.textContent = `Request failed: ${error.message}`; | |
| status.textContent = "The inference service did not complete the request."; | |
| } finally { | |
| status.classList.remove("busy"); | |
| send.disabled = false; | |
| prompt.focus(); | |
| } | |
| }); | |
| addMessage("assistant", INTRO); | |
| prompt.focus(); | |
| </script> | |
| </body> | |
| </html> | |