weazel/houseai-modularizer
0
1 2<!doctype html>3<html lang="en">4<head>5 <meta charset="utf-8" />6 <meta name="viewport" content="width=device-width, initial-scale=1" />7 <title>HouseAI — ultra-minimal</title>8 9 <!-- iOS standalone web app -->10 <meta name="apple-mobile-web-app-capable" content="yes">11 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">12 <meta name="apple-mobile-web-app-title" content="HouseAI">13 <link rel="apple-touch-icon" href="/icons/apple-touch-icon-180.png">14 15 <!-- External Dependencies -->16 <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script>17 <!-- Module Imports -->18 <script type="module" src="./config.js"></script>19 <script type="module" src="./utils.js"></script>20 <script type="module" src="./api.js"></script>21 <script type="module" src="./parser.js"></script>22 <script type="module" src="./renderer.js"></script>23<style>24 :root { --w: 720px; }25 26 /* Base layout */27 html, body { height: 100%; }28 body { margin: 0; font: 16px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif; background:#f7f7f7; padding-bottom: env(safe-area-inset-bottom); }29 textarea, input, button { font-size: 16px; } /* prevent iOS zoom */30 .app { max-width: var(--w); margin: 0 auto; padding: 12px; min-height: 100dvh; display:flex; flex-direction:column; gap: 10px; }31 32 h1 { font-size: 18px; margin: 8px 0 6px; font-weight: 600; }33 .top { display:flex; align-items:center; justify-content:space-between; gap:12px; }34 .status { font-size:12px; color:#555; display:flex; align-items:center; gap:8px; }35 .dot { width:8px; height:8px; border-radius:50%; background:#bbb; display:inline-block; }36 .ok .dot { background:#22c55e; }37 .down .dot { background:#ef4444; }38 39 .card { background:#fff; border:1px solid #e5e5e5; border-radius: 10px; box-shadow: 0 1px 2px rgba(0,0,0,.04); }40 41 /* Messages area flexes */42 .messages { flex:1; min-height: 0; overflow:auto; padding: 10px; display: flex; flex-direction: column; gap: 8px; overscroll-behavior-y: contain; -webkit-overflow-scrolling: touch; }43 .msg { max-width: 85%; padding: 8px 10px; border:1px solid #e6e6e6; border-radius: 10px; background:#fff; }44 .me { align-self: flex-end; background:#e9f3ff; border-color:#d6e9ff; }45 .ai { align-self: flex-start; background:#fcfcfc; }46 .meta { font-size: 11px; color:#888; margin-bottom: 2px; }47 48 /* Sticky composer with safe-area padding */49 form#composer { position: sticky; bottom: 0; padding: 8px; border-top:1px solid #eee; padding-bottom: calc(8px + env(safe-area-inset-bottom)); background:#fff; }50 textarea {51 width: 100%; min-height: 42px; resize: none; /* auto-grow handles height */52 padding: 10px 12px; border:1px solid #ddd; border-radius: 8px; box-sizing: border-box;53 overflow: hidden; max-height: 40dvh; line-height: 1.45;54 }55 .hint { font-size: 12px; color:#777; padding: 0 2px 8px; }56 57 /* Markdown niceties */58 .msg h1,.msg h2,.msg h3{margin:.2em 0 .4em}59 .msg ul{margin:.3em 0 .6em .8em}60 .msg code{font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.95em; padding: 0 .25em; border:1px solid #eee; border-radius:4px; background:#fafafa;}61 .msg pre{overflow:auto; padding:.6em; border:1px solid #eee; border-radius:8px; background:#fafafa;}62 details.tool { margin-top:.4em; }63 details.tool summary { cursor:pointer; user-select:none; }64 65 /* Debug nested items (tiny touch) */66 details.sub { margin: .25em 0 .25em 1em; }67 68 /* Sub-detail content smaller + wrapping */69 details.sub pre {70 font-size: 12.5px; /* smaller text */71 line-height: 1.4;72 white-space: pre-wrap; /* preserve newlines, wrap long lines */73 word-break: break-word; /* break long tokens if needed */74 overflow-wrap: anywhere; /* extra safety for long JSON keys */75 max-height: 320px; /* keep it neat */76 overflow-y: auto; /* vertical scroll only */77 overflow-x: hidden; /* no horizontal scroll */78 }79 80 details.sub summary { font-size: 14px; }81 82 /* Mobile ergonomics */83 @media (max-width: 600px) {84 body { font-size: 17px; }85 h1 { font-size: 18px; }86 .msg { padding: 8px 10px; }87 textarea { padding: 12px 14px; }88 }89 90 </style>91</head>92<body>93 94 <!-- <div id="chat-header" style="display:flex;justify-content:flex-end;padding:4px 8px;">95 <button onclick="startNewChat()" style="font-size:0.8em;opacity:0.6;border:none;background:none;cursor:pointer;">96 New chat97 </button>98 </div> -->99 100 <div class="app">101 <!-- subtle top-right new chat -->102 <div style="display:flex;justify-content:flex-end;padding:4px 8px;position:sticky;top:0;background:#f7f7f7;z-index:1;">103 <button onclick="startNewChat()" style="font-size:0.8em;opacity:0.6;border:none;background:none;cursor:pointer;">104 New chat105 </button>106 </div>107 108 <div class="top">109 <h1>Wonka, De HouseAI V1.4</h1>110 <div id="status" class="status" title="Back-end health">111 <span class="dot" aria-hidden="true"></span>112 <span><strong>API:</strong> checking…</span>113 </div>114 </div>115 116 <div id="messages" class="card messages" aria-live="polite"></div>117 118 <form id="composer" class="card" autocomplete="off">119 <div class="hint">Enter to send • Shift+Enter for newline</div>120 <div style="padding:0 8px 8px;">121 <textarea id="input" placeholder="Ask or instruct HouseAI…" rows="1" enterkeyhint="send" autocapitalize="sentences" spellcheck="true"></textarea>122 </div>123 </form>124 </div>125 126 <script type="module">127 import { CONFIG } from './config.js';128 import { newCid, getCid, autoGrow, timeStr } from './utils.js';129 import { sendMessageToAPI, fetchHealthStatus, fetchChangelog } from './api.js';130 import { render } from './renderer.js';131 132 const $ = sel => document.querySelector(sel);133 134 const messagesEl = $('#messages');135 const inputEl = $('#input');136 137 let CONVERSATION_ID = getCid();138 console.debug("Conversation ID initialised:", CONVERSATION_ID);139 140 let convo = [];141 142 function push(role, content) {143 convo.push({ role, content, t: Date.now() });144 render(convo, messagesEl);145 }146 147 async function sendMessage(text) {148 // Client-side command interception149 if (text.trim().toLowerCase() === "!changelog") {150 try {151 const { changelog } = await fetchChangelog();152 push("assistant", "📜 Backend changelog:\n\n" + changelog);153 } catch (err) {154 push("assistant", "⚠️ Could not fetch changelog: " + (err?.message || err));155 } finally {156 inputEl.focus({ preventScroll: true });157 }158 return;159 }160 161 // Normal chat flow162 push("user", text);163 try {164 const data = await sendMessageToAPI(text, CONVERSATION_ID);165 166 if (data.conversation_id && data.conversation_id !== CONVERSATION_ID) {167 CONVERSATION_ID = data.conversation_id;168 localStorage.setItem("wonka.cid", CONVERSATION_ID);169 const url = new URL(window.location.href);170 url.searchParams.set("cid", CONVERSATION_ID);171 history.replaceState({}, "", url.toString());172 }173 174 const reply = (data.result ?? data.reply ?? data.message ?? data.content ?? JSON.stringify(data));175 push("assistant", String(reply));176 } catch (err) {177 push("assistant", "⚠️ Error: " + (err && err.message ? err.message : err));178 } finally {179 inputEl.focus({ preventScroll: true });180 }181 }182 183 function startNewChat() {184 const cid = newCid();185 localStorage.setItem('wonka.cid', cid);186 const url = new URL(window.location.href);187 url.searchParams.set('cid', cid);188 history.pushState({}, '', url.toString());189 CONVERSATION_ID = cid;190 191 // Reset UI and in-memory convo192 convo = [];193 messagesEl.innerHTML = '';194 inputEl.value = '';195 inputEl.focus({ preventScroll: true });196 }197 // Submit handling198 $('#composer').addEventListener('submit', (e) => {199 e.preventDefault();200 const text = (inputEl.value || '').trim();201 if (!text) return;202 inputEl.value = '';203 autoGrow(inputEl);204 sendMessage(text);205 });206 207 inputEl.addEventListener('keydown', (e) => {208 if (e.isComposing) return;209 if (e.key === 'Enter' && !e.shiftKey) {210 e.preventDefault();211 $('#composer').dispatchEvent(new Event('submit'));212 }213 });214// Textarea auto-grow215 ['input', 'change'].forEach(ev => inputEl.addEventListener(ev, () => autoGrow(inputEl)));216 window.addEventListener('resize', () => autoGrow(inputEl));217 autoGrow(inputEl);218 // Health check219 const statusEl = $('#status');220 async function refreshHealth() {221 try {222 const { status, version } = await fetchHealthStatus();223 const ok = String(status).toLowerCase() === 'ok';224 statusEl.classList.toggle('ok', ok);225 statusEl.classList.toggle('down', !ok);226 statusEl.lastElementChild.innerHTML = `<strong>API:</strong> ${version || 'unknown'}`;227 statusEl.title = `Back-end status: ${status}${version ? ' • ' + version : ''}`;228 } catch (error) {229 console.error('Health check failed:', error);230 statusEl.classList.remove('ok');231 statusEl.classList.add('down');232 statusEl.lastElementChild.innerHTML = `<strong>API:</strong> offline`;233 statusEl.title = 'Back-end unreachable';234 }235 }236 refreshHealth();237 setInterval(refreshHealth, CONFIG.HEALTH_CHECK_INTERVAL);238</script>239</body>240</html>241 