Skip to main content

CHAT_UI_HTML

Constant CHAT_UI_HTML 

Source
pub const CHAT_UI_HTML: &str = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>OxiBonsai Chat</title>\n  <style>\n    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n    :root {\n      --bg:        #0d1117;\n      --surface:   #161b22;\n      --border:    #30363d;\n      --accent:    #58a6ff;\n      --user-bg:   #1f6feb;\n      --bot-bg:    #21262d;\n      --text:      #c9d1d9;\n      --muted:     #8b949e;\n      --radius:    12px;\n      --font:      \'Segoe UI\', system-ui, sans-serif;\n    }\n\n    body {\n      background: var(--bg);\n      color: var(--text);\n      font-family: var(--font);\n      font-size: 15px;\n      line-height: 1.5;\n      height: 100dvh;\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n    }\n\n    #app {\n      width: 100%;\n      max-width: 760px;\n      height: 100%;\n      display: flex;\n      flex-direction: column;\n      padding: 0 12px 12px;\n    }\n\n    header {\n      padding: 18px 4px 12px;\n      border-bottom: 1px solid var(--border);\n      display: flex;\n      align-items: center;\n      gap: 10px;\n    }\n    header h1 { font-size: 1.1rem; font-weight: 600; color: var(--accent); }\n    header span { font-size: 0.8rem; color: var(--muted); }\n\n    #messages {\n      flex: 1;\n      overflow-y: auto;\n      padding: 16px 0;\n      display: flex;\n      flex-direction: column;\n      gap: 12px;\n      scrollbar-width: thin;\n      scrollbar-color: var(--border) transparent;\n    }\n\n    .msg {\n      display: flex;\n      gap: 10px;\n      max-width: 90%;\n    }\n    .msg.user  { align-self: flex-end; flex-direction: row-reverse; }\n    .msg.bot   { align-self: flex-start; }\n\n    .avatar {\n      width: 32px; height: 32px;\n      border-radius: 50%;\n      background: var(--border);\n      display: flex; align-items: center; justify-content: center;\n      font-size: 14px; flex-shrink: 0;\n    }\n    .msg.user .avatar { background: var(--user-bg); }\n\n    .bubble {\n      padding: 10px 14px;\n      border-radius: var(--radius);\n      white-space: pre-wrap;\n      word-break: break-word;\n      line-height: 1.55;\n    }\n    .msg.user .bubble { background: var(--user-bg); color: #fff; border-bottom-right-radius: 3px; }\n    .msg.bot  .bubble { background: var(--bot-bg);  border: 1px solid var(--border); border-bottom-left-radius: 3px; }\n\n    .thinking { color: var(--muted); font-style: italic; font-size: 0.9em; }\n\n    #input-row {\n      display: flex;\n      gap: 8px;\n      padding-top: 10px;\n      border-top: 1px solid var(--border);\n    }\n\n    #prompt {\n      flex: 1;\n      background: var(--surface);\n      border: 1px solid var(--border);\n      border-radius: var(--radius);\n      color: var(--text);\n      font-family: var(--font);\n      font-size: 15px;\n      padding: 10px 14px;\n      resize: none;\n      outline: none;\n      height: 48px;\n      max-height: 160px;\n      overflow-y: auto;\n      transition: border-color 0.15s;\n    }\n    #prompt:focus { border-color: var(--accent); }\n    #prompt::placeholder { color: var(--muted); }\n\n    #send-btn {\n      background: var(--accent);\n      color: #0d1117;\n      border: none;\n      border-radius: var(--radius);\n      padding: 0 20px;\n      font-size: 15px;\n      font-weight: 600;\n      cursor: pointer;\n      transition: opacity 0.15s;\n      white-space: nowrap;\n    }\n    #send-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n    #send-btn:not(:disabled):hover { opacity: 0.85; }\n\n    #error-bar {\n      background: #3d1f1f; border: 1px solid #6e2e2e;\n      color: #f28b82; border-radius: 6px;\n      padding: 8px 14px; font-size: 0.85rem;\n      display: none; margin-top: 8px;\n    }\n  </style>\n</head>\n<body>\n<div id=\"app\">\n  <header>\n    <div class=\"avatar\" style=\"background:var(--accent);color:#0d1117\">\u{1f33f}</div>\n    <div>\n      <h1>OxiBonsai</h1>\n      <span>Local LLM inference</span>\n    </div>\n  </header>\n\n  <div id=\"messages\" role=\"log\" aria-live=\"polite\"></div>\n  <div id=\"error-bar\"></div>\n\n  <div id=\"input-row\">\n    <textarea id=\"prompt\" placeholder=\"Send a message\u{2026}\" rows=\"1\" aria-label=\"Message input\"></textarea>\n    <button id=\"send-btn\" aria-label=\"Send\">Send</button>\n  </div>\n</div>\n\n<script>\n(function () {\n  \'use strict\';\n\n  const messagesEl = document.getElementById(\'messages\');\n  const promptEl   = document.getElementById(\'prompt\');\n  const sendBtn    = document.getElementById(\'send-btn\');\n  const errorBar   = document.getElementById(\'error-bar\');\n\n  // Conversation history sent to the API on each turn.\n  const history = [];\n\n  function showError(msg) {\n    errorBar.textContent = msg;\n    errorBar.style.display = \'block\';\n    setTimeout(() => { errorBar.style.display = \'none\'; }, 6000);\n  }\n\n  function addMessage(role, text) {\n    const isUser = role === \'user\';\n    const div = document.createElement(\'div\');\n    div.className = \'msg \' + (isUser ? \'user\' : \'bot\');\n    div.innerHTML =\n      \'<div class=\"avatar\">\' + (isUser ? \'\u{1f9d1}\' : \'\u{1f916}\') + \'</div>\' +\n      \'<div class=\"bubble\"></div>\';\n    div.querySelector(\'.bubble\').textContent = text;\n    messagesEl.appendChild(div);\n    messagesEl.scrollTop = messagesEl.scrollHeight;\n    return div.querySelector(\'.bubble\');\n  }\n\n  function setThinking() {\n    const div = document.createElement(\'div\');\n    div.className = \'msg bot\';\n    div.id = \'thinking\';\n    div.innerHTML = \'<div class=\"avatar\">\u{1f916}</div><div class=\"bubble thinking\">Thinking\u{2026}</div>\';\n    messagesEl.appendChild(div);\n    messagesEl.scrollTop = messagesEl.scrollHeight;\n  }\n\n  function removeThinking() {\n    const el = document.getElementById(\'thinking\');\n    if (el) el.remove();\n  }\n\n  async function sendMessage() {\n    const text = promptEl.value.trim();\n    if (!text) return;\n\n    promptEl.value = \'\';\n    promptEl.style.height = \'48px\';\n    sendBtn.disabled = true;\n    errorBar.style.display = \'none\';\n\n    history.push({ role: \'user\', content: text });\n    addMessage(\'user\', text);\n    setThinking();\n\n    try {\n      const resp = await fetch(\'/v1/chat/completions\', {\n        method: \'POST\',\n        headers: { \'Content-Type\': \'application/json\' },\n        body: JSON.stringify({\n          messages: history,\n          max_tokens: 512,\n          temperature: 0.7,\n          stream: false\n        })\n      });\n\n      removeThinking();\n\n      if (!resp.ok) {\n        const errText = await resp.text().catch(() => resp.statusText);\n        showError(\'API error \' + resp.status + \': \' + errText);\n        history.pop();\n        return;\n      }\n\n      const data = await resp.json();\n      const reply = data?.choices?.[0]?.message?.content ?? \'(empty response)\';\n      history.push({ role: \'assistant\', content: reply });\n      addMessage(\'assistant\', reply);\n    } catch (err) {\n      removeThinking();\n      showError(\'Request failed: \' + err.message);\n      history.pop();\n    } finally {\n      sendBtn.disabled = false;\n      promptEl.focus();\n    }\n  }\n\n  sendBtn.addEventListener(\'click\', sendMessage);\n\n  promptEl.addEventListener(\'keydown\', function (e) {\n    if (e.key === \'Enter\' && !e.shiftKey) {\n      e.preventDefault();\n      sendMessage();\n    }\n  });\n\n  // Auto-grow textarea up to max-height\n  promptEl.addEventListener(\'input\', function () {\n    this.style.height = \'48px\';\n    this.style.height = Math.min(this.scrollHeight, 160) + \'px\';\n  });\n\n  promptEl.focus();\n})();\n</script>\n</body>\n</html>\n";
Expand description

The single-page chat UI, embedded from assets/chat.html at compile time.

This is a pure HTML/CSS/JS application with no external CDN dependencies. It communicates with the inference server via POST /v1/chat/completions.