1/* Hythm Studio â client */ 2(() => { 3 const $ = (id) => document.getElementById(id); 4 const LUCIDE = "https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/lucide.min.js"; 5 const MODE_HINTS = { 6 fast: "تص٠ÙÙ Ø³Ø±ÙØ¹ ÙØ£ÙÙÙ", 7 balanced: "Ù ÙÙØ¹ Ù ØªÙØ§Ù Ù ÙØ±Ø§Ø¦Ø¹", 8 max: "تØÙØ© خراÙÙØ© · ÙÙÙÙØ± بع٠٠ÙÙØ¨Ø¯Ø¹ Ø¨ÙØ§ ØØ¯Ùد" 9 }; 10 const CATEGORY_NAMES = { phishing: "تصÙÙØ¯", fraud: "تزÙÙØ±", scam: "Ø§ØØªÙاÙ", adult: "Ù ØØªÙÙ ÙÙØ¨Ø§ÙغÙÙ", gambling: "Ù٠ار", "illegal-goods": "Ù Ù ÙÙØ¹Ø§Øª", malware: "اختراÙ", extremism: "تطرÙ", model: "تÙÙÙÙ Hythm" }; 11 12 const els = { 13 app: $("app"), messages: $("messages"), empty: $("empty"), composer: $("composer"), input: $("input"), 14 sendBtn: $("sendBtn"), sendCost: $("sendCost"), frame: $("frame"), frameWrap: $("frameWrap"), 15 codeView: $("codeView"), code: $("code"), placeholder: $("placeholder"), statusPill: $("statusPill"), 16 statusText: $("statusText"), toast: $("toast"), creditPill: $("creditPill"), creditValue: $("creditValue"), 17 editContext: $("editContext"), editTarget: $("editTarget"), modeHint: $("modeHint"), photoCount: $("photoCount") 18 }; 19 20 const storage = { 21 get: (k) => { try { return localStorage.getItem(k); } catch { return null; } }, 22 set: (k, v) => { try { localStorage.setItem(k, v); } catch {} }, 23 del: (k) => { try { localStorage.removeItem(k); } catch {} }, 24 json: (k) => { try { return JSON.parse(localStorage.getItem(k)); } catch { return null; } } 25 }; 26 27 /* ---------------- State ---------------- */ 28 let cfg = { modes: { fast: { cost: 1, editCost: 1 }, balanced: { cost: 3, editCost: 2 }, max: { cost: 8, editCost: 4 } } }; 29 let baseCss = "", runtimeJs = ""; 30 let uid = storage.get("hythm.uid"); 31 let me = null, siteCredits = 0; 32 let mode = storage.get("hythm.mode") || "balanced"; 33 let style = ""; 34 let model = ["ultra", "x", "nova", "code"].includes(storage.get("hythm.model")) ? storage.get("hythm.model") : "omni"; 35 const MODEL_NAMES = { omni: "Mini Omni 1.01", ultra: "Omni Ultra 2.0", x: "Omni X 3.0", nova: "Omni Nova 4.0", code: "Omni Code 1.0", canvas: "Canvas", forge: "Forge", muse: "Muse" }; 36 function setModel(m) { 37 // paid models open the plans page for free accounts 38 if ((m === "nova" || m === "code") && me && (!me.plan || !(me.plan.models || []).includes(m)) && window.HythmPlans) { HythmPlans.open(m); return; } 39 model = ["ultra", "x", "nova", "code"].includes(m) ? m : "omni"; 40 els.composer && els.composer.classList.toggle("is-nova", model === "nova"); 41 els.composer && els.composer.classList.toggle("is-ultra", model === "ultra"); 42 els.composer && els.composer.classList.toggle("is-x", model === "x"); 43 try { updateSendCost(); } catch {} 44 const pl = document.getElementById("pickerModel"); if (pl) pl.textContent = MODEL_NAMES[model]; 45 storage.set("hythm.model", model); 46 document.querySelectorAll(".model-card").forEach((b) => b.setAttribute("aria-checked", String(b.dataset.model === model))); 47 } 48 document.querySelectorAll(".model-card").forEach((b) => b.addEventListener("click", () => setModel(b.dataset.model))); 49 /* ---- Task: auto | design | chat | image ---- */ 50 let task = "auto"; // Hythm figures out what you want (see /api/intent) 51 let fullSite = false; 52 const TASK_NAMES = { auto: "تÙÙØ§Ø¦Ù", design: "تص٠ÙÙ ", chat: "Ù ØØ§Ø¯Ø«Ø©", image: "ØµÙØ±Ø©" }; 53 const DESIGN_RE = /(\bwebsite\b|\bweb ?site\b|\bweb ?page\b|landing ?page|home ?page|\bui\b|\bux\b|dashboard|\bportfolio\b|online (store|shop)|e-?commerce|\bmock-?up\b|app (screen|interface|ui)|admin panel|design (a|an|me|my)\b|build (a|an|me|my) (site|page|web|landing|store|portfolio|dashboard)|Ù ÙÙØ¹|ØµÙØ[Ø©Ù]|ÙØ§Ø¬Ù[Ø©Ù]|تص٠ÙÙ |ص٠٠|ص٠ÙÙ |Ø¯Ø§Ø´Ø¨ÙØ±Ø¯|ÙÙØØ© تØÙÙ |٠تجر (Ø¥|ا)ÙÙØªØ±ÙÙÙ|Ø¨ÙØ±ØªÙÙÙÙÙ|ÙØ§ÙØ¯ÙØ¬|ÙØ¨ÙØ·)/i; 54 const isDesignText = (t) => task === "design" || (task === "auto" && DESIGN_RE.test(t)); 55 function setTask(t) { 56 task = ["auto", "design", "chat", "image"].includes(t) ? t : "auto"; 57 storage.set("hythm.task", task); 58 document.querySelectorAll(".task-opt").forEach((b) => b.classList.toggle("on", b.dataset.task === task)); 59 const pt = document.getElementById("pickerTask"); if (pt) pt.textContent = (window.HythmI18n ? HythmI18n.t : (x) => x)(TASK_NAMES[task]); 60 const io = document.getElementById("imgOpts");
60 if (io) io.hidden = task !== "image"; 61 if (task === "image") setTimeout(() => { try { pollEngine(); } catch {} }, 0); 62 const fb = document.getElementById("fullSiteBtn"); if (fb) fb.hidden = task === "chat" || task === "image"; 63 els.composer && els.composer.setAttribute("data-task", task); 64 const ib = document.getElementById("imgModeBtn"); if (ib) ib.setAttribute("aria-pressed", String(task === "image")); 65 const bar = document.getElementById("imgModeBar"); if (bar) bar.hidden = task !== "image"; 66 try { updateSendCost(); } catch {} 67 } 68 document.querySelectorAll(".task-opt").forEach((b) => b.addEventListener("click", () => setTask(b.dataset.task))); 69 setModel(model); 70 const emptySession = () => ({ log: [], versions: [], current: -1, prior: [] }); 71 let projects = storage.json("hythm.projects") || []; 72 let projectId = storage.get("hythm.currentProject"); 73 // migrate the old single session into a project 74 const legacy = storage.json("hythm.session"); 75 if (legacy && legacy.log?.length && !projects.length) { 76 projectId = "p" + Date.now(); 77 projects = [{ id: projectId, title: legacy.log.find((e) => e.type === "user")?.text.slice(0, 48) || "Ù Ø´Ø±ÙØ¹", t: Date.now() }]; 78 storage.set("hythm.p." + projectId, JSON.stringify(legacy)); 79 storage.set("hythm.projects", JSON.stringify(projects)); 80 storage.del("hythm.session"); 81 } 82 if (projectId && !projects.some((p) => p.id === projectId)) projectId = null; 83 let session = (projectId && storage.json("hythm.p." + projectId)) || emptySession(); 84 let editOff = false; // user chose "new design" instead of editing current 85 let controller = null; 86 const imageCache = new Map(); // query -> [{url,...}] 87 const pendingQueries = new Set(); 88 let currentTab = "preview"; 89 90 const THEME_META = { 91 luxe: ["ÙØ®Ù Ø°ÙØ¨Ù", "#0d0c0a", "#c9a45c"], noir: ["سÙÙ٠ائÙ", "#08080a", "#e50914"], aurora: ["Ø£ÙØ±Ùرا", "#07071a", "#8b5cf6"], 92 lumen: ["Ø¶ÙØ¡ سائÙ", "#03040a", "#8b9bff"], minimal: ["Ø¨Ø³ÙØ·", "#ffffff", "#0c0a09"], editorial: ["Ù Ø¬ÙØ©", "#f4efe6", "#c2371b"], 93 playful: ["٠رØ", "#fff8f0", "#ff5c3a"], brutal: ["Ø¨Ø±ÙØªØ§Ù", "#ffe45c", "#111111"], nature: ["Ø·Ø¨ÙØ¹Ø©", "#f4f1e9", "#2f5d3a"], 94 tech: ["تÙÙÙ", "#050b14", "#22e1ff"], corporate: ["رس٠Ù", "#f7f9fc", "#1d4ed8"], rose: ["ÙØ±Ø¯Ù Ø£ÙÙÙ", "#fbf5f3", "#7a1f3d"], 95 sunset: ["ØºØ±ÙØ¨", "#1a0b12", "#ff7a45"], ocean: ["Ù ØÙØ·", "#f2f8fb", "#0077b6"], mono: ["Ø£Ø¨ÙØ¶ ÙØ£Ø³Ùد", "#0a0a0a", "#f5f5f5"], cyber: ["Ø³Ø§ÙØ¨Ø±", "#0b0414", "#ff2bd6"], 96 pastel: ["باستÙÙ", "#fdf7ff", "#9b6bff"], forest: ["غابة", "#0e1511", "#9fd356"], desert: ["ØµØØ±Ø§Ø¡", "#f6ecdf", "#c0692f"], neon: ["ÙÙÙÙ", "#050505", "#c6ff00"], 97 royal: ["Ù ÙÙÙ", "#0b0a1f", "#d4af37"], candy: ["ØÙÙÙ", "#fff5fa", "#ff3d9a"], arctic: ["ÙØ·Ø¨Ù", "#f5fbff", "#0ea5e9"], vintage: ["عتÙÙ", "#f3e9d2", "#9c3d2e"], 98 matrix: ["٠اترÙÙØ³", "#020602", "#39ff14"], terracotta: ["ÙØ®ÙارÙ", "#fbf4ee", "#c65d3b"], galaxy: ["Ù Ø¬Ø±ÙØ©", "#05030f", "#a855f7"], paper: ["ÙØ±ÙÙ", "#fafaf7", "#1d1c19"], 99 electric: ["ÙÙØ±Ø¨Ø§Ø¦Ù", "#060818", "#3b5bff"], zen: ["زÙÙ", "#f4f2ec", "#5d6b4f"] 100 }; 101 const themeOf = (code) => (code.match(/data-theme=["']([\w-]+)/) || [])[1] || "minimal"; 102 const swatchHTML = (k) => `<span class="sw2" style="--a:${THEME_META[k][1]};--b:${THEME_META[k][2]}"></span>`; 103 // fonts live inside the code as <html data-fonts="â¦"> so every export/variation keeps them 104 let pendingType = null; 105 function withFonts(code, typeStyle, force) { 106 if (!window.hythmPickFonts) return code; 107 if (!force && /data-fonts=/.test(code)) return code; 108 const f = window.hythmPickFonts(themeOf(code), typeStyle); 109 const val = [f.d, f.b, f.ad, f.ab].join("|"); 110 if (/data-fonts=["'][^"']*["']/.test(code)) return code.replace(/data-fonts=["'][^"']*["']/, `data-fonts="${val}"`); 111 return code.replace(/<html\b/i, `<html data-fonts="${val}"`); 112 } 113 const fontsOf = (code) => ((code.match(/data-fonts=["']([^"']*)["']/) || [])[1] || "").split("|"); 114 115 const persist = () => { 116 if (window.__hythmTemp) return; // temporary chat: never saved on this device 117 session.versions = session.versions.slice(-12); 118 if (!session.log.length) return; 119 if (!projectId) { 120 projectId = "p" + Date.now(); 121 projects.unshift({ id: projectId, title: "Ù Ø´Ø±ÙØ¹ Ø¬Ø¯ÙØ¯", t: Date.now() }); 122 } 123 const p = projects.find((x) => x.id === projectId); 124 const first = session.log.find((e) => e.type === "user"); 125 if (p) { p.t = Date.now(); if (first) p.title = first.text.split("\n")[0].slice(0, 48); p.versions = session.versions.length; } 126 projects.sort((a, b) => b.t - a.t); 127 storage.set("hythm.projects", JSON.stringify(projects.slice(0, 60))); 128 storage.set("hythm.currentProject", projectId); 129 try { localStorage.setItem("hythm.p." + projectId, JSON.stringify(session)); } 130 catch { session.versions = session.versions.slice(-4); storage.set("hythm.p." + projectId, JSON.stringify(session)); } 131 renderProjects(); 132 updateLayout(); 133 }; 134 135 /* ---------------- Utils ---------------- */ 136 const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); 137 const icon = (id, cls = "") => `<svg class="${cls}"><use href="#i-${id}"/></svg>`; 138 const fmtTime = (t) =>
138 new Date(t).toLocaleString("ar", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); 139 const scrollDown = () => { els.messages.scrollTop = els.messages.scrollHeight; }; 140 function toast(msg) { 141 els.toast.textContent = msg; 142 els.toast.classList.add("show"); 143 clearTimeout(toast.t); 144 toast.t = setTimeout(() => els.toast.classList.remove("show"), 2000); 145 } 146 // Never throws: a stopped/unreachable server returns { ok:false, status:0, offline:true } instead of an uncaught error 147 /* ---------------- Hythm Shield: catches bugs, un-freezes the UI, reports to the server ---------------- */ 148 const Shield = { 149 sent: 0, 150 deadline(promise, ms, msg) { 151 let t; return Promise.race([promise, new Promise((_, rej) => { t = setTimeout(() => rej(new Error(msg || "timeout")), ms); })]).finally(() => clearTimeout(t)); 152 }, 153 report(where, err) { 154 const m = String((err && (err.stack || err.message)) || err).slice(0, 600); 155 if (/extension:\/\/|200\.js|M_ID/.test(m) || this.sent > 20) return; // browser extensions are not ours 156 this.sent++; 157 try { fetch("/api/client-error", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ where, m, v: 1, url: location.pathname }) }); } catch {} 158 }, 159 heal() { 160 // UI stuck in "busy" with nothing running â give the user their controls back 161 try { 162 if (typeof controller !== "undefined" && !controller && els.sendBtn.classList.contains("stop")) setBusy(false); 163 document.querySelectorAll(".img-dream").forEach((d) => { const card = d.closest(".msg"); if (card && Date.now() - (+card.dataset.t || Date.now()) > 240000) card.remove(); }); 164 } catch {} 165 } 166 }; 167 window.addEventListener("error", (e) => { Shield.report("error", e.error || e.message); Shield.heal(); }); 168 window.addEventListener("unhandledrejection", (e) => { Shield.report("promise", e.reason); Shield.heal(); }); 169 setInterval(() => Shield.heal(), 15000); 170 171 async function api(path, body) { 172 let res; 173 try { 174 res = await fetch(path, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : {}); 175 } catch { 176 return { ok: false, status: 0, offline: true, data: { reason: (window.HythmI18n ? HythmI18n.t : (x) => x)("٠ا ÙÙ Ø§ØªØµØ§Ù Ø¨Ø³ÙØ±Ùر Hythm") } }; 177 } 178 let data = {}; 179 try { data = await res.json(); } catch {} 180 // the server forgot this account (restart before backup) â silently restore it with the same id and retry once 181 if (res.status === 401 && data.error === "login" && path !== "/api/login" && !api.recovering && (await recoverAccount())) { 182 const b2 = body && body.uid ? { ...body, uid } : body; 183 return api(path.replace(/uid=[^&]+/, "uid=" + encodeURIComponent(uid)), b2); 184 } 185 return { ok: res.ok, status: res.status, data }; 186 } 187 async function recoverAccount() { 188 const name = storage.get("hythm.name") || (typeof me !== "undefined" && me && me.name); 189 if (!uid || !name) return false; 190 api.recovering = true; 191 try { 192 const r = await api("/api/login", { name, recover: uid }); 193 if (!r.ok) return false; 194 uid = r.data.uid; storage.set("hythm.uid", uid); 195 if (typeof applyMe === "function") applyMe(r.data.user, r.data.site); 196 return true; 197 } finally { api.recovering = false; } 198 } 199 200 /* ---------------- Credits UI ---------------- */ 201 /* ---------------- Usage (replaced credits): a budget per 5-hour window, shown as a ring ---------------- */ 202 function usageState() { 203 const u = me && me.usage; if (!u || !u.cap) return null; 204 const left = Math.max(0, Number(me.credits) || 0), leftPct = Math.max(0, Math.min(100, (left / u.cap) * 100)); 205 return { cap: u.cap, left, leftPct, usedPct: 100 - leftPct, reset: u.reset, context: u.context || 32000 }; 206 } 207 // how full THIS chat's context is (what the next request will carry), per plan 208 function contextPct() { 209 const u = usageState(); const ctx = (u && u.context) || 32000; 210 const maxTurns = ctx >= 256000 ? 24 : ctx >= 128000 ? 16 : 8, per = ctx >= 256000 ? 12000 : ctx >= 128000 ? 8000 : 4000; 211 const chars = chatHistory().reduce((n, t) =>
211 n + Math.min(per, String(t.content || "").length), 0); 212 return Math.min(100, Math.round((chars / (maxTurns * per)) * 100)); 213 } 214 function fmtReset(ms) { 215 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 216 const mins = Math.max(1, Math.ceil(Math.max(0, ms - Date.now()) / 60000)), h = Math.floor(mins / 60), m = mins % 60; 217 return (h ? h + " " + T("س") + (m ? " " : "") : "") + (m || !h ? m + " " + T("د") : ""); 218 } 219 function renderUsage() { 220 const u = usageState(), ring = document.getElementById("usageRing"); if (!ring) return; 221 ring.hidden = !u; 222 if (!u) return; 223 const fill = ring.querySelector(".ur-fill"); 224 fill.style.strokeDasharray = u.leftPct.toFixed(2) + " 100"; 225 ring.classList.toggle("low", u.leftPct < 15); ring.classList.toggle("empty", u.leftPct <= 0.5); 226 if (u.usedPct >= 80 && appSettings.usageAlert !== "off" && renderUsage.warned !== u.reset) { renderUsage.warned = u.reset; toast((window.HythmI18n ? HythmI18n.t : (x) => x)("ÙØµÙت 80% Ù Ù Ø§Ø³ØªØ®Ø¯Ø§Ù Ù â Ø¨Ùرجع ÙØ§Ù ٠بعد") + " " + fmtReset(u.reset)); } 227 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 228 ring.title = T("Ø§ÙØ§Ø³ØªØ®Ø¯Ø§Ù ") + ": " + Math.round(u.usedPct) + "% · " + T("Ø¨ÙØ±Ø¬Ø¹ ÙØ§Ù ٠بعد") + " " + fmtReset(u.reset); 229 if (els.creditValue) els.creditValue.textContent = Math.round(u.leftPct) + "%"; 230 const acc = document.getElementById("accCredits"); if (acc) acc.textContent = Math.round(u.leftPct) + "% " + T("٠تبÙÙ"); 231 } 232 function setCredits(value, animate) { 233 const prev = me ? me.credits : value; 234 if (me) me.credits = value; 235 renderUsage(); 236 if (animate && prev !== value && value < prev) { 237 const ring = document.getElementById("usageRing"); 238 if (ring) { ring.classList.remove("tick"); void ring.offsetWidth; ring.classList.add("tick"); } 239 } 240 updateSendCost(); 241 } 242 setInterval(() => { try { renderUsage(); const u = usageState(); if (u && u.reset && Date.now() > u.reset + 2000 && typeof refreshMe === "function") refreshMe(); } catch {} }, 30000); 243 // the details panel 244 function openUsage() { 245 const u = usageState(); if (!u) return; 246 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 247 const plan = (me.plan && me.plan.name) || "Hythm Free", ctx = contextPct(); 248 let p = document.getElementById("usagePop"); 249 if (!p) { p = document.createElement("div"); p.id = "usagePop"; p.className = "usage-pop"; document.body.appendChild(p); } 250 p.innerHTML = '<div class="up-head"><b>' + esc(T("Ø§ÙØ§Ø³ØªØ®Ø¯Ø§Ù ")) + '</b><span>' + esc(plan) + '</span></div>' + 251 '<div class="up-bar"><i style="width:' + u.leftPct.toFixed(1) + '%"></i></div>' + 252 '<div class="up-row"><span>' + esc(T("استخد٠ت")) + ' <b>' + u.usedPct.toFixed(1) + '%</b></span><span>' + esc(T("Ø¨ÙØ±Ø¬Ø¹ ÙØ§Ù ٠بعد")) + ' <b>' + esc(fmtReset(u.reset)) + '</b></span></div>' + 253 '<div class="up-sub"><span>' + esc(T("Ø³ÙØ§Ù ÙØ§ÙÙ ØØ§Ø¯Ø«Ø©")) + '</span><div class="up-bar thin"><i style="width:' + ctx + '%"></i></div><b>' + ctx + '%</b></div>' + 254 (ctx >= 85 ? '<p class="up-warn">' + esc(T("اÙÙ ØØ§Ø¯Ø«Ø© Ø·ÙÙÙØª â Ø§ÙØªØ Ù ØØ§Ø¯Ø«Ø© Ø¬Ø¯ÙØ¯Ø© ÙØ±Ø¯ أسرع ÙØ£Ø¯Ù.")) + '</p>' : "") + 255 '<p class="up-note">' + esc(T("Ù٠رد Ø¨ÙØØ³Ø¨ ØØ³Ø¨ Ø·ÙÙÙ Ø§ÙØÙÙÙÙØ ÙØ§ÙÙÙ ÙØ°Ø¬Ø ÙÙØ¯ÙØ´ ÙÙÙØ±Ø ÙÙØ¯ÙØ´ أخذ ÙÙØª. ٠ا ÙÙ Ø£Ø±ÙØ§Ù Ø¹Ø´ÙØ§Ø¦ÙØ©.")) + '</p>' + 256 '<div class="up-foot"><button type="button" data-up-more>' + esc(T("ش٠تغÙÙØ±Ø â")) + '</button>' + (me.plan && me.plan.id !== "free" ? "" : '<button type="button" class="pri" data-up-plus>' + esc(T("Ø²ÙØ¯ ØØ¯Ù٠٠ع Plus")) + "</button>") + "</div>"; 257 const r = document.getElementById("usageRing").getBoundingClientRect(); 258 p.style.left = Math.max(8, Math.min(innerWidth - 300, r.left + r.width / 2 - 150)) + "px"; p.style.bottom = (innerHeight - r.top + 10) + "px"; 259 p.hidden = false; 260 p.querySelector("[data-up-more]").onclick = () => { p.hidden = true; const d = document.getElementById("omniDialog"); if (d) { d.showModal(); document.querySelector('.xp-tab[data-pane="bye"]')?.click(); } }; 261 const up = p.querySelector("[data-up-plus]"); if (up) up.onclick = () => { p.hidden = true; window.HythmPlans && HythmPlans.open && HythmPlans.open("plus"); }; 262 setTimeout(() => document.addEventListener("click", function off(e) { if (!p.contains(e.target) && e.target.id !== "usageRing") { p.hidden = true; document.removeEventListener("click", off); } }), 0); 263 } 264 document.getElementById("usageRing")?.addEventListener("click", openUsage); 265 const isEditing = () => !editOff && session.current >= 0 && !!session.versions[session.current]; 266 const currentCost = () => { 267 if (task === "image") return 2.5; 268 if (task === "chat") return mode === "max" ? 2 : 1; 269 return (isEditing() ? cfg.modes[mode].editCost : cfg.modes[mode].cost) * ({ ultra: 2, x: 3, nova: 2, code: 2 }[model] || 1) * (fullSite && !isEditing() ? 2 : 1) * 4; 270 }; 271 function updateSendCost() { 272 els.sendCost.querySelector("span").textContent = currentCost(); 273 const editing = isEditing(); 274 els.editContext.hidden = !editing; 275 $("quickEdits").hidden = !editing; 276 $("themePick").hidden = !(session.current >= 0 && session.versions[session.current]); 277 try { refreshSwatch(); } catch {} 278 const cur = session.versions[session.current]; 279 if (cur) $("themeSelect").value = (cur.code.match(/data-theme=["']([\w-]+)/) || [])[1] || "minimal"; 280 if (editing) els.editTarget.textContent = `اÙÙØ³Ø®Ø© ${session.current + 1}`; 281 els.input.placeholder = (window.HythmI18n ? HythmI18n.t : (x) => x)(editing ? "ش٠بد٠تعدÙÙØ (Ù Ø«ÙØ§Ù: Ø®ÙÙ Ø§ÙØ£ÙÙØ§Ù Ø£Ø²Ø±ÙØ ضÙÙ ÙØ³Ù آراء Ø§ÙØ¹Ù ÙØ§Ø¡)" : "اسأ٠Hythm Ø£Ù Ø´Ùâ¦"); 282 } 283 284 /* ---------------- Modes & styles ---------------- */ 285 function setMode(m) { 286 mode = cfg.modes[m] ? m : "balanced"; 287 storage.set("hythm.mode", mode); 288 els.composer.dataset.mode = mode; 289 document.querySelectorAll(".mode").forEach((b) => b.setAttribute("aria-checked", String(b.dataset.mode === mode))); 290 els.modeHint.innerHTML = `${MODE_HINTS[mode]} · <kbd>Enter</kbd> ÙÙØ¥Ø±Ø³Ø§Ù`; 291 const pm = document.getElementById("pickerMode"); if (pm) pm.textContent = cfg.modes[mode]?.label || ""; 292 const tb = document.getElementById("thinkBtn"); if (tb) tb.setAttribute("aria-pressed", String(mode === "max")); 293 if (mode !== "max") storage.set("hythm.calmMode", mode); 294 updateSendCost(); 295 } 296 document.querySelectorAll(".mode").forEach((b) => b.addEventListener("click", () => setMode(b.dataset.mode))); 297 // picker v2: a one-line explanation of the chosen mode + keyboard for model rows 298 const MODE_HINT = { fast: "أسرع Ø¬ÙØ§Ø¨ ÙØ£Ù٠استÙÙØ§Ù · تÙÙÙØ± Ø®ÙÙÙ", balanced: "ØªÙØ§Ø²Ù بÙÙ Ø§ÙØ³Ø±Ø¹Ø© ÙØ§ÙØ¬ÙØ¯Ø©", max: "تÙÙÙØ± ع٠ÙÙ ÙØªØÙ٠· ÙÙÙ Ø³Ø§Ø¦Ù Ø§ÙØµØ¹Ø¨Ø© ÙØ§ÙÙ Ø´Ø§Ø±ÙØ¹" }; 299 const hintMode = () => { const el = document.getElementById("p2Hint"); if (el) el.textContent = (window.HythmI18n ? HythmI18n.t : (x) => x)(MODE_HINT[mode] || ""); }; 300 document.querySelectorAll(".mode").forEach((b) => b.addEventListener("click", () => setTimeout(hintMode, 0))); 301 document.querySelectorAll(".p2-row").forEach((r) => r.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); r.click(); } })); 302 setTimeout(hintMode, 300); 303 // the picker opens where there is room: up when the input is low on the page, down when it is high (home screen) 304 (function placePicker() { 305 const menu = document.getElementById("pickerMenu"); if (!menu) return; 306 const fit = () => { 307 if (menu.hidden) return; 308 const btn = menu.parentElement.getBoundingClientRect(); 309 const above = btn.top - 12, below = window.innerHeight - btn.bottom - 12; 310 const up = above >= Math.min(460, below) ; 311 menu.classList.toggle("p2-down", !up); 312 menu.style.maxHeight = Math.max(220, Math.min(560, (up ? above : below) - 8)) + "px"; 313 }; 314 new MutationObserver(fit).observe(menu, { attributes: true, attributeFilter: ["hidden"] }); 315 window.addEventListener("resize", fit); 316 })(); 317 document.querySelectorAll(".style-chip").forEach((b) => b.addEventListener("click", () => { 318 style = b.dataset.style; 319 document.querySelectorAll(".style-chip").forEach((x) => x.classList.toggle("active", x === b)); 320 })); 321 322 /* ---------------- Parsing model output ---------------- */ 323 function extractHtml(text) { 324 const fence = text.search(/```html/i); 325 if (fence !== -1) { 326 const body = text.slice(fence).replace(/^```html[^\n]*\n?/i, ""); 327 const end = body.indexOf("```"); 328 return { html: end === -1 ? body : body.slice(0, end), complete: end !== -1 || /<\/html>/i.test(body) }; 329 } 330 const start = text.search(/<!DOCTYPE html|<html[\s>]/i); 331 if (start !== -1) { 332 const body = text.slice(start).replace(/```[\s\S]*$/, ""); 333 return { html: body, complete: /<\/html>/i.test(body) }; 334 } 335 return null; 336 } 337 const PATCH_RE = /<{5,}\s*SEARCH\s*\n([\s\S]*?)\n?={5,}\s*\n([\s\S]*?)\n?>{5,}\s*REPLACE/g; 338 function parsePatches(text) { 339 const out = []; 340 for (const m of text.matchAll(PATCH_RE)) out.push({ search: m[1], replace: m[2] }); 341 return out; 342 } 343 function applyPatches(code, patches) { 344 let ok = 0, fail = 0; 345 for (const p of patches) { 346 if (!p.search.trim()) { fail++; continue; } 347 if (code.includes(p.search)) { code = code.replace(p.search, () => p.replace); ok++; continue; } 348 // tolerant match: compare trimmed lines 349 const lines = code.split("\n"); 350 const target = p.search.split("\n").map((l) =>
350 l.trim()).filter(Boolean); 351 let hit = -1; 352 outer: for (let i = 0; i < lines.length; i++) { 353 let j = 0, k = i; 354 while (j < target.length && k < lines.length) { 355 if (!lines[k].trim()) { k++; continue; } 356 if (lines[k].trim() !== target[j]) continue outer; 357 j++; k++; 358 } 359 if (j === target.length) { hit = i; lines.splice(i, k - i, p.replace); break; } 360 } 361 if (hit === -1) fail++; else { code = lines.join("\n"); ok++; } 362 } 363 return { code, ok, fail }; 364 } 365 // Token diet: strip indentation + blank lines (the code we send back on edits is ~20-30% smaller) 366 const compact = (code) => code.split("\n").map((l) => l.trimStart()).filter((l) => l.trim()).join("\n"); 367 function notesOf(text) { 368 let t = text.replace(PATCH_RE, "").replace(/<{5,}\s*SEARCH[\s\S]*$/, ""); 369 const fence = t.search(/```html/i); 370 if (fence !== -1) { 371 const after = t.slice(fence).replace(/^```html[^\n]*\n?/i, ""); 372 const end = after.indexOf("```"); 373 t = t.slice(0, fence) + (end !== -1 ? "\n" + after.slice(end + 3) : ""); 374 } 375 const start = t.search(/<!DOCTYPE html|<html[\s>]/i); 376 if (start !== -1) t = t.slice(0, start); 377 return t.replace(/<hythm-refuse[\s\S]*/i, "").trim(); 378 } 379 function renderNotes(text) { 380 const lines = esc(text).split("\n"); 381 let out = "", inList = false; 382 for (let line of lines) { 383 line = line.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/^#{1,6}\s*/, ""); 384 const m = line.match(/^\s*(?:[-*â¢]|\d+\.)\s+(.*)/); 385 if (m) { if (!inList) { out += "<ul>"; inList = true; } out += `<li>${m[1]}</li>`; } 386 else { if (inList) { out += "</ul>"; inList = false; } if (line.trim()) out += `<p>${line}</p>`; } 387 } 388 return out + (inList ? "</ul>" : ""); 389 } 390 391 /* ---------------- Photos ---------------- */ 392 function queriesIn(html) { 393 const qs = new Set(); 394 for (const m of html.matchAll(/data-(?:img|bg)\s*=\s*["']([^"']{2,80})["']/g)) qs.add(m[1].trim().toLowerCase()); 395 return [...qs]; 396 } 397 let fetchTimer = null; 398 function requestImages(queries, onDone) { 399 const need = queries.filter((q) => !imageCache.has(q) && !pendingQueries.has(q)); 400 if (!need.length) return Promise.resolve(); 401 need.forEach((q) => pendingQueries.add(q)); 402 return api("/api/images", { queries: need }).then(({ data }) => { 403 for (const q of need) { pendingQueries.delete(q); imageCache.set(q, data.results?.[q] || []); } 404 onDone && onDone(); 405 }).catch(() => need.forEach((q) => pendingQueries.delete(q))); 406 } 407 function applyImages(html, collect) { 408 if (!/data-(img|bg)/.test(html)) return html; 409 const doc = new DOMParser().parseFromString(html, "text/html"); 410 const counters = {}; 411 const usedUrls = new Set(); 412 const pick = (raw) => { 413 const q = raw.trim().toLowerCase(); 414 const list = imageCache.get(q); 415 if (!list || !list.length) return null; 416 // prefer a photo not used elsewhere on this page, so nothing repeats 417 let item = list.find((x) => !usedUrls.has(x.url)); 418 if (!item) { const i = counters[q] = (counters[q] ?? -1) + 1; item = list[i % list.length]; } 419 usedUrls.add(item.url); 420 if (collect) collect.push(item); 421 return item; 422 }; 423 doc.querySelectorAll("img[data-img]").forEach((img) => { 424 const item = pick(img.dataset.img); 425 if (item) { img.setAttribute("src", item.url); img.setAttribute("loading", "lazy"); img.setAttribute("decoding", "async"); img.setAttribute("referrerpolicy", "no-referrer"); } 426 }); 427 doc.querySelectorAll("[data-bg]").forEach((el) => { 428 const item = pick(el.dataset.bg); 429 if (item) el.style.backgroundImage = `url("${item.url}")` + (el.style.backgroundImage ? ", " + el.style.backgroundImage : ""); 430 }); 431 return "<!DOCTYPE html>\n" + doc.documentElement.outerHTML; 432 } 433 434 /* ---------------- Build the final page ---------------- */ 435 function buildDoc(code, collect) { 436 let html = applyImages(code, collect).replace(/^\s*<!DOCTYPE[^>]*>/i, ""); 437 const head = `<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style id="hythm-kit">${baseCss}</style>`; 438 const runtime = `<script src="${LUCIDE}"><\/script><script>${runtimeJs.replace(/<\/script/gi, "<\\/script")}<\/script>`; 439 if (/<head[^>]*>/i.test(html)) html = html.replace(/<head[^>]*>/i, (m) => m + head); 440 else if (/<html[^>]*>/i.test(html)) html = html.replace(/<html[^>]*>/i, (m) => m + "<head>" + head + "</head>"); 441 else html = head + html; 442 if (/<\/body>/i.test(html)) html = html.replace(/<\/body>(?![\s\S]*<\/body>)/i, runtime + "</body>"); 443 else html += runtime; 444 return "<!DOCTYPE html>\n" + html; 445 } 446 447 /* ---------------- Stage ---------------- */ 448 let lastCode = ""; 449 function showCode(code, { final }
449 = {}) { 450 lastCode = code; 451 els.placeholder.hidden = true; 452 els.app.classList.remove("no-design", "is-empty"); 453 const used = []; 454 const doc = buildDoc(code, used); 455 els.frame.srcdoc = doc; 456 $("guardBadge").hidden = true; 457 els.code.textContent = code; 458 if (final) { 459 els.photoCount.hidden = !used.length; 460 els.photoCount.textContent = used.length; 461 showCode.used = used; 462 } 463 setTab(currentTab); 464 } 465 function setTab(tab) { 466 currentTab = tab; 467 document.querySelectorAll("[data-tab]").forEach((b) => b.setAttribute("aria-selected", String(b.dataset.tab === tab))); 468 const has = !!lastCode; 469 els.frameWrap.hidden = !has || tab !== "preview"; 470 els.codeView.hidden = !has || tab !== "code"; 471 } 472 function selectVersion(i, { silent } = {}) { 473 const v = session.versions[i]; 474 if (!v) return; 475 session.current = i; 476 editOff = false; 477 persist(); 478 requestImages(queriesIn(v.code)).then(() => showCode(v.code, { final: true })); 479 showCode(v.code, { final: true }); 480 document.querySelectorAll(".version-card").forEach((c) => c.classList.toggle("active", Number(c.dataset.v) === i)); 481 updateSendCost(); 482 if (!silent && matchMedia("(max-width: 800px)").matches) setView("stage"); 483 } 484 function setView(view) { 485 els.app.dataset.view = view; 486 document.querySelectorAll(".mobile-tabs [data-view]").forEach((b) => b.setAttribute("aria-selected", String(b.dataset.view === view))); 487 } 488 document.querySelectorAll("[data-tab]").forEach((b) => b.addEventListener("click", () => setTab(b.dataset.tab))); 489 document.querySelectorAll("[data-device]").forEach((b) => b.addEventListener("click", () => { 490 els.frameWrap.dataset.device = b.dataset.device; 491 document.querySelectorAll("[data-device]").forEach((x) => x.setAttribute("aria-pressed", String(x === b))); 492 })); 493 document.querySelectorAll(".mobile-tabs [data-view]").forEach((b) => b.addEventListener("click", () => setView(b.dataset.view))); 494 495 /* ---------------- Chat rendering ---------------- */ 496 function renderEntry(e) { 497 const div = document.createElement("div"); 498 if (e.type === "user") { 499 div.className = "msg msg-user"; 500 if (e.text) setTimeout(() => { 501 if (div.querySelector(".msg-edit")) return; 502 const b = document.createElement("button"); b.type = "button"; b.className = "msg-edit"; 503 b.title = (window.HythmI18n ? HythmI18n.t : (x) => x)("عدÙÙ ÙØ£Ø±Ø³Ù Ù Ù Ø¬Ø¯ÙØ¯"); 504 b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>'; 505 b.addEventListener("click", () => { els.input.value = e.text; autoGrow(); els.input.focus(); els.input.setSelectionRange(e.text.length, e.text.length); els.input.dispatchEvent(new Event("input")); }); 506 div.appendChild(b); 507 }, 0); 508 const modeIcon = { fast: "zap", balanced: "sparkles", max: "flame" }[e.mode] || "sparkles"; 509 div.innerHTML = `<div class="bubble"></div><div class="bubble-meta"> 510 <span class="tag ${e.mode === "max" ? "t-max" : ""}">${icon(modeIcon)}${esc(cfg.modes[e.mode]?.label || "")}</span> 511 ${e.model ? `<span class="tag">${MODEL_NAMES[e.model] || ""}</span>` : ""} 512 ${e.edit ? `<span class="tag">${icon("patch")}تعدÙÙ</span>` : ""} 513 ${e.style ? `<span class="tag">${esc(e.styleLabel || e.style)}</span>` : ""}</div>`; 514 div.querySelector(".bubble").textContent = e.links && e.links.length ? e.links.reduce((s, l) => s.split(l.url).join(""), e.text).trim() : e.text; 515 if (!div.querySelector(".bubble").textContent) div.querySelector(".bubble").hidden = true; 516 if (e.links && e.links.length) { 517 const g = document.createElement("div"); g.className = "bubble-links"; 518 g.innerHTML = e.links.map((l) => '<a href="' + esc(/^https?:\/\//.test(l.url) ? l.url : "#") + '" target="_blank" rel="noopener">' + linkChip(l) + "</a>").join(""); 519 div.insertBefore(g, div.firstChild); 520 } 521 if (e.fileNames && e.fileNames.length) { 522 const g = document.createElement("div"); g.className = "bubble-files"; 523 g.innerHTML = e.fileNames.map((n) => '<span><i>' + esc((n.split(".").pop() || "").slice(0, 4).toUpperCase()) + "</i>" + esc(n) + "</span>").join(""); 524 div.insertBefore(g, div.firstChild); 525 } 526 if (e.thumbs && e.thumbs.length) { 527 const g = document.createElement("div"); g.className = "bubble-imgs"; 528 g.innerHTML = e.thumbs.map((t) => '<img src="' + t + '" alt="" data-view>').join(""); 529 div.insertBefore(g, div.firstChild); 530 } 531 } else if (e.type === "ai") { 532 div.className = "msg msg-ai"; 533 div.innerHTML = aiShell(); 534 const ui = aiParts(div); 535 ui.typing.remove(); 536 if (!actRestore(ui, e) && e.reasoning) { ui.thinking.hidden = false; ui.thinking.classList.remove("live"); ui.thinkingLabel.textContent = "طرÙÙØ© تÙÙÙØ±Ù"; ui.thinkingBody.textContent = e.reasoning; } 537 ui.notes.innerHTML = renderNotes(e.notes || "");
538 if (e.version != null) addVersionCard(ui, e.version, e.versionKind); 539 if (e.meta) addMeta(ui, e.meta); 540 if (e.notice) addNotice(ui, e.notice); 541 } else if (e.type === "warning") { 542 div.className = "msg"; 543 div.appendChild(warningCard(e)); 544 } else if (e.type === "chat") { 545 div.className = "msg msg-ai msg-chat"; 546 div.innerHTML = aiShell(); 547 const ui = aiParts(div); 548 ui.typing.remove(); 549 if (!actRestore(ui, e) && e.reasoning) { ui.thinking.hidden = false; ui.thinking.classList.remove("live"); ui.thinkingLabel.textContent = "طرÙÙØ© تÙÙÙØ±Ù"; ui.thinkingBody.textContent = e.reasoning; } 550 if ((e.dsImages && e.dsImages.length) || e.dsSites) ui.notes.before(dsSummary(e)); 551 if (e.apps && e.apps.length && window.HythmCards) (ui.root.querySelector(".ds-panel") || ui.notes).before(HythmCards.summary(e.apps)); 552 renderMdInto(ui.notes, visibleAnswer(e.md || "")); 553 if (e.appCards && window.HythmCards) HythmCards.fill(ui.notes, e.appCards); 554 if (e.sources && e.sources.length) { 555 const src = document.createElement("div"); src.className = "ans-sources"; 556 src.innerHTML = e.sources.map((x, i) => '<a href="' + esc(x.url) + '" target="_blank" rel="noopener"><b>' + (i + 1) + "</b>" + esc(x.title) + "</a>").join(""); 557 ui.notes.after(src); 558 } 559 if (e.md && e.md.trim()) addAnswerActions(div, e); 560 if (e.meta) addMeta(ui, e.meta); 561 if (e.notice) addNotice(ui, e.notice); 562 if (e.next && e.next.length && (session.log.indexOf(e) === -1 || e === session.log[session.log.length - 1])) { 563 const nx = document.createElement("div"); nx.className = "ans-next"; 564 e.next.forEach((q) => { const b = document.createElement("button"); b.type = "button"; b.textContent = q; b.addEventListener("click", () => { nx.remove(); send(q); }); nx.appendChild(b); }); 565 div.appendChild(nx); 566 } 567 } else if (e.type === "image") { 568 div.className = "msg msg-ai"; 569 div.appendChild(imageCard(e)); 570 } else if (e.type === "imgpair") { 571 div.className = "msg msg-ai"; 572 div.appendChild(pairCard(e, div)); 573 } else if (e.type === "notice") { 574 div.className = "msg"; 575 div.appendChild(noticeEl(e)); 576 } 577 els.messages.appendChild(div); 578 return div; 579 } 580 function addAnswerActions(div, e) { 581 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 582 const bar = document.createElement("div"); bar.className = "ans-actions"; 583 const btn = (ico, label, fn) => { const b = document.createElement("button"); b.type = "button"; b.title = T(label); b.setAttribute("aria-label", T(label)); b.innerHTML = ico; b.addEventListener("click", () => fn(b)); bar.appendChild(b); return b; }; 584 const svg = (d) => '<svg viewBox="0 0 24 24">' + d + "</svg>"; 585 btn(svg('<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'), "ÙØ³Ø®", async (b) => { try { await navigator.clipboard.writeText(visibleAnswer(e.md)); b.classList.add("done"); setTimeout(() => b.classList.remove("done"), 1200); } catch {} }); 586 btn(svg('<path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/><path d="M18.5 5.5a9 9 0 0 1 0 13"/>'), "Ø§ÙØ±Ø£ Ø¨ØµÙØª", (b) => readAloud(visibleAnswer(e.md), b)); 587 btn(svg('<path d="M3 12a9 9 0 0 1 15.5-6.2L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15.5 6.2L3 16"/><path d="M3 21v-5h5"/>'), "أعد Ø§ÙØªÙÙÙØ¯", () => { 588 const i = session.log.indexOf(e); const u = i > 0 ? session.log.slice(0, i).reverse().find((x) => x.type === "user") : null; 589 if (u && !window.__hythmTemp) api("/api/feedback", { uid, vote: "down", tags: ["أعاد Ø§ÙØªÙÙÙØ¯"], prompt: u.text, answer: visibleAnswer(e.md).slice(0, 600), model: MODEL_NAMES[model] || model }); // learning signal 590 if (u) send(u.text); 591 }); 592 const askedFor = () => (session.log.slice(0, session.log.indexOf(e)).reverse().find((x) => x.type === "user") || {}).text || ""; 593 const vote = (v) => (b) => { 594 bar.querySelectorAll(".vote").forEach((x) => x.classList.remove("on")); b.classList.add("on");
595 const base = { uid, vote: v, prompt: askedFor(), answer: visibleAnswer(e.md).slice(0, 600), model: MODEL_NAMES[model] || model }; 596 if (v === "down" && window.HythmUX) return HythmUX.feedback({ kind: "dislike", onSend: (tags, reason) => api("/api/feedback", { ...base, tags, reason }) }); 597 api("/api/feedback", base); toast(T("Ø´ÙØ±Ø§Ù! Hythm Ø¨ÙØªØ¹ÙÙ٠٠٠رأÙÙ")); 598 }; 599 btn(svg('<path d="M7 10v11"/><path d="M15 5.9 14 10h5.8a2 2 0 0 1 2 2.3l-1.4 7A2 2 0 0 1 18.4 21H7V10l4-8a3 3 0 0 1 4 3.9z"/>'), "رد Ø¬ÙØ¯", vote("up")).classList.add("vote"); 600 btn(svg('<path d="M17 14V3"/><path d="M9 18.1 10 14H4.2a2 2 0 0 1-2-2.3l1.4-7A2 2 0 0 1 5.6 3H17v11l-4 8a3 3 0 0 1-4-3.9z"/>'), "رد Ø³ÙØ¦", vote("down")).classList.add("vote"); 601 // ⯠more: details of this answer · report it 602 const more = btn(svg('<circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/>'), "اÙÙ Ø²ÙØ¯", (b) => { 603 const open = b.parentElement.querySelector(".ans-menu"); if (open) return open.remove(); 604 const m = document.createElement("div"); m.className = "ans-menu"; 605 m.innerHTML = '<button type="button" data-m="details">' + svg('<circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7.5v.5"/>') + esc(T("ØªÙØ§ØµÙÙ Ø§ÙØ±Ø¯")) + '</button>' + 606 '<button type="button" data-m="copyMd">' + svg('<path d="M4 6h16M4 12h10M4 18h7"/>') + esc(T("Ø§ÙØ³Ø® ÙÙ Markdown")) + '</button>' + 607 '<button type="button" data-m="report" class="danger">' + svg('<path d="M4 21V4h13l-2 4 2 4H4"/>') + esc(T("Ø§ÙØ¥Ø¨Ùاغ Ø¹Ù Ø§ÙØ±Ø¯")) + "</button>"; 608 b.parentElement.appendChild(m); 609 const off = (ev) => { if (!m.contains(ev.target) && ev.target !== b) { m.remove(); document.removeEventListener("pointerdown", off, true); } }; 610 setTimeout(() => document.addEventListener("pointerdown", off, true), 0); 611 m.addEventListener("click", (ev) => { 612 const k = (ev.target.closest("[data-m]") || {}).dataset?.m; if (!k) return; m.remove(); 613 if (k === "copyMd") { navigator.clipboard.writeText(e.md || "").then(() => toast(T("Ø§ÙØ³Ø® â"))).catch(() => {}); return; } 614 if (k === "report") return HythmUX.feedback({ kind: "report", onSend: (tags, reason) => api("/api/feedback", { uid, kind: "report", tags, reason, prompt: askedFor(), answer: visibleAnswer(e.md).slice(0, 600), model: MODEL_NAMES[model] || model }) }); 615 const mt = e.meta || {}, words = visibleAnswer(e.md).split(/\s+/).filter(Boolean).length; 616 const src = (e.sources || []).map((x, i) => '<a href="' + esc(x.url) + '" target="_blank" rel="noopener">[' + (i + 1) + "] " + esc(x.title) + "</a>").join("<br>"); 617 HythmUX.details([ 618 ["اÙÙÙ ÙØ°Ø¬", MODEL_NAMES[model] || model], ["اÙÙÙØª", mt.ms ? (mt.ms / 1000).toFixed(1) + "s" : ""], ["اÙÙÙ٠ات", String(words)], 619 ["Ø§ÙØªÙÙÙØ²", mt.tokens ? String(mt.tokens) : ""], ["Ø§ÙØªÙÙÙØ©", mt.cost != null ? mt.cost + " " + T("ÙØØ¯Ø©") + (mt.refunded ? " (" + T("رجعت") + ")" : "") : ""], 620 ["DeepSearch", e.dsSites ? e.dsSites + " " + T("٠صدر") : ""], ["اÙ٠صادر", src, true], ["Ø§ÙØªÙÙÙØ±", e.reasoning ? T("Ù ØªØ§Ø (اضغط \"طرÙÙØ© تÙÙÙØ±Ù\")") : ""], 621 ]); 622 }); 623 }); 624 more.classList.add("ans-more-btn"); 625 const fileCards = window.HythmFiles && HythmFiles.cards(e.md); 626 if (fileCards) div.appendChild(fileCards); 627 div.appendChild(bar); 628 } 629 /* ---------- read aloud: natural server voice, streamed sentence by sentence ---------- */ 630 let reading = null; 631 function readAloud(text, b) { 632 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 633 if (reading) { const was = reading.b; reading.stop(); if (was === b) return; } 634 const plain = String(text).replace(/```[\s\S]*?```/g, " ").replace(/<[^>]+>/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[#*_>`|$\\]/g, " ").replace(/\[\d+\]/g, "").replace(/\s+/g, " ").trim().slice(0, 6000); 635 if (!plain) return; 636 const parts = plain.match(/[^.!?Ø\n]{1,260}[.!?Ø]?/g) || [plain]; 637 const chunks = []; let cur = ""; 638 for (const p of parts) { if ((cur + p).length > 240 && cur) { chunks.push(cur.trim()); cur = ""; } cur += p + " "; } if (cur.trim()) chunks.push(cur.trim()); 639 const st = { b, stop: null, dead: false, audio: null }; reading = st; 640 b.classList.add("speaking", "on"); 641 const end = () => { st.dead = true; b.classList.remove("speaking", "on"); if (reading === st) reading = null; }; 642 st.stop = () => { st.dead = true; try { st.audio && st.audio.pause(); } catch {} try { speechSynthesis.cancel(); } catch {} end(); }; 643 const fetchOne = (t) => fetch("/api/voice/tts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uid, text: t, voice: "el_f", lang: /[\u0600-\u06FF]/.test(t) ? "ar" : "en" }) }).then((r) => (r.ok ? r.blob() : null)).catch(() => null); 644 const browserVoice = (t, done) => { // last resort: the best female voice the device has 645 if (!("speechSynthesis" in window)) return done(); 646 const u = new SpeechSynthesisUtterance(t), ar = /[\u0600-\u06FF]/.test(t), vs = speechSynthesis.getVoices().filter((v) => v.lang.startsWith(ar ? "ar" : "en")); 647 u.voice = vs.find((v) => /female|zira|hoda|laila|salma|zariyah|samantha|aria|jenny|natural/i.test(v.name)) || vs[0] || null; 648 u.lang = ar ? "ar-SA" : "en-US"; u.rate = 1; u.onend = done; u.onerror = done; speechSynthesis.speak(u); 649 }; 650 (async () => { 651 let next = fetchOne(chunks[0]); 652 for (let i = 0; i < chunks.length && !st.dead; i++) { 653 const blob = await next; if (st.dead) return; 654 if (i + 1 < chunks.length) next = fetchOne(chunks[i + 1]); // fetch the next sentence while this one plays 655 await new Promise((done) => { 656 if (!blob) return browserVoice(chunks[i], done); 657 const a = new Audio(URL.createObjectURL(blob)); st.audio = a; 658 a.onended = done; a.onerror = done; a.play().catch(done); 659 }); 660 } 661 end(); 662 })(); 663 } 664 /* ---------- Live activity: what Hythm is doing RIGHT NOW (instead of "thinkingâ¦"), and every stage when you open it ---------- */ 665 const ACT_ICON = { read: "ð", plan: "ð§", think: "ð", math: "ð§®", code: "â¨ï¸", game: "ð®", site: "ð¥", motion: "ð¬", three: "ð§", check: "ð", search: "ð", app: "â", write: "âï¸", table: "ð", files: "ð", image: "ð¨", done: "â", design: "ð¨", fix: "ð " }; 666 // reasoning text â a short, human label (Arabic + English cues) 667 const ACT_RULES = [ 668 ["game", "Ø¨ÙØ¨Ø±Ù ج اÙÙØ¹Ø¨Ø©", /\b(game|player|enemy|score|level|collision|sprite)\b|ÙØ¹Ø¨[Ø©Ù]|اÙÙØ§Ø¹Ø¨/i], 669 ["motion", "Ø¨ÙØØ±Ù٠اÙÙ Ø´Ø§ÙØ¯", /\b(motion|animation|animate|scene|keyframe|transition|HM\.film|timeline)\b|Ù ÙØ´Ù|اÙÙÙ ÙØ´Ù|Ù Ø´ÙØ¯/i], 670 ["three", "Ø¨ÙØ¨Ù٠اÙ٠جسÙÙ 3D", /\b(three\.js|mesh|geometry|3d|material|camera|render)\b|٠جس٠|Ø«ÙØ§Ø«Ù/i], 671 ["site", "Ø¨ÙØµÙ Ù Ø§ÙØµÙØØ©", /\b(html|css|layout|section|hero|navbar|landing|page|website|responsive)\b|Ù ÙÙØ¹|ØµÙØ[Ø©Ù]/i], 672 ["code", "Ø¨ÙØ¨Ø±Ù ج", /\b(code|function|class|variable|bug|python|javascript|api|loop|array|sql|regex)\b|ÙÙØ¯|بر٠ج/i], 673 ["math", "Ø¨ÙØØ³Ø¨", /\b(calculate|compute|equation|integral|derivative|solve|sum|multiply|=\s*\d)|\d+\s*[xÃ*/+-
673]\s*\d+|Ø§ØØ³Ø¨|٠عادÙ/i], 674 ["search", "Ø¨ÙØ±Ø§Ø¬Ø¹ اÙ٠صادر", /\b(source|search|according|wikipedia|article|result)\b|٠صدر|اÙ٠صادر/i], 675 ["check", "Ø¨ÙØªØ£Ùد Ù Ù Ø§ÙØ¬Ùاب", /\b(check|verify|double-check|let me re|wait|hmm|actually|make sure|confirm)\b|Ø£ØªØ£ÙØ¯|تØÙÙ/i], 676 ["plan", "Ø¨ÙØ®Ø·Ø·", /\b(plan|steps?|first|outline|approach|structure)\b|خط[Ø©Ù]|Ø®Ø·ÙØ§Øª/i], 677 ["write", "Ø¨ÙØ¬ÙÙØ² اÙÙØªØ§Ø¨Ø©", /\b(respond|answer|write|reply|format|in arabic|dialect|tone)\b|Ø£ÙØªØ¨|Ø§ÙØ¬Ùاب|Ø§ÙØ±Ø¯/i], 678 ["read", "بÙÙÙÙ Ø·ÙØ¨Ù", /\b(user (wants|asks|is asking)|the user|they want|request)\b|اÙ٠ستخد٠|بدÙ|بدÙ/i], 679 ]; 680 function actStart(ui) { const A2 = { ui, steps: [], t0: Date.now(), lastSwitch: 0, rbuf: "" }; actStep(A2, "read", "بÙÙÙÙ Ø·ÙØ¨Ù", ""); return A2; } 681 function actStep(A2, kind, label, detail, force) { 682 if (!A2) return; 683 const last = A2.steps[A2.steps.length - 1]; 684 if (last && (last.label === label || (!force && Date.now() - A2.lastSwitch < 2200))) { if (detail) last.detail = (last.detail + detail).slice(-3000); actPaint(A2, true); return; } 685 if (last) last.ms = Date.now() - last.t0; 686 A2.steps.push({ kind, label, detail: detail || "", t0: Date.now() }); A2.lastSwitch = Date.now(); 687 actPaint(A2); 688 } 689 function actFromReasoning(A2, chunk) { 690 A2.rbuf = (A2.rbuf + chunk).slice(-420); 691 const hit = ACT_RULES.find(([, , re]) => re.test(A2.rbuf)); 692 const [kind, label] = hit ? hit : ["think", "بÙÙÙÙØ±"]; 693 actStep(A2, kind, label, chunk); 694 } 695 function actFromContent(A2, content) { 696 const open = (content.match(/```/g) || []).length % 2 === 1, lang = open ? (content.slice(content.lastIndexOf("```") + 3).match(/^[\w+-]*/) || [""])[0] : ""; 697 if (open && /HM\.film|hythm-motion/.test(content)) return actStep(A2, "motion", "Ø¨ÙØØ±Ù٠اÙÙ ÙØ´Ù", "", true); 698 if (open && /three|THREE/.test(content)) return actStep(A2, "three", "Ø¨ÙØ¨Ù٠اÙÙ Ø´ÙØ¯ 3D", "", true); 699 if (open && /html/i.test(lang)) return actStep(A2, "site", /canvas|requestAnimationFrame/.test(content.slice(-4000)) ? "Ø¨ÙØ¨Ø±Ù ج اÙÙØ¹Ø¨Ø©" : "Ø¨ÙØ¨ÙÙ Ø§ÙØµÙØØ©", "", true); 700 if (open) return actStep(A2, "code", "بÙÙØªØ¨ اÙÙÙØ¯" + (lang ? " (" + lang + ")" : ""), "", true); 701 if (/\n\|[^\n]*\|\s*$/.test(content.slice(-300))) return actStep(A2, "table", "Ø¨ÙØ±ØªÙب جدÙÙ", "", true); 702 actStep(A2, "write", "بÙÙØªØ¨ Ø§ÙØ¬Ùاب", "", true); 703 } 704 function actFinish(A2) { 705 if (!A2) return; 706 const last = A2.steps[A2.steps.length - 1]; if (last) last.ms = Date.now() - last.t0; 707 A2.done = Date.now() - A2.t0; actPaint(A2); 708 } 709 const actSec = (ms) => (ms < 1000 ? "<1" : Math.round(ms / 1000)) + (window.HythmI18n && HythmI18n.lang === "en" ? "s" : "Ø«"); 710 function actTimeline(steps) { 711 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 712 return '<ol class="act-steps">' + steps.map((s, i) => '<li class="act-step' + (s.ms == null ? " now" : "") + '"><span class="act-ico">' + (ACT_ICON[s.kind] || "â¢") + '</span><div class="act-main"><div class="act-row"><b>' + esc(T(s.label)) + '</b><em>' + (s.ms == null ? esc(T("Ø§ÙØ¢Ù")) : actSec(s.ms)) + "</em></div>" + (s.detail && s.detail.trim() ? '<details class="act-detail"><summary>' + esc(T("Ø§ÙØªÙاصÙÙ")) + '</summary><pre>' + esc(s.detail.trim().slice(-1500)) + "</pre></details>" : "") + "</div></li>").join("") + "</ol>"; 713 } 714 function actPaint(A2, soft) { 715 const ui = A2.ui; if (!ui || !ui.thinking) return; 716 const T = window.HythmI18n ? HythmI18n.t : (x) => x, cur = A2.steps[A2.steps.length - 1]; 717 ui.thinking.hidden = false; ui.thinking.classList.add("act"); 718 ui.thinking.classList.toggle("live", !A2.done); 719 ui.thinkingLabel.innerHTML = A2.done ? esc(T("اÙ٠راØÙ")) + ' <span class="act-meta">' + A2.steps.length + " · " + actSec(A2.done) + "</span>" : '<span class="act-cur">' + (ACT_ICON[cur.kind] || "") + " " + esc(T(cur.label)) + "â¦</span>"; 720 if (!A2.done && els.statusText) els.statusText.textContent = T(cur.label) + "â¦"; 721 if (soft && ui.thinking.open) { const pre = ui.thinkingBody.querySelector(".act-step:last-child pre"); if (pre) { pre.textContent = cur.detail.trim().slice(-1500); return; } } 722 if (!soft || ui.thinking.open) ui.thinkingBody.innerHTML = actTimeline(A2.steps); 723 } 724 function actRestore(ui, e) { 725 if (!e.steps || !e.steps.length) return false;
726 ui.thinking.hidden = false; ui.thinking.classList.remove("live"); ui.thinking.classList.add("act"); 727 const T = window.HythmI18n ? HythmI18n.t : (x) => x, total = e.steps.reduce((q, s) => q + (s.ms || 0), 0); 728 ui.thinkingLabel.innerHTML = esc(T("اÙ٠راØÙ")) + ' <span class="act-meta">' + e.steps.length + " · " + actSec(total) + "</span>"; 729 ui.thinkingBody.innerHTML = actTimeline(e.steps); 730 return true; 731 } 732 function aiShell() { 733 return `<div class="ai-head"><span class="ai-avatar h-mark"><svg class="cloud-logo"><use href="#logo-cloud"/></svg></span>${MODEL_NAMES[model] || "Mini Omni 1.01"}</div> 734 <details class="thinking live" hidden><summary><span class="dot"></span><span class="t-label">بÙÙÙÙ Ø·ÙØ¨Ùâ¦</span>${icon("chev", "chev")}</summary><div class="thinking-body"></div></details> 735 <div class="typing"><span></span><span></span><span></span></div> 736 <div class="notes"></div><div class="slot"></div>`; 737 } 738 /* ---------- DeepSearch: live research panel (steps · sites being read · pictures) ---------- */ 739 const DS_ICON = { plan: "ð§", search: "ð", read: "ð", reflect: "ð", write: "âï¸" }; 740 const viaHythm = (u) => "/api/img?u=" + encodeURIComponent(u); // pictures come through Hythm: no hotlink blocks or trackers 741 const favicon = (h) => viaHythm("https://icons.duckduckgo.com/ip3/" + h + ".ico"); 742 function dsImagesEl(list) { 743 const g = document.createElement("div"); g.className = "ds-imgs"; 744 const n = Math.min(list.length, 7); g.dataset.n = n; g.setAttribute("data-view-group", ""); 745 g.innerHTML = list.slice(0, n).map((im, i) => '<a href="' + esc(im.page || im.url) + '" data-view data-full="' + esc(viaHythm(im.url)) + '" data-page="' + esc(im.page || "") + '" data-title="' + esc(im.title || "") + '" style="--i:' + i + '"><img src="' + esc(viaHythm(im.thumb || im.url)) + '" alt="' + esc(im.title || "") + '" loading="lazy" onerror="this.parentNode.remove()"></a>').join("") + 746 (list.length > n ? '<span class="ds-more">+' + (list.length - n) + "</span>" : ""); 747 return g; 748 } 749 // one calm line â "Searching the webâ¦" â with the icons of the sites being read; pictures arrive in a tidy mosaic at the end 750 function dsSitesHtml(sites) { 751 const seen = new Set(), list = sites.filter((x) => x.host && !seen.has(x.host) && seen.add(x.host)); 752 return list.slice(0, 10).map((x, i) => '<a href="' + esc(x.url) + '" target="_blank" rel="noopener" title="' + esc(x.title || x.host) + '" style="--i:' + i + '"><img src="' + favicon(x.host) + '" alt="" onerror="this.replaceWith(Object.assign(document.createElement(\'b\'),{textContent:\'' + esc((x.host[0] || "â¢").toUpperCase()) + '\'}))"></a>').join("") + 753 (list.length > 10 ? '<span class="ds-plus">+' + (list.length - 10) + "</span>" : ""); 754 } 755 function dsEvent(ui, e) { 756 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 757 let p = ui.root.querySelector(".ds-panel"); 758 if (!p) { 759 p = document.createElement("div"); p.className = "ds-panel live"; 760 p.innerHTML = '<div class="ds-line"><span class="ds-globe"></span><span class="ds-label">' + esc(T("Ø¬Ø§Ø±Ù Ø§ÙØ¨ØØ« باÙÙÙØ¨â¦")) + '</span></div><div class="ds-sites"></div>'; 761 p._sites = []; p._imgs = []; 762 ui.notes.before(p); 763 } 764 if ((e.step === "visit" || e.step === "source" || e.step === "skip") && e.source && e.source.host) { 765 // live: the icon appears the moment Hythm opens the site, lights up when it has been read, fades if it failed 766 const box = p.querySelector(".ds-sites"); p._icons = p._icons || {}; 767 let a = p._icons[e.source.host]; 768 if (!a && e.step !== "skip") { 769 if (Object.keys(p._icons).length >= 14) { p._extra = (p._extra || 0) + 1; let m = box.querySelector(".ds-plus"); if (!m) { m = document.createElement("span"); m.className = "ds-plus"; box.appendChild(m); } m.textContent = "+" + p._extra; } 770 else { 771 a = document.createElement("a"); a.href = e.source.url; a.target = "_blank"; a.rel = "noopener"; a.title = e.source.title || e.source.host; a.className = "reading"; 772 a.style.setProperty("--i", 0); 773 const im = new Image(); im.alt = ""; im.src = favicon(e.source.host); 774 im.onerror = () => { const b = document.createElement("b"); b.textContent = (e.source.host[0] || "â¢").toUpperCase(); im.replaceWith(b); }; 775 a.appendChild(im); box.insertBefore(a, box.querySelector(".ds-plus")); p._icons[e.source.host] = a; 776 } 777 }
778 if (a && e.step === "source") { a.classList.remove("reading"); a.classList.add("read"); if (!p._sites.some((x) => x.host === e.source.host)) p._sites.push(e.source); } 779 if (a && e.step === "skip" && !a.classList.contains("read")) { a.classList.add("gone"); setTimeout(() => { a.remove(); delete p._icons[e.source.host]; }, 400); } 780 } else if (e.step === "images" && e.images) { 781 p._imgs = e.images; 782 p.after(dsImagesEl(e.images)); 783 } else if (e.step === "write") { 784 p.classList.remove("live"); p.classList.add("done"); 785 const n = new Set(p._sites.map((x) => x.host)).size; 786 p.querySelector(".ds-label").textContent = n ? T("Ø¨ØØ« ÙÙ") + " " + n + " " + T(n === 1 ? "Ù ÙÙØ¹" : "Ù ÙØ§Ùع") : T("٠ا ÙÙÙØª ÙØªØ§Ø¦Ø¬ Ù ÙÙØ¯Ø©"); 787 } 788 els.statusText.textContent = T("Ø¬Ø§Ø±Ù Ø§ÙØ¨ØØ« باÙÙÙØ¨â¦"); 789 scrollDown(); 790 } 791 function dsSummary(e) { 792 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 793 const box = document.createElement("div"); 794 if (e.dsSites) { 795 const d = document.createElement("div"); d.className = "ds-panel done"; 796 d.innerHTML = '<div class="ds-line"><span class="ds-globe"></span><span class="ds-label">' + esc(T("Ø¨ØØ« ÙÙ") + " " + e.dsSites + " " + T(e.dsSites === 1 ? "Ù ÙÙØ¹" : "Ù ÙØ§Ùع")) + '</span></div><div class="ds-sites">' + dsSitesHtml(e.dsSiteList || []) + "</div>"; 797 box.appendChild(d); 798 } 799 if (e.dsImages && e.dsImages.length) box.appendChild(dsImagesEl(e.dsImages)); 800 return box; 801 } 802 function aiParts(div) { 803 return { 804 root: div, thinking: div.querySelector(".thinking"), thinkingBody: div.querySelector(".thinking-body"), 805 thinkingLabel: div.querySelector(".t-label"), typing: div.querySelector(".typing"), 806 notes: div.querySelector(".notes"), slot: div.querySelector(".slot") 807 }; 808 } 809 function addVersionCard(ui, index, kind) { 810 const v = session.versions[index]; 811 if (!v) return; 812 const btn = document.createElement("button"); 813 btn.type = "button"; 814 btn.className = "version-card" + (index === session.current ? " active" : ""); 815 btn.dataset.v = index; 816 const lines = v.code.split("\n").length; 817 btn.innerHTML = `<span class="version-icon">${icon(kind === "patch" ? "patch" : "file")}</span> 818 <span><span class="version-title">اÙÙØ³Ø®Ø© ${index + 1}${kind === "patch" ? " · تعدÙ٠ذÙÙ" : ""}</span> 819 <span class="version-meta">index.html · ${lines} سطر</span></span>`; 820 btn.addEventListener("click", () => selectVersion(index)); 821 const fb = document.createElement("div"); 822 fb.className = "fb-row"; 823 fb.innerHTML = '<button type="button" data-good="1" title="ÙØªÙجة ٠٠تازة" aria-label="ÙØªÙجة ٠٠تازة">ð</button><button type="button" data-good="0" title="Ø¨Ø¯ÙØ§ ØªØØ³ÙÙ" aria-label="Ø¨Ø¯ÙØ§ ØªØØ³ÙÙ">ð</button>'; 824 if (v.rated != null) fb.querySelector(`[data-good="${v.rated ? 1 : 0}"]`).classList.add("on"); 825 fb.querySelectorAll("button").forEach((b) => b.addEventListener("click", () => { 826 if (v.rated != null) return toast("ÙÙÙ٠ت ÙØ§ÙÙØ³Ø®Ø© Ù Ù ÙØ¨Ù"); 827 v.rated = b.dataset.good === "1"; persist(); 828 b.classList.add("on"); 829 api("/api/feedback", { uid, model: v.model, mode: v.mode, good: v.rated }); 830 toast(v.rated ? "Ø´ÙØ±Ø§Ù! ð" : "ØªÙ Ø§Ù Ø Ø¬Ø±ÙØ¨ Â«Ø£ÙØ®Ù » أ٠اÙÙØ¶Ø¹ Ø§ÙØ£ÙصÙ"); 831 })); 832 ui.slot.appendChild(fb); 833 ui.slot.appendChild(btn); 834 } 835 function addMeta(ui, m) { 836 const row = document.createElement("div"); 837 row.className = "msg-meta"; 838 const chips = []; 839 if (m.refunded) chips.push(`<span class="meta-chip good">٠ا Ø§ÙØØ³Ø¨ ٠٠استخدا٠Ù</span>`); 840 else if (m.cost != null && me && me.usage && me.usage.cap) chips.push(`<span class="meta-chip usage-chip">${(Math.round((m.cost / me.usage.cap) * 1000) / 10).toLocaleString("en")}% استخدا٠</span>`); 841 if (m.tokens) chips.push(`<span class="meta-chip">${m.tokens.toLocaleString("en")} تÙÙÙ</span>`); 842 if (m.ms) chips.push(`<span class="meta-chip">${(m.ms / 1000).toFixed(0)} Ø«</span>`); 843 if (m.patches) chips.push(`<span class="meta-chip good">${m.patches.ok} تعدÙ٠٠طبÙÙ${m.patches.fail ? ` · ${m.patches.fail} ÙØ´Ù` : ""}</span>`); 844 if (m.photos) chips.push(`<span class="meta-chip">${m.photos} ØµÙØ±Ø©</span>`); 845 if (m.critique) chips.push(`<span class="meta-chip good">اÙÙØ§Ùد ØØ³ÙÙ ${m.critique}</span>`); 846 if (m.continued) chips.push(`<span class="meta-chip good">ÙÙ Ù٠تÙÙØ§Ø¦Ùا٠Ã${m.continued}</span>`); 847 row.innerHTML = chips.join(""); 848 ui.slot.appendChild(row); 849 } 850 function noticeEl(n) { 851 const d = document.createElement("div"); 852 d.className = "notice " + (n.kind || "error"); 853 d.innerHTML = `${icon(n.kind === "credits" ? "wallet" : "shield")}<div><b>${esc(n.title || "")}</b>${esc(n.text || "")}${n.actions ? `<div class="notice-actions">${n.actions}</div>` : ""}</div>`; 854 d.querySelectorAll("[data-switch]").forEach((b) => b.addEventListener("click", () => { setMode(b.dataset.switch); els.input.focus(); })); 855 d.querySelectorAll("[data-retry-image]").forEach((b) => b.addEventListener("click", () => { b.disabled = true; generateImage(b.dataset.retryImage); })); 856 d.querySelectorAll("[data-retry-text]").forEach((b) => b.addEventListener("click", () => { b.disabled = true; send(b.dataset.retryText); })); 857 return d; 858 } 859 function addNotice(ui, n) { ui.slot.appendChild(noticeEl(n)); } 860 function warningCard(w) { 861 const d = document.createElement("div"); 862 d.className = "warning-card"; 863 d.setAttribute("role", "alert"); 864 d.innerHTML = `<div class="warning-inner"> 865 <div class="warning-icon">${icon("shield")}</div> 866 <div> 867 <div class="warning-title">${esc(w.title || "Ø·ÙØ¨ ٠رÙÙØ¶")}</div>
868 <span class="warning-cat">${esc(CATEGORY_NAMES[w.category] || "٠عاÙÙØ± Ø£Ø®ÙØ§ÙÙØ©")}</span> 869 <p class="warning-reason">${esc(w.reason || "")}</p> 870 <p class="warning-foot">${icon("coin")}٠ا Ø§ÙØØ³Ø¨ ٠٠استخدا٠٠شÙ.</p> 871 </div></div>`; 872 return d; 873 } 874 // "two pictures â you pick": both drawn from the same request, the choice is saved as a preference pair (trains NanoSpark) 875 function pairCard(e, row) { 876 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 877 const box = document.createElement("div"); box.className = "img-pair"; 878 box.innerHTML = '<div class="img-pair-head"><b>' + esc(T("Ø£Ù ÙØØ¯Ø© Ø£ØÙÙØ")) + '</b><span>' + esc(T("Ø§Ø®ØªÙØ§Ø±Ù Ø¨ÙØ¹ÙÙÙ NanoSpark ذÙÙÙ")) + '</span></div><div class="img-pair-row"></div>'; 879 const rowEl = box.querySelector(".img-pair-row"); 880 ["a", "b"].forEach((k) => { 881 const col = document.createElement("div"); col.className = "img-pair-col"; 882 col.appendChild(imageCard(e[k])); 883 const pick = document.createElement("button"); pick.type = "button"; pick.className = "img-pick"; pick.textContent = T(k === "a" ? "ÙØ§Ù Ø£ØÙÙ â" : "â ÙØ§Ù Ø£ØÙÙ"); 884 pick.addEventListener("click", async () => { 885 const win = e[k], lose = e[k === "a" ? "b" : "a"]; 886 api("/api/image-pref", { uid, prompt: win.prompt, win: win.url, lose: lose.url, size: win.size }).catch(() => {}); 887 Object.keys(e).forEach((x) => delete e[x]); Object.assign(e, win); // the chat keeps only the chosen picture 888 persist(); 889 const fresh = renderEntry(e); row.replaceWith(fresh); fresh.classList.add("fresh-img"); 890 toast(T("Ø´ÙØ±Ø§Ù! NanoSpark تعÙÙÙ Ù Ù Ø§Ø®ØªÙØ§Ø±Ù")); 891 }); 892 col.appendChild(pick); rowEl.appendChild(col); 893 }); 894 return box; 895 } 896 // open-ended requests (writing, names, ideas, "which is better") have no single right answer: 897 // Hythm writes a second take with a different angle and the user picks â the choice teaches Hythm 898 const DUAL_RE = /(Ø§ÙØªØ¨(ÙÙ|Ù)?\s|Ø§ÙØªØ±Ø|Ø£ÙÙØ§Ø±|اÙÙØ§Ø±|ÙÙØ±[Ø©Ù]|اس٠|أس٠اء|اس٠اء|ÙØµÙد|شعر |ÙØµ[Ø©Ù]|خاطر[Ø©Ù]|رساÙ[Ø©Ù]|ÙØ§Ø¨Ø´Ù|Ø¨ÙØ³Øª|Ù ÙØ´Ùر|عÙÙØ§Ù|سÙÙØºØ§Ù|شعار ÙØµÙ|Ø£ØØ³Ù|Ø§ØØ³Ù|Ø£ÙØ¶Ù|Ø§ÙØ¶Ù|رأÙÙ|راÙÙ|ÙØµÙØ[Ø©Ù]|\bÙÙØ§\b|suggest|ideas?\b|names?\b|write me|poem|story|caption|slogan|which is better|recommend)/i; 899 const NOT_DUAL_RE = /(```|ÙÙØ¯|code|بر٠ج|script|Ø³ÙØ±Ø¨Øª|Ù ÙÙØ¹|website|html|css|python|javascript|sql|Ø±ÙØ§Ø¶Ùات|٠عادÙ|Ø§ØØ³Ø¨|ØÙ اÙ٠سأÙ|ترج٠|translate)/i; 900 async function offerAlt(text, centry, fin) { 901 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 902 const note = document.createElement("div"); note.className = "ans-alt-note"; note.textContent = T("⦠Hythm Ù ØØªØ§Ø±â¦ ع٠ÙÙØªØ¨ Ø¬ÙØ§Ø¨ ثاÙÙ ÙØªØ®ØªØ§Ø±"); 903 fin.appendChild(note); 904 let r; try { r = await api("/api/alt-answer", { uid, prompt: text, first: visibleAnswer(centry.md).slice(0, 6000) }); } catch { r = null; } 905 note.remove(); 906 if (!r || !r.ok || !r.data.md || !fin.isConnected) return; 907 const first = centry.md, second = r.data.md; 908 const pair = document.createElement("div"); pair.className = "msg msg-ai"; 909 pair.innerHTML = '<div class="ans-pair"><div class="ans-pair-head"><b>' + esc(T("Hythm Ù ØØªØ§Ø± بÙÙ Ø¬ÙØ§Ø¨ÙÙ â Ø£Ù ÙØ§ØØ¯ Ø£ØØ³ÙØ")) + '</b><span>' + esc(T("Ø§Ø®ØªÙØ§Ø±Ù Ø¨ÙØ¹ÙÙÙ Hythm Ø´Ù Ø¨ØªØØ¨")) + '</span></div><div class="ans-pair-row"></div></div>'; 910 const row = pair.querySelector(".ans-pair-row");
911 [first, second].forEach((md, k) => { 912 const col = document.createElement("div"); col.className = "ans-pair-col"; 913 col.innerHTML = '<span class="ans-pair-tag">' + esc(T(k ? "Ø§ÙØ¬Ùاب 2" : "Ø§ÙØ¬Ùاب 1")) + '</span><div class="notes"></div>'; 914 renderMdInto(col.querySelector(".notes"), visibleAnswer(md)); 915 const b = document.createElement("button"); b.type = "button"; b.className = "ans-pick"; b.textContent = T("ÙØ§Ø¯ Ø£ØØ³Ù â"); 916 b.addEventListener("click", () => { 917 api("/api/answer-pref", { uid, prompt: text, win: visibleAnswer(k ? second : first).slice(0, 4000), lose: visibleAnswer(k ? first : second).slice(0, 4000) }).catch(() => {}); 918 centry.md = k ? second : first; persist(); 919 const d = renderEntry(centry); d.classList.add("settled"); pair.replaceWith(d); 920 toast(T("Ø´ÙØ±Ø§Ù! Hythm تعÙÙÙ Ù Ù Ø§Ø®ØªÙØ§Ø±Ù")); 921 }); 922 col.appendChild(b); row.appendChild(col); 923 }); 924 fin.replaceWith(pair); scrollDown(); 925 } 926 function pushLog(e) { session.log.push(e); session.log = session.log.slice(-60); persist(); } 927 928 function enterWarning() { 929 document.body.classList.add("is-warning"); 930 clearTimeout(enterWarning.t); 931 enterWarning.t = setTimeout(() => document.body.classList.remove("is-warning"), 6000); 932 } 933 934 /* ---------------- Send ---------------- */ 935 function setBusy(busy) { 936 els.sendBtn.classList.toggle("stop", busy); 937 els.sendBtn.querySelector(".i-send").toggleAttribute("hidden", busy); // SVG has no .hidden property: use the attribute 938 els.sendBtn.querySelector(".i-stop").toggleAttribute("hidden", !busy); 939 els.sendBtn.setAttribute("aria-label", busy ? "Ø¥ÙÙØ§Ù" : "إرساÙ"); 940 els.statusPill.hidden = !busy; 941 els.statusPill.classList.toggle("max", busy && mode === "max"); 942 } 943 944 // ---- attachments ---- 945 let attachments = []; // [{ data: dataURL (â¤1280px jpeg), thumb: small dataURL }] 946 function shrink(file, max) { 947 return new Promise((resolve, reject) => { 948 const img = new Image(), url = URL.createObjectURL(file); 949 img.onload = () => { 950 const k = Math.min(1, max / Math.max(img.width, img.height)); 951 const c = document.createElement("canvas"); c.width = Math.round(img.width * k); c.height = Math.round(img.height * k); 952 c.getContext("2d").drawImage(img, 0, 0, c.width, c.height); URL.revokeObjectURL(url); 953 resolve(c.toDataURL("image/jpeg", 0.86)); 954 }; 955 img.onerror = () => { URL.revokeObjectURL(url); reject(new Error("bad image")); }; 956 img.src = url; 957 }); 958 } 959 async function addAttachments(files) { 960 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 961 for (const f of [...files]) { 962 // a meeting / voice recording, or a Zoom transcript â transcribe and summarise 963 if (/^(audio|video)\//i.test(f.type) || /\.(m4a|mp3|mp4|mov|webm|wav|ogg|opus|flac|mkv|vtt|srt)$/i.test(f.name)) { meetingFile(f); continue; } 964 if (!/^image\/(png|jpe?g|webp|gif|bmp)$/i.test(f.type)) { 965 if (attachments.filter((a) => a.file).length >= 3) { toast(T("ÙØØ¯ 3 Ù ÙÙØ§Øª Ø¨Ø§ÙØ±Ø³Ø§ÙØ©")); continue; } 966 try { const text = await readFileText(f); if (!text.trim()) throw new Error("empty"); attachments.push({ file: true, name: f.name, text: text.slice(0, 60000) }); } 967 catch { toast(T("٠ا ÙØ¯Ø±Øª Ø£ÙØ±Ø£ اÙÙ ÙÙ")); } 968 continue; 969 } 970 if (attachments.length >= 4) { toast(T("ÙØØ¯ 4 ØµÙØ± Ø¨Ø§ÙØ±Ø³Ø§ÙØ©")); break; } 971 if (f.size > 15e6) { toast(T("Ø§ÙØµÙرة ÙØ¨Ùرة ÙØ«Ùر")); continue; } 972 try { attachments.push({ data: await shrink(f, 1280), thumb: await shrink(f, 220) }); } catch { toast(T("٠ا ÙØ¯Ø±Øª Ø£ÙØ±Ø£ Ø§ÙØµÙرة")); } 973 } 974 renderAttachments(); 975 } 976 /* ---------- Meetings & voice notes: recording â transcript â summary, decisions and tasks ---------- */ 977 const MEETING_ASK = "ÙØ®Ùص ÙØ°Ø§ Ø§ÙØ§Ø¬ØªÙ اع/Ø§ÙØªØ³Ø¬ÙÙ Ø¨Ø§ÙØ¹Ø±Ø¨Ù بشÙÙ Ø§ØØªØ±Ø§ÙÙ: 1) Ù ÙØ®Øµ ÙØµÙر (3-5 أسطر) 2) Ø£Ù٠اÙÙÙØ§Ø· 3) اÙÙØ±Ø§Ø±Ø§Øª 4) اÙÙ ÙØ§Ù : اÙÙ ÙÙ Ø© · اÙ٠سؤÙÙ (إذا Ø§ÙØ°Ùر) · اÙÙ ÙØ¹Ø¯ (إذا Ø§ÙØ°Ùر) بجدÙÙ 5) Ø£Ø³Ø¦ÙØ© Ø£Ù ÙÙØ§Ø· Ù ÙØªÙØØ© 6) Ø£Ù٠اÙÙ ÙØ§Ø·Ø¹ ٠ع تÙÙÙØªÙا [mm:ss]. ث٠اعرض أ٠أجÙÙØ² Ù ØØ¶Ø± Ø§ÙØ§Ø¬ØªÙ اع ÙÙ ÙÙ Word Ø£Ù PDF."; 978 function parseCaptions(text) { // .vtt / .srt â "[mm:ss] line" (speaker names kept) 979 const out = []; 980 for (const block of String(text).replace(/\r/g, "").split(/\n{2,}/)) { 981 const m = /(\d{1,2}:)?(\d{1,2}):(\d{2})[.,]\d{3}\s*-->/.exec(block); if (!m) continue; 982 const lines = block.split("\n").filter((l) => l.trim() && !/-->/.test(l) && !/^\d+$/.test(l.trim()) && !/^WEBVTT/.test(l)); 983 const txt = lines.join(" ").replace(/<[^>]+>/g, "").trim(); if (!txt) continue;
984 const hh = m[1] ? parseInt(m[1]) : 0, stamp = (hh ? hh + ":" + m[2].padStart(2, "0") : String(parseInt(m[2]))) + ":" + m[3]; 985 if (out.length && out[out.length - 1].endsWith(txt)) continue; 986 out.push("[" + stamp + "] " + txt); 987 } 988 return out.join("\n"); 989 } 990 async function meetingFile(f) { 991 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 992 if (!me) return openLogin(); 993 const card = document.createElement("div"); card.className = "msg msg-ai"; 994 card.innerHTML = '<div class="meet-card no-i18n"><div class="mc-ico">ðï¸</div><div class="mc-body"><b>' + esc(f.name) + "</b><small>" + (f.size > 1e6 ? (f.size / 1e6).toFixed(1) + " MB" : Math.round(f.size / 1e3) + " KB") + '</small><div class="mc-step">' + esc(T("بجÙÙØ² Ø§ÙØªØ³Ø¬ÙÙâ¦")) + '</div><div class="av-bar"><i></i></div></div></div>'; 995 els.messages.appendChild(card); updateLayout(); scrollDown(); 996 const step = (t, p) => { card.querySelector(".mc-step").textContent = t; if (p != null) card.querySelector(".av-bar i").style.width = Math.round(p * 100) + "%"; }; 997 const fail = (t) => { card.querySelector(".meet-card").classList.add("err"); step("â " + t, 1); }; 998 let transcript = ""; 999 try { 1000 if (/\.(vtt|srt)$/i.test(f.name)) { // Zoom already wrote the words: no audio work at all 1001 transcript = parseCaptions(await f.text()); 1002 if (!transcript) return fail(T("٠ا ÙÙÙØª ÙÙØ§Ù باÙÙ ÙÙ")); 1003 } else { 1004 if (f.size > 600e6) return fail(T("اÙÙ ÙÙ Ø£ÙØ¨Ø± Ù Ù 600MB â Ø§Ø±Ùع Ù ÙÙ Ø§ÙØµÙت بس (Audio only) ٠٠زÙÙ ")); 1005 // upload with progress (XHR), then read the server's progress events 1006 const text = await new Promise((ok, no) => { 1007 const x = new XMLHttpRequest(); 1008 x.open("POST", "/api/transcribe"); x.setRequestHeader("x-uid", uid); x.setRequestHeader("x-name", encodeURIComponent(f.name).slice(-80)); 1009 x.upload.onprogress = (e) => e.lengthComputable && step(T("Ø¹Ù Ø§Ø±ÙØ¹ Ø§ÙØªØ³Ø¬ÙÙâ¦") + " " + Math.round((e.loaded / e.total) * 100) + "%", (e.loaded / e.total) * 0.35); 1010 let seen = 0; 1011 x.onprogress = () => { 1012 const chunk = x.responseText.slice(seen); seen = x.responseText.length; 1013 for (const part of chunk.split("\n\n")) { 1014 if (!part.startsWith("data: ")) continue; 1015 let ev; try { ev = JSON.parse(part.slice(6)); } catch { continue; } 1016 if (ev.stage === "prepare") step(T("ع٠استخرج Ø§ÙØµÙتâ¦"), 0.4); 1017 if (ev.stage === "transcribe") step(T("Ø¹Ù ÙØ±Ùغ اÙÙÙØ§Ù â¦") + " " + ev.done + "/" + ev.total, 0.4 + 0.55 * (ev.done / ev.total)); 1018 if (ev.credits != null) setCredits(ev.credits, true); 1019 } 1020 }; 1021 x.onload = () => ok(x.responseText); x.onerror = () => no(new Error(T("اÙÙØ·Ø¹ Ø§ÙØ§ØªØµØ§Ù"))); 1022 x.send(f); 1023 }); 1024 const evs = text.split("\n\n").filter((p) => p.startsWith("data: ")).map((p) => { try { return JSON.parse(p.slice(6)); } catch { return {}; } }); 1025 const err = evs.find((e) => e.error), fin = evs.find((e) => e.done === true); // progress events carry a numeric "done" too 1026 if (err) return fail(err.error === "credits" ? T("Ø®ÙØµ استخدا٠٠ÙÙØ§ÙÙ5 ساعات") + " (" + err.need + ")" : (err.reason || err.error)); 1027 if (!fin || !fin.transcript) return fail(T("٠ا ÙØ¯Ø±Øª Ø£ÙØ±Ùغ Ø§ÙØªØ³Ø¬ÙÙ")); 1028 transcript = fin.transcript; 1029 } 1030 step("â " + T("Ø§ÙØªÙØ±ÙØº Ø¬Ø§ÙØ²") + " · " + transcript.split("\n").length + " " + T("سطر"), 1); 1031 card.querySelector(".meet-card").classList.add("ok"); 1032 const dl = document.createElement("button"); dl.type = "button"; dl.className = "mini-btn"; dl.textContent = "⬠" + T("ÙØ²ÙÙ Ø§ÙØªÙØ±ÙØº"); 1033 dl.onclick = () => { const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob(["\uFEFF" + transcript], { type: "text/plain;charset=utf-8" })); a.download = f.name.replace(/\.[^.]+$/, "") + "-transcript.txt"; a.click(); }; 1034 card.querySelector(".mc-body").appendChild(dl); 1035 const typed = els.input.value.trim(); els.input.value = ""; autoGrow(); 1036 send(typed || MEETING_ASK, { task: "chat", files: [{ name: "meeting-transcript.txt", text: transcript }] }); 1037 } catch (e) { fail(String(e.message || e)); } 1038 } 1039 async function readFileText(f) { 1040 if (f.size > 20e6) throw new Error("big"); 1041 if (/pdf$/i.test(f.type) || /\.pdf$/i.test(f.name)) { 1042 const pdfjs = await import("https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.min.mjs"); 1043 pdfjs.GlobalWorkerOptions.workerSrc = "https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.mjs"; 1044 const doc = await pdfjs.getDocument({ data: await f.arrayBuffer() }).promise; 1045 let out = ""; 1046 for (let p = 1; p <= Math.min(doc.numPages, 60) && out.length < 60000; p++) { 1047 const c = await (await doc.getPage(p)).getTextContent(); 1048 out += "\n--- page " + p + " ---\n" + c.items.map((i) => i.str).join(" "); 1049 } 1050 return out; 1051 } 1052 return await f.text(); 1053 } 1054 // ---- smart paste: code becomes an attached file, links become preview cards (YouTube / websites) ---- 1055 const URL_RE = /\bhttps?:\/\/[^\s<>"'()\[\]]+[^\s<>"'()\[\].,Ø!?Ø:;]/gi; 1056 function codeLang(t) { 1057 const s = t.slice(0, 6000); 1058 if (/<\?php/.test(s)) return "php"; 1059 if (/^\s*<(!doctype|html|head|body|div|section|script|style|svg)\b/i.test(s) || /<\/(div|body|html|script)>/i.test(s)) return "html"; 1060 try { if (/^\s*[\[{]/.test(s)) { JSON.parse(t); return "json"; } } catch {} 1061 if (/game:GetService|\blocal\s+\w+\s*=|\bthen\b[\s\S]*\bend\b|\bfunction\s+\w+[.:]\w+\(/.test(s)) return "lua"; 1062 if (/^\s*(def |class \w+(\(.*\))?:|import \w+|from \S+ import|print\(|if __name__)/m.test(s)) return "py"; 1063 if (/#include\s*[<"]/.test(s)) return "cpp"; 1064 if (/using System;|namespace \w+|public (partial )?class \w+\s*:\s*MonoBehaviour/.test(s)) return "cs"; 1065 if (/public\s+(static\s+)?(final\s+)?class\s+\w+|System\.out\.println/.test(s)) return "java"; 1066 if (/^package \w+$/m.test(s) && /\bfunc\s/.test(s)) return "go"; 1067 if (/\bfn\s+\w+\s*\(|let mut |impl\s+\w+/.test(s)) return "rs"; 1068 if (/^\s*(select|insert into|update \w+ set|create (table|index)|delete from|alter table)\b/im.test(s)) return "sql"; 1069 if (/^#!\/(usr\/)?bin\/(env )?(ba|z)?sh|^\s*(sudo |apt(-get)? |npm |pip |c
1069d |export \w+=)/m.test(s)) return "sh"; 1070 if (/\binterface\s+\w+\s*\{|:\s*(string|number|boolean|any)\b|\bexport type\b/.test(s)) return "ts"; 1071 if (/\b(const|let|var|function|async|await|=>|console\.log|document\.|require\(|import .* from)\b/.test(s)) return "js"; 1072 if (/^\s*[.#@]?[\w-][\w\s,.#:>-]*\{[^}]*:[^}]*;/m.test(s)) return "css"; 1073 if (/^\s*[\w-]+:\s*.+$/m.test(s) && /^\s{2,}[\w-]+:/m.test(s)) return "yaml"; 1074 return ""; 1075 } 1076 function looksLikeCode(t) { 1077 const lines = t.split("\n"); if (lines.length < 6 && t.length < 400) return false; 1078 const code = lines.filter((l) => /[;{}]\s*$|^\s*(def|class|import|from|function|const|let|var|if|for|while|return|elif|else|local|end|public|private|#include|using|package|<\/?\w)|=>|==|\(\)|^\s{2,}\S/.test(l)).length; 1079 return !!codeLang(t) && (code / lines.length >= 0.35 || t.length > 3000); 1080 } 1081 function insertAtCursor(text) { 1082 const el = els.input, s = el.selectionStart ?? el.value.length, e = el.selectionEnd ?? el.value.length; 1083 el.value = el.value.slice(0, s) + text + el.value.slice(e); el.selectionStart = el.selectionEnd = s + text.length; autoGrow(); el.dispatchEvent(new Event("input")); 1084 } 1085 function addLink(url) { 1086 if (attachments.some((x) => x.link && x.url === url)) return; 1087 if (attachments.filter((x) => x.link).length >= 5) return toast((window.HythmI18n ? HythmI18n.t : (x) => x)("ÙØØ¯ 5 Ø±ÙØ§Ø¨Ø· Ø¨Ø§ÙØ±Ø³Ø§ÙØ©")); 1088 let host = ""; try { host = new URL(url).hostname.replace(/^www\./, ""); } catch { return; } 1089 const item = { link: true, url, host, loading: true, kind: /(^|\.)youtu(be\.com|\.be)$/.test(host) ? "youtube" : "web" }; 1090 attachments.push(item); renderAttachments(); 1091 fetch("/api/linkinfo?u=" + encodeURIComponent(url)).then((r) => (r.ok ? r.json() : null)).catch(() => null) 1092 .then((i) => { Object.assign(item, i || { title: host }, { loading: false }); renderAttachments(); }); 1093 } 1094 const viaImg = (u) => "/api/img?u=" + encodeURIComponent(u); 1095 function linkChip(l, i) { 1096 const rm = i == null ? "" : '<button type="button" data-rm="' + i + '" aria-label="remove">Ã</button>'; 1097 if (l.kind === "youtube") return '<div class="att att-link att-yt' + (l.loading ? " loading" : "") + '"><div class="al-thumb">' + (l.id || l.thumb ? '<img alt="" src="' + esc(viaImg(l.thumb || "https://i.ytimg.com/vi/" + l.id + "/mqdefault.jpg")) + '">' : "") + '<span class="al-play"></span>' + (l.duration ? '<span class="al-dur">' + esc(l.duration) + "</span>" : "") + '</div><div class="al-meta"><b dir="auto">' + esc(l.loading ? "â¦" : l.title || "YouTube") + '</b><small><i class="al-yt"></i>' + esc(l.channel || "YouTube") + "</small></div>" + rm + "</div>"; 1098 return '<div class="att att-link att-web' + (l.loading ? " loading" : "") + '">' + (l.image ? '<div class="al-img"><img alt="" src="' + esc(viaImg(l.image)) + '" onerror="this.parentNode.remove()"></div>' : "") + '<div class="al-meta"><b dir="auto">' + esc(l.loading ? l.host : l.title || l.host) + '</b><small><img class="al-fav" alt="" src="' + esc(viaImg("https://icons.duckduckgo.com/ip3/" + (l.host || "") + ".ico")) + '" onerror="this.remove()">' + esc(l.site || l.host || "") + "</small></div>" + rm + "</div>"; 1099 } 1100 function renderAttachments() { 1101 const box = $("attachRow"); if (!box) return; 1102 box.hidden = !attachments.length; 1103 box.innerHTML = attachments.map((a, i) => a.link ? linkChip(a, i) : a.file 1104 ? '<div class="att att-file' + (a.pasted ? " att-code" : "") + '"><span class="att-ico">' + esc((a.name.split(".").pop() || "file").slice(0, 4).toUpperCase()) + '</span><span class="att-fm"><b>' + esc(a.name) + "</b>" + (a.lines ? "<small>" + a.lines + " " + esc((window.HythmI18n ? HythmI18n.t : (x) => x)("سطر")) + "</small>" : "") + '</span><button type="button" data-rm="' + i + '" aria-label="remove">Ã</button></div>' 1105 : '<div class="att"><img src="' + a.thumb + '" alt=""><button type="button" data-rm="' + i + '" aria-label="remove">Ã</button></div>').join(""); 1106 box.querySelectorAll("[data-rm]").forEach((b) => b.addEventListener("click", () => { attachments.splice(+b.dataset.rm, 1); renderAttachments(); })); 1107 els.composer && els.composer.classList.toggle("has-att", attachments.length > 0); 1108 } 1109 if ($("attachBtn")) { 1110 $("attachBtn").addEventListener("click", () => $("attachInput").click()); 1111 $("attachInput").addEventListener("change", (e) => { addAttachments(e.target.files); e.target.value = ""; }); 1112 els.input.addEventListener("paste", (e) => { 1113 const fs = [...(e.clipboardData?.files || [])].filter((f) => f.type.startsWith("image/")); if (fs.length) { e.preventDefault(); addAttachments(fs); return; } 1114 const t = e.clipboardData?.getData("text/plain") || ""; if (!t.trim()) return; 1115 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 1116 if (looksLikeCode(t)) { // a script / code block â an attached file, the text box stays clean 1117 e.preventDefault(); 1118 if (attachments.filter((x) => x.file).length >= 3) return toast(T("ÙØØ¯ 3 Ù ÙÙØ§Øª Ø¨Ø§ÙØ±Ø³Ø§ÙØ©")); 1119 const ext = codeLang(t) || "txt", n = attachments.filter((x) => x.pasted).length + 1; 1120 attachments.push({ file: true, pasted: true, name: (ext === "json" ? "data" : ext === "html" ? "page" : ext === "css" ? "style" : "script") + (n > 1 ? "-" + n : "") + "." + ext, text: t.slice(0, 60000), lines: t.split("\n").length }); 1121 renderAttachments(); toast(T("Ø§ÙØ¶Ø§Ù ÙÙ Ù٠٠رÙÙ") + " · " + ext.toUpperCase()); 1122 return; 1123 } 1124 const urls = t.match(URL_RE); 1125 if (urls && urls.length <= 5) { // links â preview cards above the box 1126 e.preventDefault(); 1127 const rest = t.replace(URL_RE, " ").replace(/[ \t]{2,}/g, " ").trim(); 1128 if (rest) insertAtCursor(rest);
1129 urls.forEach(addLink); 1130 } 1131 }); 1132 const zone = els.composer; 1133 ["dragenter", "dragover"].forEach((t) => zone.addEventListener(t, (e) => { if ([...(e.dataTransfer?.types || [])].includes("Files")) { e.preventDefault(); zone.classList.add("drop"); } })); 1134 ["dragleave", "drop"].forEach((t) => zone.addEventListener(t, () => zone.classList.remove("drop"))); 1135 zone.addEventListener("drop", (e) => { if (e.dataTransfer?.files?.length) { e.preventDefault(); addAttachments(e.dataTransfer.files); } }); 1136 } 1137 1138 // games, 3D scenes, animations and scripts: written in the chat where they play live (one page, a fraction of the credits) 1139 const GAME_RE = /(٠جسÙ?Ù |٠جس٠ات|blender|بÙÙØ¯Ø±|\bglb\b|Ù ÙØ´Ù|motion ?graphics?|اÙÙÙ ÙØ´Ù|Ø£ÙÙÙ ÙØ´Ù|Ø§ÙØªØ±Ù|logo reveal|ÙØ¹Ø¨[Ø©Ù]|Ø§ÙØ¹Ø§Ø¨|Ø£ÙØ¹Ø§Ø¨|\bgames?\b|\b3d\b|Ø«ÙØ§Ø«Ù(Ø©)? Ø§ÙØ£Ø¨Ø¹Ø§Ø¯|Ø«ÙØ§Ø«Ù Ø§ÙØ§Ø¨Ø¹Ø§Ø¯|three\.?js|Ø³ÙØ±Ø¨Øª|Ø³ÙØ±Ùبت|\bscript\b|Ù ØØ§Ùا[Ø©Ù]|simulation|اÙÙ ÙØ´Ù|Ø£ÙÙ ÙØ´Ù|animation|subway|\bpool\b|بÙÙØ§Ø±Ø¯Ù|سÙÙÙ|\bsnake\b|tetris|ØªØªØ±ÙØ³|flappy)/i; 1140 async function send(text, opts = {}) { 1141 // console commands 1142 if (/^\s*\/(usage|usge|usag|stats|Ø§ÙØ§Ø³ØªÙÙØ§Ù)\s*$/i.test(text || "")) { if (window.HythmUsage) HythmUsage.open(); return; } 1143 if (!me) return openLogin(); 1144 const linkAtt = attachments.filter((a) => a.link); 1145 if (linkAtt.length && !opts.links) { 1146 text = ((text || "").trim() + "\n" + linkAtt.map((l) => l.url).join("\n")).trim(); 1147 opts = { ...opts, links: linkAtt.map(({ kind, url, title, channel, duration, thumb, host, id, site, image }) => ({ kind, url, title, channel, duration, thumb, host, id, site, image })) }; 1148 if (!opts.task && linkAtt.some((l) => l.kind === "youtube")) opts.task = "chat"; 1149 attachments = attachments.filter((a) => !a.link); renderAttachments(); 1150 } 1151 const images = opts.images || attachments.filter((a) => !a.file && !a.link).map((a) => a.data); 1152 const thumbs = opts.thumbs || attachments.filter((a) => !a.file && !a.link).map((a) => a.thumb); 1153 const files = opts.files || attachments.filter((a) => a.file).map((a) => ({ name: a.name, text: a.text })); 1154 if (files.length && !images.length) { opts = { ...opts, task: "chat", files }; attachments = []; renderAttachments(); } 1155 if (files.length) opts = { ...opts, files }; 1156 const EDIT_RE = /(Ø«ÙØ§Ø«Ù(Ø©)? Ø§ÙØ£Ø¨Ø¹Ø§Ø¯|Ø«ÙØ§Ø«Ù Ø§ÙØ§Ø¨Ø¹Ø§Ø¯|\b3d\b|زاÙÙ(Ø©|Ù)|٠جسÙ?Ù |صÙÙ?ر(Ù|ÙØ§)|(ا|Ø¥)?جعÙ(ÙØ§|Ù|ÙÙ)?\s|تبدÙÙ|تبدÙÙ|تبدÙ|استبدÙ|بدÙÙ(Ù|ÙØ§)|بدÙ(Ù|ÙØ§)|Ø®ÙÙÙ (Ù|Ù Ø«Ù)|اÙÙØ¨(Ù|ÙØ§)|ØÙÙÙ|ØÙÙ(ÙØ§|Ù|ÙÙ)�|Ø®ÙÙ?ÙÙØ§|Ø®ÙÙ?ÙÙ�|ØØ³Ù?Ù|ÙØ¶Ù?Ø|ÙØ¸Ù?Ù|صÙÙ?Ø (اÙ)?ØµÙØ±|Ø§Ø±ÙØ¹ (اÙ)?Ø¬ÙØ¯|ÙÙÙ?Ù|غÙÙ?ر|ÙØ§ÙعÙ|ÙØ±ØªÙÙÙ|اÙÙ Ù|Ø£ÙÙ Ù|Ø¯ÙØ²ÙÙ|بÙÙØ³Ø§Ø±|�(make (it|this|the photo)|turn (it|this)|enhance|upscale|restore|colori[sz]e|realistic|cartoon|anime|pixar|ghibli|remove the background)�|Ø®Ù(Ù|ÙÙ)ÙÙ|Ø®ÙÙÙ|Ø®ÙÙÙØ§|ØØ·(ÙÙ|Ù|ÙØ§)?|ضعÙÙ|ضعÙ|اجعÙ(ÙÙ)?|صÙÙØ±ÙÙ|ØµÙØ±ÙÙ|ارس٠ÙÙ|ارس٠(Ù|ÙØ§)|اع٠Ù(Ù|ÙÙ)? (ØµÙØ±|ÙÙ ØµÙØ±)|رÙÙØ¨|Ø±ÙØ¨|غÙÙØ± (اÙ)?Ø®ÙÙÙ|بدÙÙ|بد٠(اÙ)?Ø®ÙÙÙ|ÙØ¯Ø§Ù |أ٠ا٠|Ø¬ÙØ¨|Ø¨Ø¬Ø§ÙØ¨|٠ع (Ø³ÙØ§Ø±|ØØµØ§Ù|أسد|Ù٠ر)|Ø¨ÙØ¨Ø³|ÙØ§Ø¨Ø³|بستاÙÙ|ÙØ±ØªÙÙ|Ø£ÙÙ Ù|ثر٠دÙ|\b(put|place|make (me|him|her|it)|turn (me|him|her|it|this)|in front of|next to|change the background|wearing|as a|in the style of)\b)/i; 1157 if (images.length && (task === "image" || EDIT_RE.test(text))) { 1158 attachments = []; renderAttachments(); 1159 return generateImage(text, { refs: images, thumbs }); 1160 } 1161 if (images.length) { opts = { ...opts, task: "chat", images, thumbs }; attachments = []; renderAttachments(); } 1162 // games, 3D scenes and scripts are written in the chat, where they play live (the studio is for websites) 1163 if (!opts.task && task !== "image" && GAME_RE.test(text) && !/(Ù ÙÙØ¹|ØµÙØØ© ÙØ¨ÙØ·|landing|website)/i.test(text)) opts = { ...opts, task: "chat" }; 1164 if (false && /(ÙØ¹Ø¨[Ø©Ù]|Ø§ÙØ¹Ø§Ø¨|Ø£ÙØ¹Ø§Ø¨|\bgame\b|\b3d\b|Ø«ÙØ§Ø«Ù Ø§ÙØ£Ø¨Ø¹Ø§Ø¯|three\.?js|Ø³ÙØ±Ø¨Øª|Ø³ÙØ±Ùبت|\bscript\b|Ù ØØ§Ùا[Ø©Ù]|simulation|اÙÙ ÙØ´Ù|animation)/i.test(text) && !/(Ù ÙÙØ¹|ØµÙØØ© ÙØ¨ÙØ·|landing|website)/i.test(text)) opts = { ...opts, task: "chat" }; 1165 if (!opts.task && !(opts.files && opts.files.length) && task !== "design" && task !== "chat") { 1166 const lastOut = [...session.log].reverse().find((x) => x.type !== "user" && x.type !== "notice"); 1167 const lastUser = [...session.log].reverse().find((x) => x.type === "user"); 1168 const last = lastOut ? (lastOut.type === "image" ? "image" : isEditing() ? "site" : "chat") : ""; 1169 if (last) { 1170 const it = await api("/api/intent", { prompt: text, last, lastPrompt: lastOut.prompt || (lastUser && lastUser.text) || "" });
1171 const k = it.ok && it.data && it.data.smart ? it.data.task : ""; 1172 if (k === "chat") opts = { ...opts, task: "chat" }; 1173 else if (k === "image") return generateImage(text); 1174 else if (k === "edit_image" && lastOut.url) { 1175 try { 1176 const blob = await (await fetch(lastOut.url)).blob(); 1177 const data = await blobToDataURL(blob); 1178 return generateImage(text, { refs: [data], thumbs: [lastOut.url] }); 1179 } catch { return generateImage(text); } 1180 } 1181 else if (k === "design") { opts = { ...opts, task: "design" }; } 1182 else if (k === "edit_site") opts = { ...opts, task: "design" }; 1183 } 1184 } 1185 if (!opts.task && task === "image") { 1186 const ASK = /^\s*(ÙÙØ´|ÙÙÙ|Ù٠اذا|ÙÙ \s|Ø´Ù|Ø§ÙØ´|Ø¥ÙØ´|ÙØ´|ÙÙÙ|ÙÙ|Ù ÙÙ|ÙÙÙ|٠تÙ|ÙÙÙ|ÙÙÙ ÙÙ|اØÙÙÙÙ|اØÙÙ ÙÙ|اشرØ|ÙØ¶ÙØ|ÙØ¶Ø|ÙØ³Ùر|ÙØ³Ø±|٠ا رأÙÙ|ش٠رأÙÙ|why|what|how|who|explain|tell me|describe)(?=\s|$)|[?Ø]\s*$/i; 1187 const DRAW = /^\s*(ارس٠|ÙÙÙ?د|اع٠Ù(ÙÙ|Ù)?\s*ØµÙØ±|سÙÙ?Ù\s*ØµÙØ±|ص٠٠|draw|generate|make|create|paint)/i; 1188 if (ASK.test(text) && !DRAW.test(text)) opts = { ...opts, task: "chat" }; 1189 else return generateImage(text); 1190 } 1191 if (!opts.task && task === "auto" && $("webBtn").getAttribute("aria-pressed") === "true") opts = { ...opts, task: "chat" }; // web search = an answer, never a picture 1192 if (!opts.task && task === "auto" && !isEditing() && !(opts.files && opts.files.length)) { 1193 const it = await api("/api/intent", { prompt: text }); 1194 const k = it.ok && it.data && it.data.task; 1195 if (k === "image") return generateImage(text); 1196 if (k === "design" || k === "chat") opts = { ...opts, task: k }; 1197 } 1198 const history = chatHistory(); 1199 let isChat = false; 1200 const editing = !opts.images && opts.task !== "chat" && isEditing(); // a chat answer (e.g. a game) never rewrites the open website 1201 const cost = currentCost(); 1202 if (me.credits < cost) { 1203 const cheaper = Object.entries(cfg.modes).filter(([, m]) => (editing ? m.editCost : m.cost) <= me.credits).map(([k, m]) => `<button type="button" class="btn-soft" data-switch="${k}">Ø¬Ø±ÙØ¨ ${esc(m.label)}</button>`).join(""); 1204 renderEntry({ type: "notice", kind: "credits", title: "Ø®ÙØµ استخدا٠٠ÙÙØ§ÙÙ5 ساعات", text: ` Ø¨ÙØ±Ø¬Ø¹ ÙØ§Ù ٠بعد ${fmtReset((me.usage && me.usage.reset) || Date.now())}. Ø¬Ø±ÙØ¨ ÙØ¶Ø¹ Ø£Ø®ÙØ Ø£Ù Ø²ÙØ¯ ØØ¯Ù٠٠ع Plus.`, actions: cheaper }); 1205 scrollDown(); 1206 return; 1207 } 1208 document.body.classList.remove("is-warning"); 1209 const styleLabel = document.querySelector(".style-chip.active")?.textContent; 1210 const userEntry = { type: "user", text, mode, model, edit: editing, style, styleLabel, thumbs: opts.thumbs || [], fileNames: (opts.files || []).map((f) => f.name), links: opts.links || undefined }; 1211 renderEntry(userEntry); 1212 pushLog(userEntry); 1213 1214 const div = document.createElement("div"); 1215 div.className = "msg msg-ai"; 1216 div.innerHTML = aiShell(); 1217 els.messages.appendChild(div); 1218 const ui = aiParts(div); 1219 scrollDown(); 1220 1221 const baseVersion = editing ? session.current : -1; 1222 const baseCode = editing ? session.versions[session.current].code : ""; 1223 const prior = session.prior.slice(-3); 1224 1225 setBusy(true); 1226 if (editing || opts.task === "design" || isDesignText(text)) workStart(editing); 1227 els.statusText.textContent = mode === "max" ? "ÙÙÙÙØ± بع٠Ùâ¦" : "ÙØ¨Ø¯Ø£â¦"; 1228 controller = new AbortController(); 1229 let content = "", reasoning = "", lastRender = 0, done = null, streamError = null; 1230 const act = actStart(ui); 1231 const appEv = {}, appCards = {}; // apps Hythm used for this answer (Gmail, GitHub, YouTubeâ¦) and the cards they returned 1232 // typewriter: reveal what has arrived at a steady pace instead of jumping chunk by chunk 1233 const reveal = { shown: 0, raf: 0, last: 0 }; 1234 const nextFrame = (f) => (document.hidden ? setTimeout(f, 40) : requestAnimationFrame(f)); 1235 function revealTick() { 1236 reveal.raf = 0; 1237 const full = visibleAnswer(content), target = full.length; 1238 if (reveal.shown >= target) return; 1239 const gap = target - reveal.shown; 1240 reveal.shown = Math.min(target, reveal.shown + Math.max(2, Math.ceil(gap / 12))); 1241 const now = performance.now(); 1242 const every = target > 12000 ? 600 : target > 5000 ? 250 : target > 2000 ? 90 : 32;
1242 // long answers (games, big code) refresh less often 1243 if (now - reveal.last > every || reveal.shown === target) { reveal.last = now; smoothMd(ui.notes, full.slice(0, reveal.shown)); if (window.HythmCards && Object.keys(appCards).length) HythmCards.fill(ui.notes, appCards); scrollDown(); } 1244 reveal.raf = nextFrame(revealTick); 1245 } 1246 const started = Date.now(); 1247 1248 const live = (force) => { 1249 const now = Date.now(); 1250 if (!force && now - lastRender < (isChat ? 250 : 800)) return; 1251 lastRender = now; 1252 if (isChat) { if (!reveal.raf) reveal.raf = nextFrame(revealTick); return; } 1253 if (/^\s*<hythm-refuse/i.test(content)) return; 1254 ui.notes.innerHTML = renderNotes(notesOf(content)); 1255 const ex = extractHtml(content); 1256 if (ex && ex.html.trim()) { 1257 els.statusText.textContent = `ÙÙØªØ¨ اÙÙÙØ¯â¦ ${ex.html.split("\n").length} سطر`; 1258 requestImages(queriesIn(ex.html)); // prefetch photos while it writes â no preview refresh 1259 const secs = (ex.html.match(/<section\b/gi) || []).length; 1260 const phase = /<\/body>/i.test(ex.html) ? "ÙÙÙ ÙØ¹ Ø§ÙØªÙاصÙÙ Ø§ÙØ£Ø®Ùرةâ¦" : secs ? `ÙØ¨Ù٠اÙÙØ³Ù ${secs}â¦` : /<style/i.test(ex.html) ? "ÙØ±Ø³Ù اÙÙÙÙØ©: Ø£ÙÙØ§Ù ÙØ®Ø·ÙØ· ÙØØ±ÙØ©â¦" : "ÙØ¬ÙÙØ² اÙÙÙÙÙâ¦"; 1261 workUpdate(content.length, phase); 1262 } else if (/<{5,}\s*SEARCH/.test(content)) { 1263 els.statusText.textContent = `ÙØ¬ÙÙØ² Ø§ÙØªØ¹Ø¯ÙÙØ§Øªâ¦ ${parsePatches(content).length}`; 1264 workUpdate(content.length, `ÙØ·Ø¨ÙÙ ${parsePatches(content).length} تعدÙ٠ذÙÙâ¦`); 1265 } else { 1266 workUpdate(content.length, "ÙÙØªØ¨ اÙÙÙØ±Ø©â¦"); 1267 } 1268 scrollDown(); 1269 }; 1270 1271 try { 1272 const res = await fetch("/api/generate", { 1273 method: "POST", signal: controller.signal, headers: { "Content-Type": "application/json" }, 1274 body: JSON.stringify({ uid, temp: !!window.__hythmTemp, plugin: window.__hythmPlugin ? { id: window.__hythmPlugin.id } : null, memory: window.__hythmTemp ? "off" : appSettings.memory, persona: { tone: appSettings.tone, dialect: appSettings.dialect, format: appSettings.format, emoji: appSettings.emoji, instructions: (appSettings.instructions || "").slice(0, 1000) }, mode, model, prompt: text, style, code: baseCode, prior, task: opts.task || task, fullSite: fullSite && !baseCode, history, images: opts.images || [], files: opts.files || [], web: $("webBtn").getAttribute("aria-pressed") === "true" }) 1275 }); 1276 if (!res.ok) { 1277 let data = {}; try { data = await res.json(); } catch {} 1278 ui.root.remove(); workEnd(); 1279 if (res.status === 422 && data.blocked) { 1280 const w = { type: "warning", title: data.title, reason: data.reason, category: data.category }; 1281 renderEntry(w); pushLog(w); enterWarning(); 1282 } else if (res.status === 402 && data.error === "plan") { 1283 renderEntry({ type: "notice", kind: "credits", title: "Ù ÙØ²Ø© Ù Ù Hythm Plus", text: " " + (data.reason || "") }); 1284 if (window.HythmPlans) HythmPlans.open(data.model || ""); 1285 } else if (res.status === 402) { 1286 renderEntry({ type: "notice", kind: "credits", title: "Ø®ÙØµ استخدا٠٠ÙÙØ§ÙÙ5 ساعات", text: ` Ø¨ÙØ±Ø¬Ø¹ ÙØ§Ù ٠بعد ${fmtReset((me.usage && me.usage.reset) || Date.now())}.` }); 1287 if (window.HythmPlans && me && (!me.plan || me.plan.id === "free")) HythmPlans.nudge("credits"); 1288 } else if (res.status === 401) { 1289 if (await recoverAccount()) { ui.root.remove(); const last = [...els.messages.querySelectorAll(".msg-user")].pop(); if (last) last.remove(); session.log.pop(); setBusy(false); controller = null; workEnd(); return send(text, opts); } 1290 openLogin(); 1291 } else { 1292 const msg = { 429: "Ø¹ÙØ¯Ù Ø·ÙØ¨ Ø´ØºÙØ§Ù. استÙÙ ÙØ®Ùص.", 503: "Ø±ØµÙØ¯ اÙÙ ÙÙØ¹ Ø®ÙØµ. ØªÙØ§ØµÙ ٠ع Ø§ÙØ¥Ø¯Ø§Ø±Ø©." }[res.status] || data.error || `خطأ ${res.status}`; 1293 renderEntry({ type: "notice", kind: "error", title: "٠ا ÙØ¯Ø±Øª أبعت Ø§ÙØ·Ùب", text: " " + msg }); 1294 } 1295 scrollDown(); 1296 return; 1297 } 1298 1299 const reader = res.body.getReader(); 1300 const decoder = new TextDecoder(); 1301 let buffer = ""; 1302 while (true) { 1303 const { done: end, value }
1303 = await Shield.deadline(reader.read(), 90000, "stalled").catch((x) => { try { controller && controller.abort(); } catch {} streamError = (window.HythmI18n ? HythmI18n.t : (y) => y)("Ø§ÙØ±Ø¯ عÙÙØ ÙÙÙÙØªÙ ÙØ±Ø¬ÙعتÙ٠اÙÙØ±Ùدت. Ø¬Ø±ÙØ¨ Ù٠ا٠٠رة."); return { done: true }; }); 1304 if (end) break; 1305 buffer += decoder.decode(value, { stream: true }); 1306 const parts = buffer.split("\n\n"); 1307 buffer = parts.pop(); 1308 for (const part of parts) { 1309 if (!part.startsWith("data: ")) continue; 1310 let ev; try { ev = JSON.parse(part.slice(6)); } catch { continue; } 1311 if (ev.start) { setCredits(ev.credits, true); if (ev.kind === "chat") { isChat = true; workEnd(); els.statusPill.hidden = true; } } 1312 if (ev.ds) { dsEvent(ui, ev.ds); const d = ev.ds; actStep(act, "search", d.step === "plan" ? "Ø¨ÙØ®Ø·Ø· Ø§ÙØ¨ØØ«" : d.step === "visit" || d.step === "source" ? "بÙÙØ±Ø£ " + ((d.source && d.source.host) || "اÙ٠صادر") : d.step === "images" ? "Ø¨ÙØ¬Ù ع ØµÙØ±" : d.step === "write" ? "بÙÙØªØ¨ ٠٠اÙ٠صادر" : "Ø¨ÙØ¯ÙÙØ± باÙÙÙØ¨", (d.q || d.text || (d.source && d.source.title) || "") + "\n"); } 1313 if (ev.app && window.HythmCards) { appEv[ev.app.key] = ev.app; (ev.app.cards || []).forEach((c) => (appCards[c.cid] = c)); HythmCards.activity(ui, ev.app); actStep(act, "app", (ev.app.label || ev.app.name || "Ø¨ÙØ´ØªØºÙ عÙ٠تطبÙÙØ§ØªÙ"), (ev.app.query || "") + "\n"); scrollDown(); } 1314 if (ev.r) { 1315 workUpdate(0, "ÙÙÙÙØ± بع٠٠ÙÙØ®Ø·Ø· Ø§ÙØªØµÙ ÙÙ â¦"); 1316 reasoning += ev.r; 1317 actFromReasoning(act, ev.r); 1318 } 1319 if (ev.c) { 1320 if (!content) ui.typing.remove(); 1321 content += ev.c; 1322 if (content.length - (act.lastC || 0) > 160) { act.lastC = content.length; actFromContent(act, content); } 1323 live(false); 1324 } 1325 if (ev.phase === "plan") { workUpdate(0, "Omni Ultra ÙØ±Ø³Ù خطة Ø§ÙØ¥Ø®Ø±Ø§Ø¬ اÙÙÙÙâ¦"); actStep(act, "plan", "Ø¨ÙØ±Ø³Ù خطة Ø§ÙØªØµÙ ÙÙ ", "", true); } 1326 if (ev.phase === "build") { workUpdate(0, "Ø§ÙØ®Ø·Ø© Ø¬Ø§ÙØ²Ø©Ø ÙØ¨Ø¯Ø£ Ø§ÙØ¨Ùاءâ¦"); actStep(act, "site", "Ø¨ÙØ¨ÙÙ Ø§ÙØªØµÙ ÙÙ ", "", true); } 1327 if (ev.phase === "critique") { workUpdate(content.length, "اÙÙØ§Ùد اÙÙÙÙ ÙØ±Ø§Ø¬Ø¹ Ø§ÙØªØµÙ ÙÙ â¦", true); actStep(act, "check", "اÙÙ Ø¯ÙØ± اÙÙÙÙ Ø¨ÙØ±Ø§Ø¬Ø¹", "", true); } 1328 if (ev.phase === "refined") { workUpdate(content.length, "ÙØ·Ø¨ÙÙ Ø§ÙØªØØ³ÙÙØ§Øª اÙÙÙØ§Ø¦ÙØ©â¦", true); actStep(act, "fix", "Ø¨ÙØ·Ø¨ÙÙ Ø§ÙØªØØ³ÙÙØ§Øª", "", true); } 1329 if (ev.cont) els.statusText.textContent = `ÙÙÙ Ù٠تÙÙØ§Ø¦Ùا٠(${ev.cont})â¦ Ù Ø¬Ø§ÙØ§Ù`; 1330 if (ev.error) streamError = ev.error; 1331 if (ev.done) done = ev; 1332 } 1333 } 1334 } catch (err) { 1335 if (err.name !== "AbortError") streamError = err.message; 1336 } 1337 1338 // ---- settle ---- 1339 workUpdate(content.length, "ÙØ¬Ùب Ø§ÙØµÙر ÙÙØ´ØºÙÙ Hythm Guardâ¦", true); 1340 ui.typing.remove(); 1341 actFinish(act); 1342 setBusy(false); 1343 controller = null; 1344 if (done) { setCredits(done.credits, true); siteCredits = done.site; } 1345 else refreshMe(true); 1346 1347 if (isChat) { 1348 workEnd(); 1349 if (done?.reroute) { 1350 // the model asked for the design studio â re-send as a design (the chat was refunded) 1351 ui.root.remove(); 1352 const last = [...els.messages.querySelectorAll(".msg-user")].pop(); if (last) last.remove(); 1353 session.log.pop(); 1354 if (done.reroute === "edit" || (done.reroute === "image" && images.length)) return generateImage(text, { refs: images, thumbs }); 1355 if (GAME_RE.test(text)) return send(text, { task: "chat" }); // a game is written in the chat, never as a website 1356 return done.reroute === "image" ? generateImage(text) : send(text, { task: "design" }); 1357 } 1358 // let the typewriter finish before the final render, so nothing jumps 1359 await new Promise((r) => { const t0 = Date.now(); (function chk() { if (reveal.shown >= visibleAnswer(content).length || Date.now() - t0 > 4000) return r(); if (!reveal.raf) reveal.raf = nextFrame(revealTick); setTimeout(chk, 40); })(); }); 1360 const nextQs = followups(content); 1361 const shown = visibleAnswer(content); 1362 const dsp = ui.root.querySelector(".ds-panel"); 1363 const centry = { type: "chat", apps: Object.values(appEv).map(({ cards, ...x }) => ({ ...x, cards: (cards || []).slice(0, 8) }
1363)), appCards, next: nextQs, sources: done?.sources || [], dsImages: dsp ? (dsp._imgs || []) : [], dsSites: dsp ? new Set((dsp._sites || []).map((x) => x.host)).size : 0, dsSiteList: dsp ? (dsp._sites || []).slice(0, 14).map((x) => ({ url: x.url, host: x.host, title: x.title })) : [], md: content, reasoning: reasoning.slice(0, 6000), steps: act.steps.map((s) => ({ kind: s.kind, label: s.label, ms: s.ms || 0, detail: (s.detail || "").slice(-1200) })), meta: { cost: done?.cost ?? 1, refunded: !!done?.refunded, ms: done?.ms || Date.now() - started, tokens: done?.usage ? done.usage.prompt + done.usage.completion : 0 } }; 1364 if (!shown.trim()) centry.notice = { kind: "error", title: "٠ا ÙØµÙ رد", text: " " + (streamError || "ØªÙ Ø§ÙØ¥ÙÙØ§Ù أ٠٠ا رجع ش٠٠٠اÙÙ ÙØ¯ÙÙ."), 1365 actions: '<button type="button" class="btn-soft" data-retry-text="' + esc(text) + '">' + esc((window.HythmI18n ? HythmI18n.t : (x) => x)("Ø¬Ø±ÙØ¨ ٠رة ثاÙÙØ©")) + "</button>" }; 1366 ui.root.remove(); 1367 const fin = renderEntry(centry); fin.classList.add("settled"); pushLog(centry); updateLayout(); scrollDown(); 1368 if (shown.trim() && !images.length && !(done && done.sources && done.sources.length) && appSett
1368ings.dual !== "never" && (appSettings.dual === "always" || DUAL_RE.test(text)) && !NOT_DUAL_RE.test(text) && shown.length < 6000) offerAlt(text, centry, fin); 1369 return; 1370 } 1371 1372 const entry = { type: "ai", notes: notesOf(content), reasoning: reasoning.slice(0, 6000) }; 1373 const meta = { cost: done?.cost ?? cost, refunded: !!done?.refunded, ms: done?.ms || Date.now() - started }; 1374 if (done?.usage) meta.tokens = (done.usage.prompt || 0) + (done.usage.completion || 0); 1375 if (done?.continuations) meta.continued = done.continuations; 1376 1377 if (done?.refused || /<hythm-refuse/i.test(content)) { 1378 ui.root.remove(); 1379 const reason = done?.refuseReason || (content.match(/<hythm-refuse[^>]*>([\s\S]*?)(<\/hythm-refuse>|$)/i)?.[1] || "").trim(); 1380 const w = { type: "warning", title: "Hythm Ø±ÙØ¶ ÙØ°Ø§ Ø§ÙØ·Ùب", reason: reason || "Ø§ÙØ·Ùب ٠ا Ø¨ÙØªÙاÙ٠٠ع اÙ٠عاÙÙØ± Ø§ÙØ£Ø®ÙاÙÙØ©.", category: "model" }; 1381 renderEntry(w); pushLog(w); enterWarning(); workEnd(); 1382 if (lastCode && session.current >= 0) showCode(session.versions[session.current].code, { final: true }); 1383 scrollDown(); 1384 return; 1385 } 1386 1387 const ex = extractHtml(content); 1388 const patches = parsePatches(content); 1389 let newCode = null, kind = "full"; 1390 if (ex && ex.html.trim()) { 1391 newCode = compact(done?.finalCode || ex.html.trim()); 1392 if (done?.critiqueFixes) meta.critique = done.critiqueFixes; 1393 if (!ex.complete || done?.truncated) entry.notice = { kind: "error", title: "اÙÙÙØ¯ ÙØµÙ ÙØ§Ùص", text: " اÙÙ ÙØ¯ÙÙ ÙØµÙ ÙÙØØ¯ Ø§ÙØ£ÙØµÙ ÙØ¨Ù ٠ا ÙØ®Ùص. اÙ٠عاÙÙØ© بتÙÙ Ù٠اÙÙØ§Ùص تÙÙØ§Ø¦ÙØ§ÙØ ÙØ¥Ø°Ø§ Ø¨Ø¯Ù ÙØªÙجة Ø£ÙÙ Ù Ø¬Ø±ÙØ¨ اÙÙØ¶Ø¹ Ø§ÙØ£Ø¹ÙÙ." }; 1394 } else if (patches.length && baseVersion >= 0) { 1395 const r = applyPatches(baseCode, patches); 1396 meta.patches = { ok: r.ok, fail: r.fail }; 1397 if (r.ok) { newCode = compact(r.code); kind = "patch"; } 1398 else entry.notice = { kind: "error", title: "٠ا ÙØ¯Ø±Øª أطبÙÙ Ø§ÙØªØ¹Ø¯ÙÙ", text: " Ø¬Ø±ÙØ¨ ØªØ¹ÙØ¯ ØµÙØ§ØºØ© Ø§ÙØ·Ùب بشÙÙ Ø£ÙØ¶Ø." }; 1399 } 1400 1401 if (newCode) { 1402 newCode = withFonts(newCode, pendingType); pendingType = null; 1403 await requestImages(queriesIn(newCode)); 1404 session.versions.push({ code: newCode, t: Date.now(), mode, model }); 1405 const idx = session.versions.length - 1; 1406 entry.version = idx; entry.versionKind = kind; 1407 session.current = idx; 1408 editOff = false; 1409 const used = []; 1410 buildDoc(newCode, used); 1411 if (used.length) meta.photos = used.length; 1412 session.prior.push(text); session.prior = session.prior.slice(-6); 1413 } else if (!entry.notice) { 1414 if (streamError) entry.notice = { kind: "error", title: "صار خطأ", text: " " + streamError }; 1415 else if (!content.trim()) entry.notice = { kind: "error", title: "٠ا ÙØµÙ رد", text: " ØªÙ Ø§ÙØ¥ÙÙØ§Ù أ٠٠ا رجع ش٠٠٠اÙÙ ÙØ¯ÙÙ." }; 1416 } 1417 if (streamError && newCode) entry.notice = { kind: "error", title: "اÙÙØ·Ø¹ Ø§ÙØ§ØªØµØ§Ù", text: " " + streamError }; 1418 entry.meta = meta; 1419 1420 ui.root.remove(); 1421 renderEntry(entry); 1422 pushLog(entry); 1423 workEnd(); 1424 if (newCode) selectVersion(entry.version, { silent: true }); 1425 else if (session.current >= 0) showCode(session.versions[session.current].code, { final: true }); 1426 updateSendCost(); 1427 scrollDown(); 1428 } 1429 1430 /* ---------------- memory panel: see, add and delete what Hythm remembers ---------------- */ 1431 (function memoryPanel() { 1432 const T = (x) => (window.HythmI18n ? HythmI18n.t(x) : x); 1433 const panel = $("memPanel"); if (!panel) return; 1434 async function load(action, extra) { 1435 const r = await api("/api/memory", { uid, action: action || "get", ...(extra || {}) }); 1436 if (!r.ok) return; 1437 const d = r.data; 1438 $("memList").innerHTML = d.dreams.length ? d.dreams.map((x, i) => '<li><span>' + esc(x) + '</span><button type="button" data-i="' + i + '" title="' + esc(T("Ø§ÙØ³Ù ÙØ§Ø¯")) + '">Ã</button></li>').join("") 1439 : '<li class="mem-empty">' + esc(T("ÙØ³Ø§ ٠ا ÙÙ Ø°ÙØ±Ùات. اØÙ٠٠ع Hythm Ø£Ù Ø£Ø¶Ù Ø´Ù Ø¨Ø¥ÙØ¯Ù.")) + "</li>"; 1440 $("memMeta").textContent = [d.topics.length && T("Ù ÙØ§Ø¶ÙعÙ") + ": " + d.topics.join("Ø "), d.langs.length && T("ÙØºØ§ØªÙ") + ": " + d.langs.join("Ø ")].filter(
1440Boolean).join(" · "); 1441 $("memList").querySelectorAll("[data-i]").forEach((btn) => btn.addEventListener("click", () => load("forget", { index: +btn.dataset.i }))); 1442 } 1443 $("memView").addEventListener("click", () => { panel.hidden = !panel.hidden; if (!panel.hidden) load(); }); 1444 $("memAdd").addEventListener("submit", (e) => { e.preventDefault(); const t = $("memInput").value.trim(); if (!t) return; $("memInput").value = ""; load("add", { text: t }); }); 1445 $("memClear").addEventListener("click", () => { if (confirm(T("Ù ØªØ£ÙØ¯Ø Hythm Ø±Ø ÙÙØ³Ù Ù٠ش٠تعÙÙ٠٠عÙÙ."))) load("clear").then(() => toast(T("اÙÙ Ø³ØØª Ø§ÙØ°Ø§Ùرة â"))); }); 1446 })(); 1447 1448 /* ---------------- share · install as an app · shortcuts ---------------- */ 1449 (function appExtras() { 1450 const T = (x) => (window.HythmI18n ? HythmI18n.t(x) : x); 1451 // share the current conversation as a read-only link 1452 const share = $("shareBtn"); 1453 const syncShare = () => { share.hidden = !(session && session.log && session.log.some((x) => x.type === "user")); }; 1454 setInterval(syncShare, 1500); 1455 share.addEventListener("click", async () => { 1456 const items = session.log.map((x) => x.type === "user" ? { role: "user", text: x.text } 1457 : x.type === "chat" ? { role: "ai", text: x.md || "" } 1458 : x.type === "image" ? { role: "ai", text: x.prompt ? "ð¼ï¸ " + x.prompt : "", image: x.url } 1459 : x.type === "ai" ? { role: "ai", text: x.notes || "" } : null).filter(Boolean); 1460 const r = await api("/api/share", { uid, title: ($("chatTitle") && $("chatTitle").textContent) || "Hythm", items }); 1461 if (!r.ok) return toast(T("٠ا ÙØ¯Ø±Øª أع٠٠رابط اÙÙ Ø´Ø§Ø±ÙØ©")); 1462 const link = location.origin + r.data.url; 1463 try { if (navigator.share && matchMedia("(pointer: coarse)").matches) await navigator.share({ title: "Hythm", url: link }); else { await navigator.clipboard.writeText(link); toast(T("Ø§ÙØ³Ø® رابط اÙÙ Ø´Ø§Ø±ÙØ© â")); } } 1464 catch { prompt(T("رابط اÙÙ Ø´Ø§Ø±ÙØ©:"), link); } 1465 }); 1466 // installable app (Android / desktop Chrome; iOS gets a hint) 1467 if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js").catch(() => {}); 1468 let deferred = null; const inst = $("installBtn"); 1469 const standalone = matchMedia("(display-mode: standalone)").matches || navigator.standalone; 1470 window.addEventListener("beforeinstallprompt", (e) => { e.preventDefault(); deferred = e; if (!standalone) inst.hidden = false; }); 1471 const ios = /iphone|ipad|ipod/i.test(navigator.userAgent) && !standalone; 1472 if (ios && !storage.get("hythm.iosHint")) inst.hidden = false; 1473 inst.addEventListener("click", async () => { 1474 if (deferred) { deferred.prompt(); const c = await deferred.userChoice.catch(() => null); deferred = null; if (c && c.outcome === "accepted") inst.hidden = true; return; } 1475 if (ios) { toast(T("عÙÙ Ø§ÙØ¢ÙÙÙÙ: Ø§ÙØ¨Ø³ زر اÙÙ Ø´Ø§Ø±ÙØ© â¬ï¸ بعدÙÙ Â«Ø¥Ø¶Ø§ÙØ© Ø¥ÙÙ Ø§ÙØ´Ø§Ø´Ø© Ø§ÙØ±Ø¦ÙØ³ÙØ©Â»")); storage.set("hythm.iosHint", "1"); inst.hidden = true; } 1476 }); 1477 window.addEventListener("appinstalled", () => { inst.hidden = true; toast(T("Ø§ÙØ¶Ø§Ù Hythm ÙØªØ·Ø¨ÙÙØ§ØªÙ â")); }); 1478 // app shortcuts: /?new=1 and /?mode=image 1479 const q = new URLSearchParams(location.search); 1480 setTimeout(() => { 1481 if (q.get("new") === "1") $("newChat").click(); 1482 if (q.get("mode") === "image" && task !== "image") toggleImageMode(true); 1483 // from a shared picture: "make one like it" â image mode with the idea ready to send 1484 if (q.get("try")) { if (task !== "image") toggleImageMode(true); els.input.value = q.get("try").slice(0, 300); els.input.dispatchEvent(new Event("input")); els.input.focus(); } 1485 // shared INTO Hythm from another app (PWA share target), or the "voice call" shortcut 1486 if (q.get("q") || q.get("link")) { els.input.value = [q.get("title"), q.get("q"), q.get("link")].filter(Boolean).join("\n").slice(0, 4000); els.input.dispatchEvent(new Event("input")); els.input.focus(); } 1487 if (q.get("voice") === "1") { const v = document.getElementById("voiceBtn"); if (v) v.click(); } 1488 if (q.has("new") || q.has("mode") || q.has("source") || q.has("try") || q.has("ref") || q.has("q") || q.has("link") || q.has("title") || q.has("voice")) history.replaceState(null, "", location.pathname); 1489 }, 800); 1490 })(); 1491 1492 /* ---------------- "/" quick commands ---------------- */ 1493 (function slashMenu() { 1494 const T = (x) => (window.HythmI18n ? HythmI18n.t(x) : x); 1495 const CMDS = [ 1496 { k: "ØµÙØ±Ø©", en: "image", ico: "ð¼ï¸", d: "Ø§Ø±Ø³Ù Ø£Ù Ø´Ù Ø¨ÙØµÙ", run: () => { if (task !== "image") toggleImageMode(true); fill("ارس٠"); } }, 1497 { k: "Ø§Ø¹ÙØ§Ù", en: "ad", ico: "ðï¸", d: "Ø¥Ø¹ÙØ§Ù ÙÙ ÙØªØ¬Ù: أرÙÙ ØµÙØ±ØªÙ ÙØ§Ùتب Ø§ÙØ³Ø¹Ø±", run: () => { fill('ØØ· اÙÙ ÙØªØ¬ عÙÙ Ø®ÙÙÙØ© جذابة ÙÙ ÙØªÙب "â¦" بسعر '); $("attachBtn").click(); } }, 1498 { k: "pdf", en: "pdf", ico: "ð", d: "اع٠ÙÙ Ù ÙÙ PDF Ø¹Ù Ù ÙØ¶Ùع", run: () => fill("اع٠ÙÙ Ù ÙÙ PDF Ù Ø±ØªÙØ¨ ع٠") }, 1499 { k: "excel", en: "excel", ico: "ð", d: "جدÙÙ Excel / CSV", run: () => fill("اع٠Ù٠جدÙÙ Excel (CSV) ÙÙÙ ") }, 1500 { k: "Ø¨ØØ«", en: "search", ico: "ð", d: "Ø§Ø¨ØØ« ب٠عÙÙ٠ات ØØ¯Ùثة", run: () => fill("Ø§Ø¨ØØ« ع٠") }, 1501 { k: "Ù ÙÙØ¹", en: "site", ico: "ð", d: "ص٠٠ÙÙ Ù ÙÙØ¹ Ø£Ù ØµÙØØ©", run: () => fill("ص٠٠ÙÙ Ù ÙÙØ¹ ") },
1502 { k: "ترج٠", en: "translate", ico: "ð", d: "ترج٠ة ÙØ£Ù ÙØºØ©", run: () => fill("ترج٠ÙÙØ¥ÙجÙÙØ²Ù: ") }, 1503 { k: "Ø¬Ø¯ÙØ¯", en: "new", ico: "âï¸", d: "Ù ØØ§Ø¯Ø«Ø© Ø¬Ø¯ÙØ¯Ø©", run: () => { $("newChat").click(); } }, 1504 { k: "usage", en: "usage", ico: "â¡", d: "ÙÙØØ© Ø§ÙØ§Ø³ØªÙÙØ§Ù (ÙÙØ¥Ø¯Ø§Ø±Ø©)", run: () => { els.input.value = ""; autoGrow(); window.HythmUsage && HythmUsage.open(); } } 1505 ]; 1506 function fill(t) { els.input.value = t; autoGrow(); els.input.focus(); els.input.setSelectionRange(t.length, t.length); els.input.dispatchEvent(new Event("input")); } 1507 const box = document.createElement("div"); box.className = "slash-menu no-i18n"; box.hidden = true; box.setAttribute("role", "listbox"); 1508 els.composer.prepend(box); 1509 let items = [], sel = 0; 1510 function render(q) { 1511 q = q.toLowerCase(); 1512 items = CMDS.filter((c) => !q || c.k.toLowerCase().startsWith(q) || c.en.startsWith(q)); 1513 if (!items.length) { box.hidden = true; return; } 1514 sel = Math.min(sel, items.length - 1); 1515 box.innerHTML = items.map((c, i) => '<button type="button" role="option" class="' + (i === sel ? "on" : "") + '" data-i="' + i + '"><span>' + c.ico + "</span><b>/" + esc(c.k) + "</b><small>" + esc(T(c.d)) + "</small></button>").join(""); 1516 box.hidden = false; 1517 } 1518 function pick(i) { const c = items[i]; box.hidden = true; if (c) { els.input.value = ""; c.run(); } } 1519 els.input.addEventListener("input", () => { 1520 const m = /^\/([^\s]*)$/.exec(els.input.value); 1521 if (m) render(m[1]); else box.hidden = true; 1522 }); 1523 els.input.addEventListener("keydown", (e) => { 1524 if (box.hidden) return; 1525 if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); sel = (sel + (e.key === "ArrowDown" ? 1 : items.length - 1)) % items.length; render(els.input.value.slice(1)); } 1526 else if (e.key === "Enter" || e.key === "Tab") { if (/^\/(usage|usge|stats)$/i.test(els.input.value.trim())) return; e.preventDefault(); e.stopImmediatePropagation(); pick(sel); } 1527 else if (e.key === "Escape") { box.hidden = true; } 1528 }, true); 1529 box.addEventListener("mousedown", (e) => { const b = e.target.closest("[data-i]"); if (b) { e.preventDefault(); pick(+b.dataset.i); } }); 1530 })(); 1531 1532 /* ---------------- Composer events ---------------- */ 1533 function autoGrow() { els.input.style.height = "auto"; els.input.style.height = Math.min(els.input.scrollHeight, 180) + "px"; } 1534 els.composer.addEventListener("submit", (e) => { 1535 e.preventDefault(); 1536 if (controller) { controller.abort(); return; } 1537 let text = els.input.value.trim(); 1538 if (!text && attachments.length) { // only an attachment â a sensible default request 1539 const Tr = window.HythmI18n ? HythmI18n.t : (x) => x; 1540 text = attachments.some((a) => a.link && a.kind === "youtube") ? Tr("ÙØ®ÙصÙÙ ÙØ§ÙÙÙØ¯Ù٠٠ع Ø£Ù٠اÙÙÙØ§Ø· ÙØ§ÙØ£ÙÙØ§Øª") 1541 : attachments.some((a) => a.link) ? Tr("Ø´Ù ÙÙ Ø¨ÙØ§ÙØ±Ø§Ø¨Ø·Ø ÙØ®ÙصÙÙ ÙØ§Ù") 1542 : attachments.some((a) => a.pasted) ? Tr("راجع ÙØ§ÙÙÙØ¯: Ø§Ø´Ø±Ø Ø´Ù Ø¨ÙØ¹Ù Ù ÙØµÙÙØ Ø£Ù Ù Ø´ÙÙØ© ÙÙÙ") 1543 : attachments.some((a) => a.file) ? Tr("ÙØ®ÙصÙÙ ÙØ§ÙÙ ÙÙ") : Tr("Ø´Ù ÙÙ Ø¨ÙØ§ÙØµÙØ±Ø©Ø"); 1544 } 1545 if (!text) return els.input.focus(); 1546 els.input.value = ""; autoGrow(); 1547 if (!isEditing() && isDesignText(text) && needsClarify(text)) return showClarify(text); 1548 send(text); 1549 }); 1550 els.input.addEventListener("input", autoGrow); 1551 const syncSend = () => { 1552 const has = !!els.input.value.trim() || attachments.length > 0; 1553 els.composer.classList.toggle("has-text", has); 1554 // empty box â the send button IS the Hythm Live button (one button, like the big assistants) 1555 const live = !has && !els.sendBtn.classList.contains("stop"); 1556 els.sendBtn.classList.toggle("is-live", live); 1557 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 1558 if (!els.sendBtn.classList.contains("stop")) els.sendBtn.setAttribute("aria-label", T(live ? "Hythm Live â Ø§ØÙ٠٠عÙ" : "إرØ
1558³Ø§Ù")); 1559 els.sendBtn.title = live ? T("Hythm Live â Ø§ØÙ٠٠عÙ") : ""; 1560 }; 1561 els.sendBtn.addEventListener("click", (e) => { 1562 if (!els.sendBtn.classList.contains("is-live") || els.sendBtn.classList.contains("stop")) return; 1563 e.preventDefault(); e.stopImmediatePropagation(); 1564 const v = document.getElementById("voiceBtn"); if (v) v.click(); 1565 }, true); 1566 els.input.addEventListener("input", syncSend); 1567 // image mode: once you pause typing, the server writes the picture's prompt ahead of time, so "send" starts drawing at once 1568 let prepTimer = 0, prepLast = ""; 1569 els.input.addEventListener("input", () => { 1570 clearTimeout(prepTimer); 1571 if (task !== "image" || !uid) return; 1572 prepTimer = setTimeout(() => { 1573 const text = els.input.value.trim(); 1574 if (text.length < 6 || text === prepLast) return; 1575 prepLast = text; 1576 fetch("/api/image-prep", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uid, prompt: text, style: imgChoice.style, quality: imgChoice.quality }) }).catch(() => {}); 1577 }, 900); 1578 }); 1579 new MutationObserver(syncSend).observe(document.getElementById("attachRow"), { childList: true }); 1580 setInterval(syncSend, 700); 1581 let intentTimer = 0, intentSeq = 0; 1582 els.input.addEventListener("input", () => { 1583 clearTimeout(intentTimer); 1584 if (task !== "auto") return; 1585 const t = els.input.value.trim(); 1586 intentTimer = setTimeout(async () => { 1587 const my = ++intentSeq; 1588 const it = t.length > 2 ? ((await api("/api/intent", { prompt: t })).data || {}) : {}; 1589 const k = it.by === "rules" || (it.by === "learned" && it.confidence >= 0.85) ? it.task : ""; 1590 if (my !== intentSeq || task !== "auto") return; 1591 const io = document.getElementById("imgOpts"); if (io) io.hidden = k !== "image"; 1592 if (els.composer) els.composer.classList.toggle("hint-image", k === "image"); 1593 if (els.composer) els.composer.classList.toggle("hint-design", k === "design"); 1594 if (k === "image") pollEngine.force = true, pollEngineAuto(); 1595 }, 450); 1596 }); 1597 els.input.addEventListener("keydown", (e) => { 1598 if (e.key !== "Enter" || e.shiftKey || e.isComposing) return;
1599 const ctrlToSend = (typeof appSettings !== "undefined" && appSettings.sendKey === "ctrl"); 1600 if (ctrlToSend && !(e.ctrlKey || e.metaKey)) return; // plain Enter = new line 1601 e.preventDefault(); els.composer.requestSubmit(); 1602 }); 1603 document.querySelectorAll(".suggestion").forEach((b) => b.addEventListener("click", () => { els.input.value = b.textContent.trim(); autoGrow(); els.input.focus(); })); 1604 // Quick edits: fill the prompt 1605 document.querySelectorAll("#quickEdits [data-q]").forEach((b) => b.addEventListener("click", () => { els.input.value = b.dataset.q; autoGrow(); els.input.focus(); })); 1606 1607 // Instant theme switch â a new version, zero credits 1608 $("themeSelect").addEventListener("change", (e) => { 1609 const v = session.versions[session.current]; 1610 if (!v) return; 1611 const t = e.target.value; 1612 let code = /data-theme=["'][\w-]+["']/.test(v.code) ? v.code.replace(/data-theme=["'][\w-]+["']/, `data-theme="${t}"`) : v.code.replace(/<html/i, `<html data-theme="${t}"`); 1613 code = withFonts(code, null, true); 1614 session.versions.push({ code, t: Date.now(), mode: v.mode }); 1615 const idx = session.versions.length - 1; 1616 const entry = { type: "ai", notes: (window.HythmI18n ? HythmI18n.t : (x) => x)(`بدÙÙØª Ø§ÙØ«Ù٠إÙÙ **${e.target.selectedOptions[0].textContent}** â Ù Ø¬Ø§ÙØ§Ù.`), version: idx, versionKind: "patch", meta: { cost: 0 } }; 1617 renderEntry(entry); pushLog(entry); selectVersion(idx, { silent: true }); scrollDown(); 1618 }); 1619 1620 // Free prompt enhancer 1621 $("enhanceBtn").addEventListener("click", async () => { 1622 const text = els.input.value.trim(); 1623 if (!text) return toast("Ø§ÙØªØ¨ ÙÙØ±Ø© ÙØµÙرة Ø£ÙÙ"); 1624 const b = $("enhanceBtn"); b.classList.add("busy"); b.disabled = true; 1625 const { ok, data } = await api("/api/enhance", { uid, prompt: text }); 1626 b.classList.remove("busy"); b.disabled = false; 1627 if (!ok) return toast(data.reason || "٠ا ÙØ¯Ø±Øª Ø£ØØ³Ù٠اÙÙØµÙ ÙÙÙ"); 1628 els.input.value = data.prompt; autoGrow(); els.input.focus(); toast("ØªÙ ØªØØ³Ù٠اÙÙØµÙ â¨"); 1629 }); 1630 1631 $("clearEdit").addEventListener("click", () => { editOff = true; updateSendCost(); els.input.focus(); toast("Ø§ÙØ·Ùب Ø§ÙØ¬Ø§Ù Ø±Ø ÙÙÙ٠تص٠ÙÙ Ø¬Ø¯ÙØ¯"); }); 1632 1633 function loadProject(id) { 1634 if (controller) controller.abort(); 1635 projectId = id; 1636 session = (id && storage.json("hythm.p." + id)) || emptySession(); 1637 if (id) storage.set("hythm.currentProject", id); else storage.del("hythm.currentProject"); 1638 els.messages.querySelectorAll(".msg").forEach((m) => m.remove()); 1639 lastCode = ""; els.code.textContent = ""; els.frame.srcdoc = ""; els.placeholder.hidden = false; 1640 els.photoCount.hidden = true; 1641 document.body.classList.remove("is-warning"); 1642 editOff = false; 1643 session.log = session.log.filter((e) => { try { renderEntry(e); return true; } catch (err) { Shield.report("restore", err); return false; } }); 1644 if (session.current >= 0 && session.versions[session.current]) selectVersion(session.current, { silent: true }); 1645 setTab("preview"); setView("chat"); updateSendCost(); renderProjects(); updateLayout(); closeSidebar(); 1646 scrollDown(); 1647 } 1648 $("newChat").addEventListener("click", () => { 1649 loadProject(null); 1650 els.input.focus(); 1651 }); 1652 1653 /* ---------------- Layout v3: sidebar · projects · search · picker ---------------- */ 1654 function updateLayout() { 1655 els.app.classList.toggle("is-empty", !session.log.length); 1656 els.app.classList.toggle("no-design", !session.versions.length); 1657 const p = projects.find((x) => x.id === projectId); 1658 $("chatTitle").textContent = p && session.log.length ? p.title : (window.HythmI18n ? HythmI18n.t : (x) => x)("Ù Ø´Ø±ÙØ¹ Ø¬Ø¯ÙØ¯"); 1659 } 1660 function setGreeting() { 1661 const n = (me?.name || "").trim(); 1662 const h = new Date().getHours(); 1663 const part = h < 12 ? "ØµØ¨Ø§Ø Ø§ÙØ¥Ø¨Ø¯Ø§Ø¹" : h < 18 ? "Ø¯ÙØ±Ù Ø§ÙØ¢Ù" : "٠ساء Ø§ÙØ¥Ø¨Ø¯Ø§Ø¹"; 1664 $("greeting").textContent = n ? `${part} ÙØ§ ${n}` : part; 1665 $("userName").textContent = n || "ضÙÙ"; 1666 } 1667 const timeAgo = (t) => { 1668 const m = Math.round((Date.now() - t) / 60000); 1669 if (m < 1) return "Ø§ÙØ¢Ù"; if (m < 60) return `ÙØ¨Ù ${m} د`; 1670 const h = Math.round(m / 60); if (h < 24) return `ÙØ¨Ù ${h} س`; 1671 return `ÙØ¨Ù ${Math.round(h / 24)} ÙÙÙ `; 1672 }; 1673 const markText = (text, q) => { 1674 const e = esc(text); 1675 if (!q) return e; 1676 const i = text.toLowerCase().indexOf(q.toLowerCase());
1677 return i < 0 ? e : esc(text.slice(0, i)) + "<mark>" + esc(text.slice(i, i + q.length)) + "</mark>" + esc(text.slice(i + q.length)); 1678 }; 1679 function renderProjects() { 1680 const q = $("projectSearch").value.trim(); 1681 const ql = q.toLowerCase(); 1682 const list = projects.map((p) => { 1683 if (!ql) return { p, hit: null }; 1684 if (p.title.toLowerCase().includes(ql)) return { p, hit: null }; 1685 // search inside the project's requests too 1686 const sess = storage.json("hythm.p." + p.id); 1687 const hit = sess?.log?.find((e) => e.type === "user" && e.text.toLowerCase().includes(ql)); 1688 return hit ? { p, hit: hit.text } : null; 1689 }).filter(Boolean); 1690 list.sort((x, y) => (y.p.pinned ? 1 : 0) - (x.p.pinned ? 1 : 0)); // pinned chats stay on top 1691 $("projects").innerHTML = list.map(({ p, hit }) => ` 1692 <li class="project${p.id === projectId ? " active" : ""}${p.pinned ? " pinned" : ""}" data-id="${p.id}"> 1693 <button type="button" class="project-open"><b>${p.pinned ? '<i class="pin-ico" aria-hidden="true">ð</i>' : ""}${markText(p.title, q)}</b><small>${hit ? markText(hit.slice(0, 60), q) : `${timeAgo(p.t)}${p.versions ? " · " + p.versions + " ÙØ³Ø®Ø©" : ""}`}</small></button> 1694 <button type="button" class="project-ren" aria-label="غÙÙØ± Ø§ÙØ§Ø³Ù " title="غÙÙØ± Ø§ÙØ§Ø³Ù "><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></button> 1695 <button type="button" class="project-del" aria-label="ØØ°Ù اÙÙ Ø´Ø±ÙØ¹" title="ØØ°Ù"><svg><use href="#i-trash"/></svg></button> 1696 <button type="button" class="project-more" aria-label="Ø®ÙØ§Ø±Ø§Øª" title="Ø®ÙØ§Ø±Ø§Øª" aria-haspopup="menu"><svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="19" cy="12" r="1.8"/></svg></button> 1697 </li>`).join(""); 1698 $("projectsEmpty").hidden = !!list.length; 1699 $("projectsEmpty").textContent = q ? "٠ا ÙÙ ÙØªØ§Ø¦Ø¬ Ù٠«" + q + "»" : "٠ا ÙÙ Ù ØØ§Ø¯Ø«Ø§Øª ÙØ³Ø§."; 1700 $("projects").querySelectorAll(".project").forEach((li) => { 1701 li.querySelector(".project-open").addEventListener("click", () => loadProject(li.dataset.id)); 1702 li.querySelector(".project-more").addEventListener("click", (ev) => { ev.stopPropagation(); chatMenu(li, ev.currentTarget); }); 1703 li.querySelector(".project-ren").addEventListener("click", (ev) => { 1704 ev.stopPropagation(); 1705 const b = li.querySelector(".project-open b"), p = projects.find((x) => x.id === li.dataset.id); if (!p) return; 1706 const inp = document.createElement("input"); inp.className = "project-rename"; inp.value = p.title; inp.maxLength = 80; 1707 b.replaceWith(inp); inp.focus(); inp.select(); 1708 const done = (ok) => { 1709 const v = inp.value.trim(); 1710 if (ok && v) { p.title = v; storage.set("hythm.projects", JSON.stringify(projects)); if (p.id === projectId && $("chatTitle")) $("chatTitle").textContent = v; } 1711 renderProjects(); 1712 }; 1713 inp.addEventListener("keydown", (e) => { if (e.key === "Enter") done(true); if (e.key === "Escape") done(false); e.stopPropagation(); }); 1714 inp.addEventListener("blur", () => done(true)); 1715 inp.addEventListener("click", (e) => e.stopPropagation()); 1716 }); 1717 li.querySelector(".project-del").addEventListener("click", () => { 1718 if (!confirm(window.HythmI18n ? HythmI18n.t("ØØ°Ù ÙØ§ÙÙ Ø´Ø±ÙØ¹ ÙÙÙ ÙØ³Ø®ÙØ") : "ØØ°Ù ÙØ§ÙÙ Ø´Ø±ÙØ¹ ÙÙÙ ÙØ³Ø®ÙØ")) return; 1719 storage.del("hythm.p." + li.dataset.id); 1720 projects = projects.filter((x) => x.id !== li.dataset.id); 1721 storage.set("hythm.projects", JSON.stringify(projects)); 1722 if (li.dataset.id === projectId) loadProject(null); else renderProjects(); 1723 }); 1724 }); 1725 } 1726 // ⯠on a conversation: rename · download as a file · delete 1727 function T(x) { return window.HythmI18n ? HythmI18n.t(x) : x; } 1728 function chatAsMd(id) { 1729 const p = projects.find((x) => x.id === id) || { title: "Hythm" }; 1730 const sess = id === projectId ? session : storage.json("hythm.p." + id); 1731 const out = ["# " + p.title, "", "_" + new Date(p.t || Date.now()).toLocaleStr
1731ing() + " · Hythm_", ""]; 1732 for (const e of sess?.log || []) { 1733 if (e.type === "user") out.push("## ð§ " + T("Ø£ÙØª"), "", e.text || "", ...(e.fileNames?.length ? ["", "ð " + e.fileNames.join(", ")] : []), ""); 1734 else if (e.type === "chat" || e.type === "ai") { const md = visibleAnswer(e.md || e.notes || ""); if (md.trim()) out.push("## ⦠Hythm", "", md, ""); } 1735 else if (e.type === "image" && e.url && !/^blob:/.test(e.url)) out.push("## ⦠Hythm", "", "![" + (e.prompt || "image").replace(/[\[\]]/g, "") + "](" + (/^https?:/.test(e.url) ? e.url : location.origin + e.url) + ")", ""); 1736 } 1737 return { title: p.title, md: out.join("\n").replace(/\n{3,}/g, "\n\n") }; 1738 } 1739 async function downloadChat(id, kind) { 1740 const { title, md } = chatAsMd(id); 1741 const base = (title || "Hythm").replace(/[\\/:*?"<>|]+/g, " ").trim().slice(0, 60) || "Hythm"; 1742 const HF = window.HythmFiles; 1743 if ((kind === "pdf" || kind === "docx") && HF) return HF.download({ name: base + "." + kind, theme: "midnight", content: md }); 1744 const body = kind === "txt" ? md.replace(/^#+\s*/gm, "").replace(/\*\*|__|\x60{3}\w*|_([^_]+)_/g, (m, g) => g || "").replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "[$1] $2") 1745 : kind === "json" ? JSON.stringify({ title, exported: new Date().toISOString(), messages: (id === projectId ? session : storage.json("hythm.p." + id))?.log?.filter((e) => ["user", "chat", "ai", "image"].includes(e.type)) 1746 .map((e) => ({ role: e.type === "user" ? "user" : "assistant", content: e.type === "user" ? e.text : e.type === "image" ? e.url : visibleAnswer(e.md || e.notes || "") })) || [] }, null, 2) : md; 1747 const blob = new Blob([body], { type: (kind === "json" ? "application/json" : kind === "txt" ? "text/plain" : "text/markdown") + ";charset=utf-8" }); 1748 const u = URL.createObjectURL(blob), l = document.createElement("a"); l.href = u; l.download = base + "." + kind; document.body.appendChild(l); l.click(); l.remove(); setTimeout(() => URL.revokeObjectURL(u), 4000); 1749 toast(T("ØªÙ ØªÙØ²Ù٠اÙÙ ØØ§Ø¯Ø«Ø© â")); 1750 } 1751 function chatMenu(li, btn) { 1752 document.querySelector(".chat-pop")?.remove(); 1753 const m = document.createElement("div"); m.className = "chat-pop"; m.setAttribute("role", "menu"); 1754 const row = (ico, label, fn, cls) => { const b = document.createElement("button"); b.type = "button"; b.className = "cp-row" + (cls ? " " + cls : ""); b.innerHTML = '<span class="cp-ico">' + ico + "</span><span>" + esc(T(label)) + "</span>"; b.addEventListener("click", (e) => { e.stopPropagation(); fn(b); }); m.appendChild(b); return b; }; 1755 const pp = projects.find((x) => x.id === li.dataset.id); 1756 row("ð", pp && pp.pinned ? "Ø¥ÙØºØ§Ø¡ Ø§ÙØªØ«Ø¨Ùت" : "ØªØ«Ø¨ÙØª Ø¨Ø§ÙØ£Ø¹ÙÙ", () => { m.remove(); if (!pp) return; pp.pinned = !pp.pinned; storage.set("hythm.projects", JSON.stringify(projects)); renderProjects(); toast(T(pp.pinned ? "Ø§ÙØ«Ø¨ØªØª اÙÙ ØØ§Ø¯Ø«Ø© Ø¨Ø§ÙØ£Ø¹ÙÙ ð" : "اÙÙØºÙ Ø§ÙØªØ«Ø¨Ùت")); }); 1757 row("âï¸", "غÙÙØ± Ø§ÙØ§Ø³Ù ", () => { m.remove(); li.querySelector(".project-ren").click(); }); 1758 const dl = row("â¬ï¸", "ØªÙØ²Ù٠اÙÙ ØØ§Ø¯Ø«Ø©", () => sub.hidden = !sub.hidden, "cp-has"); 1759 const sub = document.createElement("div"); sub.className = "cp-sub"; sub.hidden = true; m.appendChild(sub); 1760 for (const [k, ico, label] of [["pdf", "ð", "PDF"], ["docx", "ð", "Word (.docx)"], ["md", "ð", "Markdown (.md)"], ["txt", "ð", T("ÙØµ (.txt)")], ["json", "ð§©", "JSON"]]) { 1761 const b = document.createElement("button"); b.type = "button"; b.className = "cp-row"; b.innerHTML = '<span class="cp-ico">' + ico + "</span><span>" + esc(label) + "</span>"; 1762 b.addEventListener("click", (e) => { e.stopPropagation(); m.remove(); downloadChat(li.dataset.id, k); }); sub.appendChild(b); 1763 } 1764 const sep = document.createElement("hr"); m.appendChild(sep); 1765 row("ðï¸", "ØØ°Ù", () => { m.remove(); li.querySelector(".project-del").click(); }, "danger"); 1766 document.body.appendChild(m); 1767 const r = btn.getBoundingClientRect(), rtl = document.documentElement.dir === "rtl"; 1768 m.style.top = Math.min(r.bottom + 4, innerHeight - m.offsetHeight - 260) + "px"; 1769 m.style.left = Math.max(8, Math.min(innerWidth - m.offsetWidth - 8, rtl ? r.left : r.right - m.offsetWidth)) + "px"; 1770 btn.setAttribute("aria-expanded", "true"); 1771 const close = (e) => { if (e && m.contains(e.target)) return; m.remove(); btn.removeAttribute("aria-expanded"); document.removeEventListener("pointerdown", close, true); document.removeEventListener("keydown", esck); }; 1772 const esck = (e) => { if (e.key === "Escape") close(); }; 1773 setTimeout(() => { document.addEventListener("pointerdown", close, true); document.addEventListener("keydown", esck); }); 1774 } 1775 $("projectSearch").addEventListener("input", renderProjects); 1776 $("projectSearch").addEventListener("keydown", (e) => { 1777 if (e.key === "Enter") { const first = $("projects").querySelector(".project-open"); if (first) first.click(); } 1778 if (e.key === "Escape") { e.target.value = ""; renderProjects(); e.target.blur(); } 1779 }); 1780 // Keyboard shortcuts. Keys are read by their PHYSICAL position (e.code), so they work with an Arabic keyboard layout too 1781 // (Ctrl+K on an Arabic layout sends "Ù" as e.key). Shortcuts the browser keeps for itself (Ctrl+Shift+I/C = developer tools, 1782 // Ctrl+N/T/W) can't be taken by a web page, so those actions use Alt instead. 1783 const keyOf = (e) => { const c = e.code || ""; return c.startsWith("Key") ? c.slice(3).toLowerCase() : c.startsWith("Digit") ? c.slice(5) : ({ Slash: "/", Comma: ",", Period: ".", Backslash: "\\", Enter: "enter", Escape: "escape", Space: " " }[c] || String(e.key || "").toLowerCase()); }; 1784 document.addEventListener("keydown", (e) => { 1785 const mod = e.ctrlKey || e.metaKey, k = keyOf(e), alt = e.altKey && !mod;
1786 const typing = /^(input|textarea|select)$/i.test(document.activeElement?.tagName || "") || document.activeElement?.isContentEditable; 1787 const go = (fn) => { e.preventDefault(); e.stopPropagation(); window.HythmTips && HythmTips.hide(); fn(); }; 1788 if (mod && !e.shiftKey && k === "k") go(() => { openSidebar(); $("projectSearch").focus(); $("projectSearch").select(); }); 1789 else if (mod && e.shiftKey && k === "o") go(() => $("newChat").click()); 1790 else if (mod && !e.shiftKey && k === "/") go(() => $("shortcutsDialog")?.showModal()); 1791 else if (mod && !e.shiftKey && k === "u") go(() => $("attachBtn")?.click()); 1792 else if (mod && !e.shiftKey && k === "b") go(() => (els.app.classList.contains("side-collapsed") || (isMobile() && !els.app.classList.contains("side-open")) ? openSidebar() : isMobile() ? closeSidebar() : $("sideToggle").click())); 1793 else if (mod && e.shiftKey && k === "e") go(() => $("omniBtn").click()); 1794 else if (mod && e.shiftKey && k === "l") go(() => document.querySelector('[data-set="lang"] button:not(.on)')?.click()); 1795 else if (mod && !e.shiftKey && k === ",") go(() => $("settingsBtn").click()); 1796 else if (alt && k === "i") go(() => toggleImageMode()); 1797 else if (alt && k === "c") go(() => openAccount()); 1798 else if (alt && k === "a") go(() => (window.HythmApps ? HythmApps.open() : null)); 1799 else if (alt && k === "w") go(() => ($("quickWeb") || $("webBtn"))?.click()); 1800 else if (alt && k === "t") go(() => $("thinkBtn")?.click()); 1801 else if (alt && k === "m") go(() => $("micBtn")?.click()); 1802 else if (alt && k === "v") go(() => $("voiceBtn")?.click()); 1803 else if (alt && k === "p") go(() => window.HythmPlans && HythmPlans.setTemp && document.getElementById("tempBtn")?.click()); 1804 else if (k === "/" && !typing && !mod && !e.altKey) go(() => els.input.focus()); 1805 else if (e.key === "Escape") { if (controller) { controller.abort(); return; } if (document.querySelector(".menu.pop-open")) { document.querySelectorAll(".menu.pop-open").forEach((m) => { m.hidden = true; m.classList.remove("pop-open"); }); return; } 1806 if (task === "image" && !document.querySelector("dialog[open]")) { toggleImageMode(false); if (els.app.classList.contains("images-view")) setView("chat"); return; } closePicker(); closeSidebar(); } 1807 }, true); 1808 1809 // sidebar collapse (desktop) / drawer (mobile) 1810 const isMobile = () => matchMedia("(max-width: 960px)").matches; 1811 function openSidebar() { if (isMobile()) els.app.classList.add("side-open"); else { els.app.classList.remove("side-collapsed"); storage.set("hythm.side", "open"); } } 1812 function closeSidebar() { els.app.classList.remove("side-open"); } 1813 $("sideToggle").addEventListener("click", () => { if (isMobile()) closeSidebar(); else { els.app.classList.add("side-collapsed"); storage.set("hythm.side", "closed"); } }); 1814 $("menuBtn").addEventListener("click", openSidebar); 1815 $("scrim").addEventListener("click", closeSidebar); 1816 if (storage.get("hythm.side") === "closed") els.app.classList.add("side-collapsed"); 1817 $("galleryBtn").addEventListener("click", () => $("photosBtn").click()); 1818 1819 // model / mode picker 1820 function closePicker() { $("pickerMenu").hidden = true; $("pickerBtn").setAttribute("aria-expanded", "false"); } 1821 $("pickerBtn").addEventListener("click", (e) => { 1822 e.stopPropagation(); 1823 const open = $("pickerMenu").hidden; 1824 $("pickerMenu").hidden = !open; $("pickerBtn").setAttribute("aria-expanded", String(open)); 1825 }); 1826 $("pickerMenu").addEventListener("click", (e) => { if (e.target.closest(".menu-item")) setTimeout(closePicker, 120); e.stopPropagation(); }); 1827 document.addEventListener("click", closePicker); 1828 1829 // + menu â styles row 1830 $("plusBtn").addEventListener("click", () => { 1831 const open = $("styles").hidden; 1832 $("styles").hidden = !open; $("plusBtn").setAttribute("aria-expanded", String(open)); 1833 }); 1834 document.querySelectorAll(".style-chip").forEach((b) => b.addEventListener("click", () => { 1835 $("styleBadge").hidden = !b.dataset.style; $("styleBadge").textContent = b.textContent; 1836 $("styles").hidden = true; $("plusBtn").setAttribute("aria-expanded", "false"); 1837 })); 1838 1839 /* ---------------- Hythm Guard reports (from the preview) ---------------- */ 1840 window.addEventListener("message", (e) => { 1841 if (e.source !== els.frame.contentWindow || !e.data?.hythmGuard) return; 1842 const { total, fixes } = e.data.hythmGuard; 1843 const b = $("guardBadge"); 1844 b.hidden = false;
1845 b.classList.toggle("fixed", total > 0); 1846 $("guardText").textContent = total ? `صÙÙØ ${total}` : "سÙÙÙ "; 1847 b.title = total ? "Hythm Guard صÙÙØ تÙÙØ§Ø¦ÙاÙ:\n" + Object.entries(fixes).map(([k, n]) => `⢠${k} Ã${n}`).join("\n") : "Hythm Guard: ٠ا Ù٠٠شاÙÙ ÙØ±Ø§Ø¡Ø© أ٠تباÙÙ"; 1848 }); 1849 1850 /* ---------------- Working overlay: mechanical morphing cloud + ETA ---------------- */ 1851 const EXPECTED = { fast: 15000, balanced: 26000, max: 42000 }; 1852 let work = null; 1853 /* ---- Shape engine: the cloud morphs through dozens of shapes (+ endless random blobs) ---- */ 1854 const shapeEngine = (() => { 1855 const N = 64, CX = 120, CY = 90, W = 170, H = 112; 1856 const polar = (fn) => Array.from({ length: N }, (_, i) => { const t = (i / N) * Math.PI * 2; const r = fn(t); return [Math.cos(t - Math.PI / 2) * r, Math.sin(t - Math.PI / 2) * r]; }); 1857 const param = (fn) => Array.from({ length: N }, (_, i) => fn((i / N) * Math.PI * 2)); 1858 const SHAPES = [ 1859 ["a cloud", "غÙÙ Ø©", () => polar((t) => { const up = Math.cos(t) > -0.15; return up ? 1 + 0.18 * Math.abs(Math.sin(2.5 * t)) : 0.72; }), 1.25], 1860 ["a heart", "ÙÙØ¨", () => param((t) => [16 * Math.pow(Math.sin(t), 3), -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t))])], 1861 ["a star", "ÙØ¬Ù Ø©", () => polar((t) => 0.62 + 0.38 * Math.pow(Math.abs(Math.cos(2.5 * t)), 2))], 1862 ["a flower", "ÙØ±Ø¯Ø©", () => polar((t) => 0.7 + 0.3 * Math.cos(6 * t))], 1863 ["a butterfly", "ÙØ±Ø§Ø´Ø©", () => param((t) => { const r = Math.exp(Math.sin(t)) - 2 * Math.cos(4 * t) + Math.pow(Math.sin((2 * t - Math.PI) / 24), 5); return [Math.sin(t) * r, -Math.cos(t) * r]; })], 1864 ["a fish", "Ø³Ù ÙØ©", () => param((t) => [Math.cos(t) - Math.pow(Math.sin(t), 2) / Math.SQRT2, Math.cos(t) * Math.sin(t)]), 1.4], 1865 ["infinity", "ÙØ§ÙÙØ§ÙØ©", () => param((t) => { const d = 1 + Math.pow(Math.sin(t), 2); return [Math.cos(t) / d, Math.sin(t) * Math.cos(t) / d]; }), 1.5], 1866 ["a gear", "ترس", () => polar((t) => 0.78 + 0.16 * Math.tanh(4 * Math.sin(8 * t)))], 1867 ["a raindrop", "ÙØ·Ø±Ø©", () => param((t) => [Math.sin(t) * Math.pow(Math.sin(t / 2), 1.2), -Math.cos(t)])], 1868 ["a diamond", "٠اسة", () => polar((t) => 1 / (Math.abs(Math.cos(t)) + Math.abs(Math.sin(t)) * 1.3))], 1869 ["a leaf", "ÙØ±ÙØ© شجر", () => param((t) => [Math.sin(t) * Math.abs(Math.sin(t)) * 0.9 * Math.cos(t / 2 - 0.3), -Math.cos(t)])], 1870 ["a cat", "ÙØ·Ø©", () => polar((t) => { const e = Math.exp(-Math.pow((Math.abs(t - Math.PI) - 2.45) * 3.4, 2)); return 0.8 + 0.42 * e; })], 1871 ["a crown", "تاج", () => polar((t) => { const up = Math.cos(t) > 0.1; return up ? 0.82 + 0.25 * Math.max(0, Math.cos(5 * t)) : 0.78; }), 1.2], 1872 ["a sun", "ش٠س", () => polar((t) => 0.78 + 0.12 * Math.sign(Math.sin(12 * t)) * Math.pow(Math.abs(Math.sin(12 * t)), 0.4))], 1873 ["a moon", "Ù٠ر", () => param((t) => { const x = Math.cos(t), y = Math.sin(t); return x > 0 ? [x * 0.35, y] : [x, y]; })], 1874 ["a bubble", "ÙÙØ§Ø¹Ø©", () => polar(() => 1)], 1875 ["a clover", "برسÙÙ ", () => polar((t) => 0.55 + 0.45 * Math.abs(Math.cos(2 * t)))], 1876 ["a shell", "ØµØ¯ÙØ©", () => polar((t) => 0.6 + 0.35 * Math.abs(Math.sin(t / 2)) + 0.06 * Math.cos(14 * t))], 1877 ["a bolt", "برÙ", () => polar((t) => 0.55 + 0.45 * Math.pow(Math.abs(Math.sin(1.5 * t + 0.4)), 3))], 1878 ["a puffy cloud", "غÙÙ Ø© Ù ÙÙÙØ´Ø©", () => polar((t) => 0.85 + 0.15 * Math.abs(Math.sin(4 * t)) + (Math.cos(t) < -0.3 ? -0.1 : 0)), 1.3] 1879 ]; 1880 const randomBlob = () => { const k = 3 + Math.floor(Math.random() * 5), ph = Math.random() * 6, a1 = 0.1 + Math.random() * 0.22, a2 = Math.random() * 0.12; return polar((t) => 0.78 + a1 * Math.sin(k * t + ph) + a2 * Math.cos((k + 2) * t)); }; 1881 const fit = (pts, stretch = 1) => { 1882 let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; 1883 pts.forEach(([x, y]) => { minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y); }); 1884 const sx = W / (maxX - minX || 1), sy = H / (maxY - minY || 1), sc = Math.min(sx, sy); 1885 return pts.map(([x, y]) => [CX + (x - (minX + maxX) / 2) * sc * Math.min(stretch, sx / sc), CY + (y - (minY + maxY) / 2) * sc]); 1886 }; 1887 const toPath = (p) => { 1888 let d = `M${p[0][0].toFixed(1)} ${p[0][1].toFixed(1)}`; 1889 for (let i = 0; i < N; i++) { 1890 const p0 = p[(i - 1 + N) % N], p1 = p[i], p2 = p[(i + 1) % N], p3 = p[(i + 2) % N]; 1891 d += ` C${(p1[0] + (p2[0] - p0[0]) / 6).toFixed(1)} ${(p1[1] + (p2[1] - p0[1]) / 6).toFixed(1)} ${(p2[0] - (p3[0] - p1[0]) / 6).toFixed(1)} ${(p2[1] - (p3[1] - p1[1]) / 6).toFixed(1)} ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`; 1892 } 1893 return d + "Z"; 1894 }; 1895 const ease = (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2); 1896 const el = document.getElementById("mcShape");
1897 let cur = fit(SHAPES[0][2](), SHAPES[0][3]), raf = 0, running = false, order = [], count = 0; 1898 el.setAttribute("d", toPath(cur)); 1899 const nextShape = () => { 1900 count++; 1901 if (count % 4 === 0) return { en: "something new", ar: "Ø´ÙÙ Ø¬Ø¯ÙØ¯", pts: fit(randomBlob()) }; 1902 if (!order.length) order = SHAPES.slice(1).sort(() => Math.random() - 0.5).concat([SHAPES[0]]); 1903 const sh = order.shift(); 1904 return { en: sh[0], ar: sh[1], pts: fit(sh[2](), sh[3]) }; 1905 }; 1906 const morph = () => { 1907 if (!running) return; 1908 const nx = nextShape(), from = cur, t0 = performance.now(), DUR = 1500; 1909 const cap = document.getElementById("workShape"); 1910 const lang = window.HythmI18n?.lang || "en"; 1911 cap.classList.remove("in"); void cap.offsetWidth; 1912 cap.textContent = lang === "en" ? `â becoming ${nx.en}â¦` : `â Ø¹Ù ØªØµÙØ± ${nx.ar}â¦`; 1913 cap.classList.add("in"); 1914 const step = (now) => { 1915 if (!running) return; 1916 const k = ease(Math.min(1, (now - t0) / DUR)); 1917 cur = from.map((p, i) => [p[0] + (nx.pts[i][0] - p[0]) * k, p[1] + (nx.pts[i][1] - p[1]) * k]); 1918 el.setAttribute("d", toPath(cur)); 1919 if (k < 1) raf = requestAnimationFrame(step); else setTimeout(morph, 1300); 1920 }; 1921 raf = requestAnimationFrame(step); 1922 }; 1923 const TIPS = { 1924 en: ["Tip: the ⦠button shows your design in every theme â free.", "Tip: âAaâ reshuffles fonts from a library of 229 â free.", "Hythm Guard is checking contrast, blurry text and invisible buttons.", "Tip: Omni Ultra 2.0 plans the art direction before it builds.", "Tip: press Ctrl+K to search all your projects.", "Fun fact: the loader has 20 shapes + endless random ones.", "Tip: edits only send the changed lines â cheaper and faster.", "Tip: the ð badge lists errors and fixes them with one click."], 1925 ar: ["ÙØµÙØØ©: زر â¦ Ø¨ÙØ¹Ø±Ø¶ تص٠Ù٠٠بÙÙ Ø§ÙØ«Ù٠ات Ù Ø¬Ø§ÙØ§Ù.", "ÙØµÙØØ©: زر Aa Ø¨ÙØ¨Ø¯ÙÙ Ø§ÙØ®Ø·ÙØ· Ù Ù Ù ÙØªØ¨Ø© 229 خط Ù Ø¬Ø§ÙØ§Ù.", "Hythm Guard ع٠ÙÙØØµ Ø§ÙØªØ¨Ø§ÙÙ ÙØ§ÙÙØªØ§Ø¨Ø© Ø§ÙØ¶Ø¨Ø§Ø¨ÙØ© ÙØ§Ùأزرار اÙÙ Ø®ÙÙØ©.", "ÙØµÙØØ©: Omni Ultra 2.0 Ø¨ÙØ®Ø·Ø· Ø§ÙØ¥Ø®Ø±Ø§Ø¬ اÙÙÙÙ ÙØ¨Ù ٠ا ÙØ¨ÙÙ.", "ÙØµÙØØ©: Ctrl+K ÙÙØ¨ØØ« بÙÙ Ù Ø´Ø§Ø±ÙØ¹Ù.", "٠عÙÙÙ Ø©: اÙÙÙØ¯Ø± ÙÙÙ 20 Ø´ÙÙ ÙØ£Ø´ÙØ§Ù Ø¹Ø´ÙØ§Ø¦ÙØ© Ø¨ÙØ§ ÙÙØ§ÙØ©.", "ÙØµÙØØ©: Ø§ÙØªØ¹Ø¯ÙÙØ§Øª بتبعت Ø§ÙØ£Ø³Ø·Ø± اÙÙ ØªØºÙØ±Ø© Ø¨Ø³Ø Ø£Ø±Ø®Øµ ÙØ£Ø³Ø±Ø¹.", "ÙØµÙØØ©: شارة ð Ø¨ØªØ¹Ø±Ø¶ Ø§ÙØ£Ø®Ø·Ø§Ø¡ ÙØ¨ØªØµÙÙØÙØ§ بضغطة."] 1926 }; 1927 let tipTimer = 0; 1928 const tip = () => { const L = TIPS[window.HythmI18n?.lang || "en"] || TIPS.en; const e = document.getElementById("workTip"); e.classList.remove("in"); void e.offsetWidth; e.textContent = L[Math.floor(Math.random() * L.length)]; e.classList.add("in"); }; 1929 return { 1930 start() { if (running) return; running = true; morph(); tip(); tipTimer = setInterval(tip, 6000); }, 1931 stop() { running = false; cancelAnimationFrame(raf); clearInterval(tipTimer); } 1932 }; 1933 })(); 1934 function workStart(editing) { 1935 work = { t0: Date.now(), tFirst: 0, expected: editing ? 3500 : EXPECTED[mode] || 26000, editing }; 1936 els.app.classList.remove("no-design", "is-empty"); 1937 els.placeholder.hidden = true; 1938 $("working").hidden = false; $("guardBadge").hidden = true; $("diagBtn").hidden = true; 1939 if (appSettings.motion !== "reduced") shapeEngine.start(); 1940 $("working").classList.toggle("max", mode === "max"); 1941 $("workBar").style.width = "3%"; 1942 $("workPhase").textContent = mode === "max" ? "ÙÙÙÙØ± بع٠٠ÙÙØ®Ø·Ø· Ø§ÙØªØµÙ ÙÙ â¦" : "ÙÙÙÙØ± ÙÙØ®Ø·Ø·â¦"; 1943 $("workEta").textContent = "اÙÙÙØª اÙÙ ÙØ¯Ùر: ÙØªÙ Ø§ÙØØ³Ø§Ø¨â¦"; 1944 } 1945 function workUpdate(chars, phase, finishing) { 1946 if (!work) return; 1947 if (phase) $("workPhase").textContent = phase; 1948 if (finishing) { $("workBar").style.width = "97%"; $("workEta").textContent = "Ø«ÙØ§ÙÙ ÙØ¨ÙØ¬ÙØ²â¦"; return; } 1949 if (chars && !work.tFirst) work.tFirst = Date.now(); 1950 if (chars >
1950 work.expected * 0.92) work.expected = Math.round(chars * 1.15); 1951 const pct = Math.min(0.95, chars / work.expected); 1952 $("workBar").style.width = Math.max(3, pct * 100).toFixed(1) + "%"; 1953 if (!chars || !work.tFirst) { $("workEta").textContent = "اÙÙÙØª اÙÙ ÙØ¯Ùر: ÙØªÙ Ø§ÙØØ³Ø§Ø¨â¦"; return; } 1954 const secs = Math.max(1, (Date.now() - work.tFirst) / 1000); 1955 const speed = chars / secs; 1956 const left = Math.max(3, Math.round((work.expected - chars) / Math.max(speed, 1))); 1957 $("workEta").textContent = left > 90 ? `اÙÙÙØª اÙÙ ÙØ¯Ùر: ~${Math.round(left / 60)} دÙÙÙØ©` : `اÙÙÙØª اÙÙ ÙØ¯Ùر: ~${left} ثاÙÙØ©`; 1958 } 1959 function workEnd() { 1960 work = null; 1961 shapeEngine.stop(); 1962 $("working").hidden = true; 1963 if (!session.versions.length) { els.app.classList.add("no-design"); els.placeholder.hidden = false; } 1964 } 1965 1966 /* ---------------- Smart understanding: a few key questions for short prompts ---------------- */ 1967 const needsClarify = (t) => !storage.get("hythm.noAsk") && t.split(/\s+/).filter(Boolean).length <= 8 && t.length < 80; 1968 const QUESTIONS = [ 1969 { id: "goal", q: "ش٠اÙÙØ¯Ù Ø§ÙØ£Ø³Ø§Ø³Ù ٠٠اÙÙ ÙÙØ¹Ø", opts: ["Ø¨ÙØ¹ Ù ÙØªØ¬ أ٠خد٠ة", "تعرÙÙ Ø¨Ø§ÙØ¹Ùا٠ة ÙØ§ÙÙØµØ©", "ØØ¬Ùزات ÙÙ ÙØ§Ø¹Ùد", "عرض أع٠ا٠(Ø¨ÙØ±ØªÙÙÙÙÙ)", "ÙÙØØ© تØÙ٠أ٠تطبÙÙ"] }, 1970 { id: "aud", q: "Ù ÙÙ Ø§ÙØ¬Ù ÙÙØ±Ø", opts: ["شباب ÙØ¹ØµØ±ÙÙÙ", "Ø¹Ø§Ø¦ÙØ§Øª", "Ø´Ø±ÙØ§Øª ÙÙ ØØªØ±ÙÙÙ", "ÙØ®Ø¨Ø© ÙÙØ®Ø§Ù Ø©", "Ø£Ø·ÙØ§Ù ÙØ£ÙاÙÙ"] }, 1971 { id: "vibe", q: "Ø£Ù Ø£Ø¬ÙØ§Ø¡ Ø¨ØªØØ¨Ø", swatches: true }, 1972 { id: "type", q: "Ø´ÙÙ Ø§ÙØ®Ø·Ø", opts: [["Ø¹ØµØ±Ù ÙØ¸ÙÙ", "modern"], ["ÙÙØ§Ø³ÙÙÙ ÙØ®Ù ", "classic"], ["Ø¬Ø±ÙØ¡ ÙØ¶Ø®Ù ", "bold"], ["Ù Ø±Ø ÙØ¯Ø§Ø¦Ø±Ù", "playful"], ["تÙÙÙ Ù Ø³ØªÙØ¨ÙÙ", "tech"]] } 1973 ]; 1974 function showClarify(text) { 1975 const div = document.createElement("div"); 1976 div.className = "msg"; 1977 const card = document.createElement("div"); 1978 card.className = "clarify"; 1979 card.innerHTML = ` 1980 <div class="clarify-head"><span class="ai-avatar h-mark"><svg class="cloud-logo"><use href="#logo-cloud"/></svg></span><div><b>Ø®ÙÙÙ٠أÙÙÙ Ù Ø£ØØ³Ù</b><small>${esc(text)} · Ø¬Ø§ÙØ¨ بسرعة أ٠تخطÙÙ</small></div></div> 1981 ${QUESTIONS.map((Q) => ` 1982 <div class="cq" data-q="${Q.id}"> 1983 <div class="cq-title">${Q.q}</div> 1984 <div class="cq-opts"> 1985 ${Q.swatches 1986 ? `<button type="button" class="cq-opt" data-v="">⦠Hythm ÙØ®ØªØ§Ø±</button>` + Object.entries(THEME_META).map(([k, m]) => `<button type="button" class="cq-opt sw-opt" data-v="${k}" title="${m[0]}">${swatchHTML(k)}<span>${m[0]}</span></button>`).join("") 1987 : Q.opts.map((o) => { const [l, v] = Array.isArray(o) ? o : [o, o]; return `<button type="button" class="cq-opt" data-v="${v}">${l}</button>`; }).join("")} 1988 <button type="button" class="cq-opt cq-other">ØºÙØ± ذÙÙâ¦</button> 1989 </div> 1990 <input class="cq-input" type="text" placeholder="Ø§ÙØªØ¨ ÙØµØ¯Ùâ¦" hidden> 1991 </div>`).join("")} 1992 <div class="cq"><div class="cq-title">اس٠اÙÙ Ø´Ø±ÙØ¹ <small>(Ø§Ø®ØªÙØ§Ø±Ù)</small></div><input class="cq-input" id="cqName" type="text" placeholder="Ù Ø«ÙØ§Ù: Ø³ØØ§Ø¨ ÙÙÙÙÙØ©"></div> 1993 <div class="clarify-actions"> 1994 <button type="button" class="clarify-go">ص٠ÙÙ Ø§ÙØ¢Ù</button> 1995 <button type="button" class="clarify-skip">تخطÙÙ Ø§ÙØ£Ø³Ø¦ÙØ©</button> 1996 <button type="button" class="clarify-never">٠ا تسأÙÙ٠٠رة ثاÙÙØ©</button> 1997 </div>`; 1998 div.appendChild(card); 1999 els.messages.appendChild(div); 2000 els.app.classList.remove("is-empty"); 2001 scrollDown(); 2002 card.querySelectorAll(".cq").forEach((cq) => { 2003 cq.querySelectorAll(".cq-opt").forEach((b) => b.addEventListener("click", () => { 2004 cq.querySelectorAll(".cq-opt").forEach((x) => x.classList.toggle("on", x === b)); 2005 const inp = cq.querySelector(".cq-input"); 2006 if (inp) { inp.hidden = !b.classList.contains("cq-other"); if (!inp.hidden) inp.focus(); } 2007 })); 2008 }); 2009 const answer = (id) => { 2010 const cq = card.querySelector(`.cq[data-q="${id}"]`); 2011 const on = cq.querySelector(".cq-opt.on"); 2012 if (!on) return null; 2013 if (on.classList.contains("cq-other")) return { text: cq.querySelector(".cq-input").value.trim() || null, value: null }; 2014 return { text: on.textContent.trim(), value: on.dataset.v }; 2015 }; 2016 const finish = (skip) => { 2017 let full = text; 2018 if (!skip) { 2019 const g = answer("goal"), a = answer("aud"), v = answer("vibe"), t = answer("type"), name = card.querySelector("#cqName").value.trim(); 2020 const parts = []; 2021 if (g?.text) parts.push("اÙÙØ¯Ù: " + g.text); 2022 if (a?.text) parts.push("Ø§ÙØ¬Ù ÙÙØ±: " + a.text); 2023 if (v?.value) parts.push(`Ø§ÙØ£Ø¬Ùاء: ${THEME_META[v.value][0]} (use data-theme="${v.value}")`); 2024 else if (v?.text && v.text !== "⦠Hythm ÙØ®ØªØ§Ø±") parts.push("Ø§ÙØ£Ø¬Ùاء: " + v.text); 2025 if (t?.text) parts.push("Ø§ÙØ®Ø·: " + t.text);
2026 if (name) parts.push("اس٠اÙÙ Ø´Ø±ÙØ¹: " + name); 2027 if (t?.value && window.HYTHM_TYPE_STYLES[t.value]) pendingType = t.value; 2028 if (parts.length) full = text + "\nâ " + parts.join("\nâ "); 2029 } 2030 div.remove(); 2031 send(full); 2032 }; 2033 card.querySelector(".clarify-go").addEventListener("click", () => finish(false)); 2034 card.querySelector(".clarify-skip").addEventListener("click", () => finish(true)); 2035 card.querySelector(".clarify-never").addEventListener("click", () => { storage.set("hythm.noAsk", "1"); toast("ØªÙ Ø§Ù Ø Ø±Ø Ø£ØµÙ Ù Ù Ø¨Ø§Ø´Ø±Ø© Ù Ù ÙÙÙ"); finish(true); }); 2036 } 2037 2038 /* ---------------- Theme swatches (two colors) + font shuffle ---------------- */ 2039 function refreshSwatch() { 2040 const v = session.versions[session.current]; 2041 $("fontsBtn").hidden = !v; 2042 if (!v) return; 2043 const k = themeOf(v.code), m = THEME_META[k] || THEME_META.minimal; 2044 $("swatchNow").style.setProperty("--a", m[1]); $("swatchNow").style.setProperty("--b", m[2]); 2045 $("swatchName").textContent = m[0]; 2046 const f = fontsOf(v.code); 2047 $("fontsBtn").title = f[0] ? `Ø§ÙØ®Ø·ÙØ·: ${f[0]} + ${f[1]} · ${f[2]} + ${f[3]} â Ø§Ø¶ØºØ· ÙÙØªØ¨Ø¯ÙÙ (Ù Ø¬Ø§ÙØ§Ù)` : "بدÙÙ Ø§ÙØ®Ø·ÙØ· (Ù Ø¬Ø§ÙØ§Ù)"; 2048 } 2049 $("themePick").addEventListener("click", (e) => { 2050 e.preventDefault(); e.stopPropagation(); 2051 const pop = $("swatchPop"); 2052 if (!pop.hidden) { pop.hidden = true; return; } 2053 const cur = themeOf(session.versions[session.current]?.code || ""); 2054 pop.innerHTML = Object.entries(THEME_META).map(([k, m]) => `<button type="button" class="sw-item${k === cur ? " on" : ""}" data-t="${k}">${swatchHTML(k)}<span>${m[0]}</span></button>`).join(""); 2055 const r = $("themePick").getBoundingClientRect(); 2056 pop.style.top = r.bottom + 8 + "px"; pop.style.left = Math.max(8, Math.min(innerWidth - 260, r.left + r.width / 2 - 125)) + "px"; 2057 pop.hidden = false; 2058 pop.querySelectorAll(".sw-item").forEach((b) => b.addEventListener("click", () => { 2059 pop.hidden = true; 2060 if (b.dataset.t === cur) return; 2061 const sel = $("themeSelect"); sel.value = b.dataset.t; sel.dispatchEvent(new Event("change")); 2062 })); 2063 }); 2064 document.addEventListener("click", (e) => { if (!e.target.closest("#swatchPop")) $("swatchPop").hidden = true; }); 2065 $("fontsBtn").addEventListener("click", () => { 2066 const v = session.versions[session.current]; 2067 if (!v) return; 2068 const code = withFonts(v.code, null, true); 2069 session.versions.push({ code, t: Date.now(), mode: v.mode }); 2070 const idx = session.versions.length - 1, f = fontsOf(code); 2071 const entry = { type: "ai", notes: (window.HythmI18n ? HythmI18n.t : (x) => x)(`بدÙÙØª Ø§ÙØ®Ø·ÙØ· Ø¥ÙÙ **${f[0]}** + **${f[1]}**Ø ÙÙÙØ¹Ø±Ø¨Ù **${f[2]}** + **${f[3]}**Ø Ù Ø¬Ø§ÙØ§Ù.`), version: idx, versionKind: "patch", meta: { cost: 0 } }; 2072 renderEntry(entry); pushLog(entry); selectVersion(idx, { silent: true }); scrollDown(); 2073 }); 2074 2075 /* ---------------- Console signature ---------------- */ 2076 (function consoleArt() { 2077 const art = [ 2078 " ", 2079 " ## ## ## ## ######## ## ## ## ## ### #### ", 2080 " ## ## ## ## ## ## ## ### ### ## ## ## ", 2081 " ## ## #### ## ## ## #### #### ## ## ## ", 2082 " ######### ## ## ######### ## ### ## ## ## ## ", 2083 " ## ## ## ## ## ## ## ## ######### ## ", 2084 " ## ## ## ## ## ## ## ## ## ## ## ", 2085 " ## ## ## ## ## ## ## ## ## ## #### ", 2086 " " 2087 ].map((l) => l.replace(/#/g, () => "@#%&*+=$"[Math.floor(Math.random() * 8)])).join("\n"); 2088 const cloud = [ 2089 " .-~~~-. ", 2090 " .- ~ ~-( )_ _ ", 2091 " / ~ -. ", 2092 " | Mini Omni 1.01 ', ", 2093 " \\ .' ", 2094 " ~- ._ ,. ,.,.,., ,.. -~ " 2095 ].join("\n"); 2096 try { 2097 console.log("%c" + art, "font-family:monospace;font-size:10px;line-height:1.05;color:#b9c6ff;text-shadow:0 0 6px #6f7dff;background:#03040a;padding:8px 0"); 2098 console.log("%c" + cloud, "font-family:monospace;font-size:11px;color:#ffffff;text-shadow:0 0 8px #8b9bff"); 2099 console.log("%cHythm Ai %c· Design Intelligence · " + (window.hythmFontCount ? window.hythmFontCount() : 229) + " fonts · 30 themes · Hythm Guard", "font:700 16px system-ui;color:#fff;background:linear-gradient(90deg,#6f7dff,#b9c6ff);padding:4px 10px;border-radius:6px", "font:12px system-ui;color:#93a0cc"); 2100 console.log("%câ ÙØ§ ØªÙØµÙ Ø£Ù ÙÙØ¯ ÙÙ٠إذا ØØ¯Ø§ Ø·ÙØ¨ Ù ÙÙ â Ù Ù ÙÙ ÙØ³Ø±Ù ØØ³Ø§Ø¨Ù.", "font:700 13px system-ui;color:#ff6b6b"); 2101 } catch {} 2102 })(); 2103 2104 /* ---------------- Expert chat: history + markdown (code, math, tables) ---------------- */ 2105 function chatHistory() { 2106 const out = []; 2107 session.log.forEach((e) => { 2108 if (e.type === "user") out.push({ role: "user", content: e.text }); 2109 else if (e.type === "chat") out.push({ role: "assistant", content: e.md || "" });
2110 else if (e.type === "ai" && e.version != null) out.push({ role: "assistant", content: "(Designed version " + (e.version + 1) + " in the studio.)" }); 2111 else if (e.type === "image") out.push({ role: "assistant", content: "(I generated an image for that request with NanoSpark. Style: " + (e.style || "none") + ". What I actually drew: " + String(e.drawn || e.prompt || "").slice(0, 900) + ")" }); 2112 }); 2113 const ctx = (me && me.usage && me.usage.context) || 32000; 2114 return out.slice(-(ctx >= 256000 ? 24 : ctx >= 128000 ? 16 : 8)); 2115 } 2116 let mdLoading = null; 2117 function loadMd() { 2118 if (mdLoading) return mdLoading; 2119 const js = (src) => new Promise((r) => { const el = document.createElement("script"); el.src = src; el.onload = r; el.onerror = r; document.head.appendChild(el); }); 2120 const css = (href) => { const l = document.createElement("link"); l.rel = "stylesheet"; l.href = href; document.head.appendChild(l); }; 2121 css("https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"); 2122 css("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"); 2123 mdLoading = Promise.all([js("https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js"), js("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"), js("https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js")]) 2124 .then(() => js("https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js")) 2125 .then(() => { 2126 if (!window.marked) return; 2127 marked.use({ gfm: true, breaks: false, renderer: { 2128 html(t) { return esc(typeof t === "string" ? t : (t && t.text) || ""); }, // no raw HTML from the model 2129 link(href, title, text) { const h = typeof href === "object" ? href.href : href; const tx = typeof href === "object" ? href.text : text; return /^(https?:|mailto:|#)/i.test(h || "") ? `<a href="${esc(h)}" target="_blank" rel="noopener noreferrer">${tx}</a>` : tx; }, 2130 image(href) { const h = typeof href === "object" ? href.href : href; return /^https?:/i.test(h || "") ? `<img src="${esc(h)}" alt="" loading="lazy">` : ""; } 2131 } }); 2132 document.querySelectorAll(".md-body[data-md]").forEach((el) => renderMdInto(el, el.dataset.md)); 2133 }); 2134 return mdLoading; 2135 } 2136 // answer helpers: hide the follow-up tag while streaming, parse it at the end 2137 // a motion / 3D / game page pasted as plain text (no code fence), or importing the engine as a module â repaired so it renders and plays 2138 function fixCodeAnswer(md) { 2139 let t = String(md || ""); 2140 if (!/HM\.film\(|hythm-motion\.js|<!doctype html|<html[\s>]/i.test(t)) return t; 2141 t = t.replace(/^\s*import\s*\{\s*HM\s*\}\s*from\s*["'][^"']*hythm-motion\.js["'];?\s*$/gm, "").replace(/<script\s+type=["']module["']>/gi, "<script>"); 2142 if (/```/.test(t)) return t; 2143 const start = t.search(/<!doctype html|<html[\s>]|<script\s+src=["']\/hythm-motion\.js/i); if (start < 0) return t; 2144 const endM = /<\/html>/i.exec(t.slice(start)), lastScript = t.lastIndexOf("</script>"); 2145 const end = endM ? start + endM.index + 7 : lastScript > start ? lastScript + 9 : t.length; 2146 let code = t.slice(start, end).trim(); 2147 if (!/<html/i.test(code)) code = '<!doctype html>\n<html><head><meta charset="utf-8"></head>\n<body>\n' + code + "\n</body></html>"; 2148 return t.slice(0, start).trim() + "\n\n```html\n" + code + "\n```\n\n" + t.slice(end).trim(); 2149 } 2150 function visibleAnswer(md) { md = fixCodeAnswer(md); return (window.HythmFiles ? HythmFiles.strip(md) : String(md || "")).replace(/<\s*\/?\s*r?hythm-route[^>]*>(\s*(design|image|edit)\s*<\s*\/\s*r?hythm-route\s*>)?/gi, "").replace(/<\s*hythm-next[\s\S]*$/i, "").replace(/\s+$/, ""); } 2151 function followups(md) { 2152 const m = String(md || "").match(/<\s*hythm-next\s*>([\s\S]*?)(<\s*\/\s*hythm-next\s*>|$)/i); 2153 return m ? m[1].split("|").map((x) => x.trim()).filter((x) => x && x.length < 140).slice(0, 3) : []; 2154 } 2155 // render markdown by diffing top-level blocks: finished blocks stay put, new blocks fade in 2156 const mdScratch = document.createElement("div"); 2157 // while an answer streams, heavy extras (syntax colouring, live games, diagrams) wait for the final render 2158 var mdStreaming = false; // var: renderMdInto can run before this line during start-up 2159 function smoothMd(el, md) { 2160 mdStreaming = true; try { renderMdInto(mdScratch, md); } finally { mdStreaming = false; } 2161 el.classList.add("md-body"); 2162 const next = [...mdScratch.children], cur = el.children; 2163 next.forEach((n, i) => { 2164 const c = cur[i]; 2165 if (!c) { n.classList.add("md-in"); el.appendChild(n); } 2166 else if (c.outerHTML.replace(/ md-in|md-in ?/g, "") !== n.outerHTML) { if (c.classList.contains("md-in")) n.classList.add("md-in-done"); el.replaceChild(n, c); } 2167 }); 2168 while (el.children.length > next.length) el.lastElementChild.remove(); 2169 } 2170 // ---- Hythm Live Code: runs code in a locked sandbox (no access to the site, cookies or your account) ---- 2171 // JavaScript / HTML / SVG natively, Python via Pyodide (real CPython in WebAssembly). Errors come back with a "fix it" button. 2172 const PYODIDE = "https://cdn.jsdelivr.net/npm/[email protected]/"; 2173 const CONSOLE_CSS = "<style>body{font:13px/1.6 ui-monospace,Consolas,monospace;margin:0;color:#ddd;background:#111}#h{padding:8px 12px;color:#8b8f98;border-bottom:1px solid #222;font:12px system-ui}#o{padding:8px 12px}.l{white-space:pre-wrap;border-bottom:1px solid #1d1d1d;padding:2px 0}.e{color:#ff8080}.i{color:#8bc6ff}</style>"; 2174 const CONSOLE_JS = "var o=document.getElementById('o'),H=document.getElementById('h');function w(c,a){var d=document.createElement('div');d.className='l '+c;d.textContent=[].map.call(a,function(x){try{return typeof x==='object'?JSON.stringify(x,null,2):String(x)}catch(e){return String(x)}}).join(' ');o.appendChild(d)}" + 2175 "function fail(m){w('e',[m]);parent.postMessage({hythmRun:'error',message:String(m).slice(0,2000)},'*')}function done(ms){H.textContent='â '+ms+' ms';parent.postMessage({hythmRun:'done'},'*')}"; 2176 function runnerDoc(lang, code) { 2177 const L = String(lang || "").toLowerCase(); 2178 const safe = (t) => t.replace(/<\/script>/gi, "<\\/script>"); 2179 // the sandbox (no same-origin, for safety) blocks localStorage â games save best scores there, so they get an 2180 // in-memory stand-in instead of crashing on start 2181 const STORE_SHIM = "<script>(function(){function S(){var d={};return{getItem:function(k){return Object.prototype.hasOwnProperty.call(d,k)?d[k]:null},setItem:function(k,v){d[k]=String(v)},removeItem:function(k){delete d[k]},clear:function(){d={}},key:function(i){return Object.keys(d)[i]||null},get length(){return Object.keys(d).length}}}" + 2182 "['localStorage','sessionStorage'].forEach(function(n){try{window[n].length}catch(e){try{Object.defineProperty(window,n,{value:S(),configurable:true})}catch(_){}}});try{document.cookie}catch(e){try{Object.defineProperty(document,'cookie',{get:function(){return''},set:function(){},configurable:true})}catch(_){}}})();<\/script>"; 2183 if (L === "svg") return "<body style='margin:0;display:grid;place-items:center;min-height:100vh;background:#fff'>" + code + "</body>"; 2184 if (/^(html|xml)$/.test(L)) return /<head[^>]*>/i.test(code) ? code.replace(/<head[^>]*>/i, (m) => m + STORE_SHIM) : STORE_SHIM + code; 2185 if (/^(py|python|python3)$/.test(L)) { 2186 return CONSOLE_CSS + "<div id=h>â³ loading Pythonâ¦</div><div id=o></div><script>" + CONSOLE_JS + "<\/script><script src='" + PYODIDE + "pyodide.js'><\/script><script>(async function(){var t0=performance.now();try{" + 2187 "var py=await loadPyodide({indexURL:'" + PYODIDE + "',stdout:function(s){w('',[s])},stderr:function(s){w('e',[s])}});H.textContent='â¶ runningâ¦';" + 2188 "var src=" + JSON.stringify(code) + ";try{await py.loadPackagesFromImports(src)}catch(e){}" + 2189 "await py.runPythonAsync(src);done(Math.round(performance.now()-t0))}catch(e){fail(e&&e.message||e);H.textContent='â error'}})();<\/script>"; 2190 } 2191 // JavaScript (default) 2192 return CONSOLE_CSS + "<div id=h>â¶ runningâ¦</div><div id=o></div><script>" + CONSOLE_JS + ";console.log=function(){w('',arguments)};console.info=function(){w('i',arguments)};console.error=function(){w('e',arguments)};console.warn=console.log;" + 2193 "window.onerror=function(m,s,l){fail(m+(l?' (line '+l+')':''));return true};window.onunhandledrejection=function(e){fail(e.reason&&e.reason.message||e.reason)};var __t0=performance.now();<\/script><script>" + safe(code) + "\n;done(Math.round(performance.now()-__t0))<\/script>"; 2194 } 2195 function livePlay(pre, lang, code) { 2196 if (pre.previousElementSibling && pre.previousElementSibling.classList.contains("live-play")) return; 2197 const T = (x) => (window.HythmI18n ? HythmI18n.t(x) : x); 2198 const card = document.createElement("div"); card.className = "live-play no-i18n";
2199 const kind = /GLTFExporter|OrbitControls|RoomEnvironment/.test(code) ? "3d" : /hythm-motion|HM\.film|captureStream|MediaRecorder|gsap/i.test(code) ? "motion" : "game"; 2200 const label = { "3d": ["ð§ Ø§Ø¹Ø±Ø¶ اÙ٠جس٠", "شاشة ÙØ§Ù ÙØ©"], motion: ["ð¬ Ø´ØºÙ٠اÙÙÙØ¯ÙÙ", "شاشة ÙØ§Ù ÙØ©"], game: ["ⶠشغÙ٠اÙÙØ¹Ø¨Ø©", "Ø§ÙØ¹Ø¨ بشاشة ÙØ§Ù ÙØ©"] }[kind]; 2201 card.classList.add("lp-" + kind); 2202 card.innerHTML = '<div class="lp-stage"><button type="button" class="lp-start">' + esc(T(label[0])) + '</button></div><div class="lp-bar"><button type="button" class="btn-soft" data-full>â¶ ' + esc(T(label[1])) + '</button><button type="button" class="btn-soft" data-code>{ } ' + esc(T("اعرض اÙÙÙØ¯")) + "</button></div>"; 2203 pre.before(card); 2204 pre.classList.add("lp-folded"); 2205 const start = () => { 2206 const stage = card.querySelector(".lp-stage"); 2207 const f = document.createElement("iframe"); 2208 f.setAttribute("sandbox", "allow-scripts allow-pointer-lock allow-downloads"); // null origin: no cookies, no site access; downloads = the GLB / video it exports 2209 f.setAttribute("allow", "fullscreen; gamepad; autoplay"); 2210 // an error catcher runs first inside the game, so Hythm sees its own bugs and can repair them 2211 const catcher = "<script>(function(){var s=0;function send(m){m=String(m||'').trim();if(!m||/^(Uncaught|Error|Script error)[.]?( [(]line 0[)])?$/i.test(m))return;if(s++<3)parent.postMessage({hythmPlayErr:m.slice(0,400)},'*')}" + 2212 "window.addEventListener('error',function(e){var er=e.error;send(((er&&(er.name?er.name+': ':'')+er.message)||e.message||'Error')+(e.lineno?' (line '+e.lineno+')':'')+(e.target&&e.target.src?' loading '+e.target.src:''))},true);" + 2213 "window.addEventListener('unhandledrejection',function(e){send('Unhandled: '+(e.reason&&(e.reason.message||e.reason)))});})();<\/script>"; 2214 const doc = runnerDoc(lang, code); 2215 f.srcdoc = /<head[^>]*>/i.test(doc) ? doc.replace(/<head[^>]*>/i, (m) => m + catcher) : catcher + doc; 2216 stage.innerHTML = ""; stage.appendChild(f); 2217 card._frame = f; 2218 setTimeout(() => { try { f.focus(); } catch {} }, 300); 2219 }; 2220 card._fix = (err) => { 2221 if (card.querySelector(".lp-err")) return; 2222 const bar = document.createElement("div"); bar.className = "lp-err"; 2223 bar.innerHTML = "<span>â " + esc(err) + '</span><button type="button" class="btn-soft">ð ' + esc(T("صÙÙØ Ø§ÙØ®Ø·Ø£")) + "</button>"; 2224 card.querySelector(".lp-bar").before(bar); 2225 const repair = () => { 2226 // never interrupt an answer that is still being written â wait for it, then repair 2227 if (controller) { bar.querySelector("button").disabled = true; bar.querySelector("span").textContent = "ð " + T("ÙÙÙØª Ø®Ø·Ø£Ø Ø±Ø ØµÙÙØÙ Ù٠ا ÙØ®Ùص Ø§ÙØ±Ø¯â¦"); return setTimeout(repair, 1500); } 2228 bar.querySelector("button").disabled = true; bar.querySelector("span").textContent = "ð " + T("ÙÙÙØª خطأ باÙÙØ¹Ø¨Ø©Ø ع٠صÙÙØÙâ¦"); 2229 send("The game you just wrote crashed with this error: \"" + err + "\". Find the root cause, fix it, and reply with the COMPLETE corrected HTML page (all the code, in one ```html block), then one line saying what you fixed.", { task: "chat", files: [{ name: "game.html", text: code }] }); 2230 }; 2231 bar.querySelector("button").addEventListener("click", repair); 2232 // repair once by itself (only for the game that was just written, never in a loop) 2233 if (!card.dataset.autofixed && !/The game you just wrote crashed/.test((session.log.filter((x) => x.type === "user").pop() || {}).text || "")) { card.dataset.autofixed = 1; setTimeout(repair, 1200); } 2234 }; 2235 // start on click (a page full of chats should not run many games at once) 2236 card.querySelector(".lp-start").addEventListener("click", start); 2237 card.querySelector("[data-full]").addEventListener("click", () => { if (!card.querySelector("iframe")) start(); const st = card.querySelector(".lp-stage"); (st.requestFullscreen || st.webkitRequestFullscreen || (() => {})).call(st); }); 2238 card.querySelector("[data-code]").addEventListener("click", () => pre.classList.toggle("lp-folded")); 2239 // the answer that was just written starts playing by itself; older chats wait for a click 2240 setTimeout(() => { const msgs = els.messages.querySelectorAll(".msg"); if (card.isConnected && msgs.length && msgs[msgs.length - 1].contains(card) && !card.querySelector("iframe")) start(); }, 60); 2241 } 2242 // errors reported by a playing game â its card offers (or starts) a repair 2243 window.addEventListener("message", (e) => { 2244 if (!e.data || typeof e.data.hythmPlayErr !== "string") return; 2245 const card = [...document.querySelectorAll(".live-play")].find((c) => c._frame && c._frame.contentWindow === e.source); 2246 if (card && card._fix) card._fix(e.data.hythmPlayErr); 2247 }); 2248 let runCtx = null; 2249 function runCode(lang, code) { 2250 const d = document.getElementById("runDialog"); if (!d) return; 2251 const T = (x) => (window.HythmI18n ? HythmI18n.t(x) : x); 2252 runCtx = { lang, code }; 2253 const f = d.querySelector("iframe"); 2254 f.setAttribute("sandbox", "allow-scripts allow-modals"); // null origin: no cookies, no site access 2255 f.srcdoc = runnerDoc(lang, code); 2256 let bar = d.querySelector(".run-bar"); 2257 if (!bar) { 2258 bar = document.createElement("div"); bar.className = "run-bar"; 2259 bar.innerHTML = '<button type="button" class="btn-soft" data-rerun>â» ' + esc(T("شغÙ٠٠رة ثاÙÙØ©")) + '</button><button type="button" class="btn-soft" data-fix hidden>ð ' + esc(T("صÙÙØ Ø§ÙØ®Ø·Ø£")) + '</button><button type="button" class="btn-soft" data-dl>⬠' + esc(T("ÙØ²Ù٠اÙÙ ÙÙ")) + '</button><span class="run-status"></span>'; 2260 d.appendChild(bar); 2261 bar.querySelector("[data-rerun]").addEventListener("click", () => runCtx && runCode(runCtx.lang, runCtx.code)); 2262 bar.querySelector("[data-dl]").addEventListener("click", () => runCtx && downloadCode(runCtx.lang, runCtx.code)); 2263 bar.querySelector("[data-fix]").addEventListener("click", () => { 2264 if (!runCtx || !runCtx.error) return; 2265 d.close(); 2266 const FN = String.fromCharCode(96, 96, 96); 2267 send("صÙÙØ ÙØ§Ùخطأ باÙÙÙØ¯ ÙØ§Ø±Ø¬Ø¹Ù٠اÙÙÙØ¯ ÙØ§Ù ٠بعد Ø§ÙØªØµÙÙØ ٠ع Ø´Ø±Ø ÙØµÙر ÙÙØ³Ø¨Ø¨:\n" + FN + "\n" + runCtx.error + "\n" + FN + "\n\n" + FN + (runCtx.lang || "") + "\n" + runCtx.code.slice(0, 12000) + "\n" + FN); 2268 }); 2269 } 2270 bar.querySelector("[data-fix]").hidden = true; 2271 bar.querySelector(".run-status").textContent = /^(py|python|python3)$/i.test(lang) ? T("Ø£Ù٠٠رة Python Ø¨ÙØ§Ø®Ø¯ Ø«ÙØ§ÙÙ ÙÙØªØÙ ÙÙâ¦") : ""; 2272 d.showModal(); 2273 } 2274 window.addEventListener("message", (e) => { 2275 const m = e.data; if (!m || !m.hythmRun || !runCtx) return; 2276 const d = document.getElementById("runDialog"); const bar = d && d.querySelector(".run-bar"); if (!bar) return; 2277 if (m.hythmRun === "error") { runCtx.error = m.message; bar.querySelector("[data-fix]").hidden = false; bar.querySelector(".run-status").textContent = ""; } 2278 if (m.hythmRun === "done") bar.querySelector(".run-status").textContent = ""; 2279 }); 2280 const EXT = { python: "py", py: "py", javascript: "js", js: "js", typescript: "ts", ts: "ts", html: "html", css: "css", json: "json", csharp: "cs", cs: "cs", cpp: "cpp", c: "c", java: "java", kotlin: "kt", swift: "swift", go: "go", rust: "rs", php: "php", ruby: "rb", sql: "sql", bash: "sh", sh: "sh", lua: "lua", luau: "luau", dart: "dart", yaml: "yml", xml: "xml", svg: "svg", mermaid: "mmd", jsx: "jsx", tsx: "tsx", vue: "vue" }; 2281 function downloadCode(lang, code) { 2282 const ext = EXT[String(lang || "").toLowerCase()] || "txt"; 2283 const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([code], { type: "text/plain;charset=utf-8" })); 2284 a.download = "hythm-code." + ext; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 3000); 2285 } 2286 // ---- diagrams: mermaid blocks (UML, flowcharts, sequence, ER, gantt, pie, charts) are drawn live ---- 2287 let mermaidLoading = null; 2288 function renderMermaid(pre, code) { 2289 mermaidLoading = mermaidLoading || import("https://cdn.jsdelivr.net/npm/[email protected]/dist/mermaid.esm.min.mjs").then((m) => { 2290 const mm = m.default; const dark = document.documentElement.dataset.theme !== "light" && !matchMedia("(prefers-color-scheme: light)").matches; 2291 mm.initialize({ startOnLoad: false, theme: dark ? "dark" : "default", securityLevel: "strict", fontFamily: "inherit" }); return mm; 2292 }); 2293 mermaidLoading.then(async (mm) => { 2294 const id = "mmd" + Math.random().toString(36).slice(2); 2295 const { svg } = await mm.render(id, code); 2296 const box = document.createElement("div"); box.className = "mermaid-box"; box.innerHTML = svg; 2297 pre.after(box); pre.classList.add("mermaid-src"); 2298 }).catch(() => {}); 2299 } 2300 if (window.HythmFiles) HythmFiles.renderMd = (el, md) => renderMdInto(el, md); 2301 function imageMarkers(md) { return String(md || "").replace(/\[\[image:\s*([^\]]{8,600})\]\]/gi, (_, p) => "\n\n%%IMG%%" + encodeURIComponent(p.trim()) + "%%\n\n"); } 2302 function wireImageMarkers(el) { 2303 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 2304 el.querySelectorAll("p, li").forEach((n) => { 2305 const m = /%%IMG%%([^%]+)%%/.exec(n.textContent || ""); if (!m) return; 2306 const prompt = decodeURIComponent(m[1]); 2307 const b = document.createElement("button"); b.type = "button"; b.className = "img-marker"; b.innerHTML = "ð¨ <b>" + esc(T("Ø§Ø±Ø³Ù ÙØ§")) + "</b><small>" + esc(prompt.slice(0, 140)) + "</small>"; 2308 b.addEventListener("click", () => { b.disabled = true; generateImage(prompt + (/thumbnail/i.test(prompt) ? " (YouTube thumbnail)" : "")); }); 2309 n.replaceWith(b); 2310 }); 2311 } 2312 function renderMdInto(el, md) { 2313 if (/\[\[image:/i.test(md || "")) { renderMdIntoRaw(el, imageMarkers(md)); wireImageMarkers(el); return; } 2314 return renderMdIntoRaw(el, md); 2315 } 2316 function renderMdIntoRaw(el, md) { 2317 el.classList.add("md-body"); 2318 el.dataset.md = md; 2319 if (!window.marked) { el.innerHTML = "<p>" + esc(md).replace(/\n/g, "<br>") + "</p>"; loadMd(); return; } 2320 // protect math from markdown 2321 const math = []; 2322 const safe = md.replace(/\$\$[\s\S]+?\$\$|\\\[[\s\S]+?\\\]|\\\([\s\S]+?\\\)|\$[^$\n]+?\$/g, (m) => { math.push(m); return "@@MATH" + (math.length - 1) + "@@"; }); 2323 let html = marked.parse(safe); 2324 html = html.replace(/@@MATH(\d+)@@/g, (_, i) => esc(math[+i])); 2325 el.innerHTML = html; 2326 el.querySelectorAll("pre code").forEach((c) => { 2327 if (window.hljs && !mdStreaming && c.textContent.length < 60000) try { hljs.highlightElement(c); } catch {} 2328 const pre = c.parentElement; 2329 if (pre.querySelector(".copy-code")) return; 2330 const lang = (c.className.match(/language-([\w+#-]+)/) || [])[1]; 2331 const bar = document.createElement("div"); bar.className = "code-bar"; 2332 const runnable = /^(html|xml|svg|javascript|js|py|python|python3)$/i.test(lang || ""); 2333 bar.innerHTML = `<span>${esc(lang || "code")}</span>${runnable ? '<button type="button" class="run-code">â¶ Run</button>' : ""}<button type="button" class="dl-code" title="Download">â¬</button><button type="button" class="copy-code">Copy</button>`; 2334 if (runnable) bar.querySelector(".run-code").addEventListener("click", () => runCode(lang, c.innerText)); 2335 bar.querySelector(".dl-code").addEventListener("click", () => downloadCode(lang, c.innerText)); 2336 if (/^mermaid$/i.test(lang || "") && !mdStreaming) renderMermaid(pre, c.innerText); 2337 // a finished game / 3D scene / canvas page plays right in the chat (the code folds away under it) 2338 if (!mdStreaming && /^(html|htm)$/i.test(lang || "") && /<\/html>\s*$/i.test(c.innerText.trim()) && /(three|<canvas|requestAnimationFrame|webgl|phaser|kaboom|matter\.js|p5\.js|@keyframes|hythm-motion|HM\.film)/i.test(c.innerText)) livePlay(pre, lang, c.innerText); 2339 bar.querySelector(".copy-code").addEventListener("click", async (ev) => { try { await navigator.clipboard.writeText(c.innerText); ev.target.textContent = "â"; setTimeout(() => (ev.target.textContent = "Copy"), 1200); } catch {} }); 2340 pre.prepend(bar); 2341 }); 2342 if (window.renderMathInElement) try { renderMathInElement(el, { delimiters: [{ left: "$$", right: "$$", display: true }
2342, { left: "\\[", right: "\\]", display: true }, { left: "\\(", right: "\\)", display: false }, { left: "$", right: "$", display: false }], throwOnError: false }); } catch {} 2343 } 2344 2345 /* ---------------- NanoSpark Beta: image generation ---------------- */ 2346 // NanoSpark signature: the winged-pup mark in the bottom corner of every picture (trimmed once, soft shadow so it reads on light images too) 2347 window.HYTHM_MODEL_MARK = "nanospark"; 2348 // image loader: a plain grey picture with a soft light gliding over it â no text, no logo inside. 2349 // Above it, one quiet line says what is happening and roughly how long is left. 2350 const trF = (x) => (window.HythmI18n ? HythmI18n.t(x) : x); 2351 const IMG_EST = { fast: 12, hd: 20, pro: 34 }; 2352 function forgeHTML() { 2353 return '<div class="img-dream gray" data-t0="' + Date.now() + '" data-est="' + (IMG_EST[imgChoice.quality] || 20) + '"><i class="gr-shine"></i><i class="gr-grain"></i></div>'; 2354 } 2355 function imgStatusHTML() { return '<div class="img-status"><b class="img-stage">' + esc(trF("Ø¬Ø§Ø±Ù Ø¥ÙØ´Ø§Ø¡ Ø§ÙØµÙرة")) + '</b><span class="dream-msg"></span><span class="img-eta"></span></div>'; } 2356 setInterval(() => { 2357 document.querySelectorAll(".img-dream.gray").forEach((el) => { 2358 const card = el.closest(".img-card"); if (!card) return; 2359 const s = (Date.now() - +el.dataset.t0) / 1000, est = +el.dataset.est || 20, left = Math.
2359ceil(est - s); 2360 const stage = card.querySelector(".img-stage"), eta = card.querySelector(".img-eta"), msg = card.querySelector(".dream-msg"); 2361 const label = trF(s < est * .35 ? "Ø¬Ø§Ø±Ù Ø¥ÙØ´Ø§Ø¡ Ø§ÙØµÙرة" : s < est * .75 ? "Ø¨Ø±Ø³Ù Ø§ÙØªÙاصÙÙ" : "اÙÙ٠سات Ø§ÙØ£Ø®Ùرة"); 2362 if (stage && stage.textContent !== label) stage.textContent = label; 2363 if (eta) eta.textContent = left > 1 ? "~" + left + (trF("Ø«") === "Ø«" ? " Ø«" : "s") : trF("ÙØØ¸Ø§Øªâ¦"); 2364 if (msg) msg.hidden = !/#|\d/.test(msg.textContent) || msg.textContent === trF("Ø¬Ø§Ø±Ù Ø§ÙØ¥Ùشاءâ¦"); // only queue info ("in line #3") 2365 el.style.setProperty("--gp", Math.min(.92, s / est).toFixed(3)); 2366 }); 2367 }, 500); // the pictures are signed with the model mark only (no second cloud mark) 2368 let markP = null; 2369 const nanoMarkImg = () => markP || (markP = new Promise((ok, no) => { 2370 const im = new Image(); im.onload = () => { 2371 const c = document.createElement("canvas"); c.width = im.width; c.height = im.height; const x = c.getContext("2d"); x.drawImage(im, 0, 0); 2372 const d = x.getImageData(0, 0, c.width, c.height).data; let l = c.width, t = c.height, r = 0, b = 0; 2373 for (let y = 0; y < c.height; y++) for (let X = 0; X < c.width; X++) if (d[(y * c.width + X) * 4 + 3] > 20) { if (X < l) l = X; if (X > r) r = X; if (y < t) t = y; if (y > b) b = y; } 2374 if (r <= l) return no(new Error("empty mark")); 2375 const o = document.createElement("canvas"); o.width = r - l + 1; o.height = b - t + 1; o.getContext("2d").drawImage(c, l, t, o.width, o.height, 0, 0, o.width, o.height); ok(o); 2376 }; im.onerror = no; im.src = "/nanospark-mark.png"; 2377 })); 2378 async function nanoMark(blob) { 2379 const [mark, bmp] = await Promise.all([nanoMarkImg(), createImageBitmap(blob)]); 2380 const c = document.createElement("canvas"); c.width = bmp.width; c.height = bmp.height; const x = c.getContext("2d"); 2381 x.drawImage(bmp, 0, 0); 2382 const w = Math.round(Math.min(c.width, c.height) * .13), h = Math.round(w * mark.height / mark.width), pad = Math.round(Math.min(c.width, c.height) * .025); 2383 x.save(); x.globalAlpha = .88; x.shadowColor = "rgba(0,0,0,.55)"; x.shadowBlur = Math.max(4, w * .08); x.shadowOffsetY = Math.max(1, w * .015); 2384 x.drawImage(mark, c.width - w - pad, c.height - h - pad, w, h); x.restore(); 2385 return await new Promise((ok) => c.toBlob((b) => ok(b || blob), "image/jpeg", .93)); 2386 } 2387 // ð² surprise: a random creative idea, with a fitting style 2388 const IDEAS = [ 2389 ["ÙØ·Ø© ÙØ§Ø¨Ø³Ø© ÙØ¸Ø§Ø±Ø© Ø¨ØªÙØ±Ø£ ÙØªØ§Ø¨ Ø¨Ù ÙØªØ¨Ø© ÙØ¯ÙÙ Ø©Ø Ø¶ÙØ¡ Ø´Ù ÙØ¹ داÙÙ", "art"], ["٠دÙÙØ© عائ٠ة Ø¨Ø§ÙØºÙÙÙ ÙÙØª Ø§ÙØºØ±ÙØ¨Ø Ø¬Ø³ÙØ± Ù Ù Ø§ÙØ¶ÙØ¡", "cinematic"], 2390 ["ØÙت Ø¶Ø®Ù Ø¨ÙØ·Ùر ÙÙÙ ØµØØ±Ø§Ø¡ ÙØ§Ø¯Ù ر٠باÙÙÙÙ ØªØØª درب Ø§ÙØªØ¨Ø§ÙØ©", "cinematic"], ["Ø±ÙØ¨Ùت ØµØºÙØ± Ø¨ÙØ³ÙÙ ÙØ±Ø¯Ø© Ø¨ØØ¯ÙÙØ© عÙÙ Ø³Ø·Ø Ø§ÙÙ Ø±ÙØ®", "3d"], 2391 ["سÙÙ Ø´Ø¹Ø¨Ù ÙØ¯ÙÙ Ø¨Ø¹Ù ÙØ§Ù ÙÙØª اÙÙ Ø·Ø±Ø Ø£Ø¶ÙØ§Ø¡ داÙÙØ© ÙØ§ÙØ¹ÙØ§Ø³Ø§Øª", "photo"], ["تÙÙÙ Ù Ù Ø§ÙØ²Ø¬Ø§Ø¬ اÙÙ ÙÙÙÙ ÙÙÙ ÙÙØ¹Ø©", "art"], 2392 ["ÙÙØ¬Ø§Ù ÙÙÙØ© Ø¬ÙØ§ØªÙ Ø¹Ø§ØµÙØ© Ø¨ØØ±ÙØ© ØµØºÙØ±Ø©", "photo"], ["بÙÙ Ø© ØÙÙÙ Ø© ÙØ§Ø¨Ø³Ø© عباءة Ø³Ø§ØØ± بغابة Ø³ØØ±ÙØ©", "anime"], 2393 ["Ø³ÙØ§Ø±Ø© ÙÙØ§Ø³ÙÙÙØ© بشارع ÙÙÙ٠٠٠طر", "neon"], ["شعار ÙÙ ÙÙ٠اس٠٠غÙÙ Ø©Ø ÙÙØ¨ ÙÙÙØ© ÙØºÙÙ Ø©", "logo"], 2394 ["Ø¨ÙØª شجرة ضخ٠بغابة Ø¶Ø¨Ø§Ø¨ÙØ© ÙÙÙ Ø£Ø¶ÙØ§Ø¡ ØµØºÙØ±Ø©", "3d"], ["رائد ÙØ¶Ø§Ø¡ Ø¨ÙØ²Ø±Ø¹ شجرة Ø²ÙØªÙ٠عÙ٠اÙÙ٠ر", "cinematic"] 2395 ]; 2396 const sb = document.getElementById("imgSurprise"); 2397 if (sb) sb.addEventListener("click", () => { 2398 const [p, st] = IDEAS[Math.floor(Math.random() * IDEAS.length)]; 2399 els.input.value = "ارس٠" + p; autoGrow(); els.input.focus(); els.input.dispatchEvent(new Event("input")); 2400 const btn = document.querySelector('[data-img-set="style"] button[data-v="' + st + '"]'); if (btn) btn.click(); 2401 }); 2402 document.querySelectorAll('[data-img-set="style"] button').forEach((b) => { 2403 const im = new Image(); im.className = "sty-prev"; im.alt = ""; im.loading = "lazy"; 2404 im.onload = () => b.classList.add("has-prev"); im.onerror = () => im.remove(); 2405 im.src = "/styles/" + b.dataset.v + ".jpg"; b.prepend(im); 2406 }); 2407 const imgChoice = { style: "none", size: "auto", quality: storage.get("hythm.imgq") || "hd", pair: storage.get("hythm.imgpair") || "" }; 2408 document.querySelectorAll('[data-img-set="quality"] button').forEach((x) => x.classList.toggle("on", x.dataset.v === imgChoice.quality)); 2409 document.querySelectorAll('[data-img-set="pair"] button').forEach((x) => x.classList.toggle("on", x.dataset.v === imgChoice.pair)); 2410 // image options v3: the style strip on top, and one tidy row of dropdowns (size · quality · two pictures) + surprise me 2411 (function imgBar() { 2412 const box = document.getElementById("imgOpts"); if (!box || box.dataset.bar) return; box.dataset.bar = 1; 2413 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 2414 const bar = document.createElement("div"); bar.className = "img-bar"; 2415 const DEF = [["size", "اÙÙÙØ§Ø³", "â¬"], ["quality", "Ø§ÙØ¬Ùدة", "â¦"], ["pair", "ØµÙØ±ØªÙÙ", "â"]];
2416 DEF.forEach(([set, label, ico]) => { 2417 const row = box.querySelector('[data-img-set="' + set + '"]'); if (!row) return; 2418 const dd = document.createElement("div"); dd.className = "img-dd"; dd.dataset.dd = set; 2419 dd.innerHTML = '<button type="button" class="img-dd-btn" aria-haspopup="listbox" aria-expanded="false"><span class="img-dd-ico">' + ico + '</span><span class="img-dd-lbl">' + esc(T(label)) + '</span><b class="img-dd-val"></b><svg viewBox="0 0 24 24" class="img-dd-car"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><div class="img-dd-menu" role="listbox" hidden></div>'; 2420 dd.querySelector(".img-dd-menu").appendChild(row); 2421 const btn = dd.querySelector(".img-dd-btn"), menu = dd.querySelector(".img-dd-menu"); 2422 const sync = () => { const on = row.querySelector("button.on") || row.querySelector("button"); dd.querySelector(".img-dd-val").innerHTML = on ? on.innerHTML : ""; }; 2423 btn.addEventListener("click", (e) => { e.stopPropagation(); const open = menu.hidden; box.querySelectorAll(".img-dd-menu").forEach((m) => { m.hidden = true; m.previousElementSibling.setAttribute("aria-expanded", "false"); }); menu.hidden = !open; btn.setAttribute("aria-expanded", String(open)); }); 2424 row.addEventListener("click", (e) => { if (e.target.closest("button")) setTimeout(() => { sync(); menu.hidden = true; btn.setAttribute("aria-expanded", "false"); }, 0); }); 2425 new MutationObserver(sync).observe(row, { subtree: true, attributes: true, attributeFilter: ["class"] }); 2426 sync(); bar.appendChild(dd); 2427 }); 2428 const sp = document.createElement("span"); sp.className = "img-bar-sp"; bar.appendChild(sp); 2429 const sur = document.getElementById("imgSurprise"); if (sur) bar.appendChild(sur); 2430 box.appendChild(bar); 2431 const st = box.querySelector('[data-img-set="style"]'); if (st) st.classList.add("img-style-strip"); 2432 document.addEventListener("click", (e) => { if (!e.target.closest(".img-dd")) box.querySelectorAll(".img-dd-menu").forEach((m) => { m.hidden = true; m.previousElementSibling.setAttribute("aria-expanded", "false"); }); }); 2433 })(); 2434 document.querySelectorAll("[data-img-set]").forEach((row) => row.querySelectorAll("button").forEach((b) => b.addEventListener("click", () => { 2435 imgChoice[row.dataset.imgSet] = b.dataset.v; 2436 if (row.dataset.imgSet === "quality") storage.set("hythm.imgq", b.dataset.v); 2437 if (row.dataset.imgSet === "pair") storage.set("hythm.imgpair", b.dataset.v); 2438 row.querySelectorAll("button").forEach((x) => x.classList.toggle("on", x === b)); 2439 }))); 2440 /* ---------- one ad â every platform size ---------- */ 2441 const RAW_ADS = new Map(); // saved url â { blob (picture before the words), plan } 2442 function keepRaw(url, blob, plan) { 2443 RAW_ADS.set(url, { blob, plan }); 2444 if (RAW_ADS.size > 20) RAW_ADS.delete(RAW_ADS.keys().next().value); 2445 try { // also kept on this device, so the sizes still work after a refresh 2446 const rq = indexedDB.open("hythm-ads", 1); 2447 rq.onupgradeneeded = () => rq.result.createObjectStore("raw"); 2448 rq.onsuccess = () => { try { rq.result.transaction("raw", "readwrite").objectStore("raw").put({ blob, plan, t: Date.now() }, url); } catch {} }; 2449 } catch {} 2450 } 2451 function rawOf(url) { 2452 if (RAW_ADS.has(url)) return Promise.resolve(RAW_ADS.get(url)); 2453 return new Promise((res) => { 2454 try { 2455 const rq = indexedDB.open("hythm-ads", 1); 2456 rq.onupgradeneeded = () => rq.result.createObjectStore("raw"); 2457 rq.onsuccess = () => { try { const g = rq.result.transaction("raw").objectStore("raw").get(url); g.onsuccess = () => res(g.result || null); g.onerror = () => res(null); } catch { res(null); } }; 2458 rq.onerror = () => res(null); 2459 } catch { res(null); } 2460 }); 2461 } 2462 let jszip = null; 2463 const loadZip = () => jszip || (jszip = new Promise((ok, no) => { if (window.JSZip) return ok(window.JSZip); const sc = document.createElement("script"); sc.src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js"; sc.onload = () => ok(window.JSZip); sc.onerror = () => { jszip = null; no(new Error("zip")); }; document.head.appendChild(sc); })); 2464 async function openAdSizes(e) { 2465 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 2466 const HC = window.HythmCanvas; if (!HC || !HC.adFormats) return; 2467 const d = document.createElement("dialog"); d.className = "ux-sheet ad-sizes no-i18n"; 2468 d.innerHTML = '<div class="ux-card"><header><b>ð¦ ' + esc(T("Ø¥Ø¹ÙØ§Ù٠بÙ٠اÙÙ ÙØ§Ø³Ø§Øª")) + '</b><button type="button" class="ux-x" data-x>â</button></header>' + 2469 "<p>" + esc(T("ÙÙØ³ Ø§ÙØ¥Ø¹ÙØ§Ù Ù Ø±ØªÙØ¨ ÙÙÙ Ù ÙØµØ© â Ø¬Ø§Ùز ÙÙÙØ´Ø±. Ù Ø¬Ø§ÙØ§ÙØ Ø¨Ø¯Ù٠٠ا ÙÙØØ³Ø¨ ٠٠استخدا٠Ù.")) + '</p><div class="as-grid">' + 2470 HC.AD_FORMATS.map((f) => '<figure class="as-item" data-id="' + f.id + '"><div class="as-pv" style="aspect-ratio:' + f.w + "/" + f.h + '"><i class="as-spin"></i></div><figcaption><b>' + esc(f.name) + "</b><small>" + f.w + "Ã" + f.h + '</small><button type="button" class="mini-btn" disabled>â¬</button></figcaption></figure>').join("") + 2471 '</div><footer><button type="button" class="ux-btn" data-zip disabled>⬠' + esc(T("ÙØ²Ù٠اÙÙÙ (ZIP)")) + "</button></footer></div>"; 2472 document.body.appendChild(d); d.showModal(); 2473 d.addEventListener("close", () => setTimeout(() => d.remove(), 300)); 2474 d.addEventListener("click", (ev) => { if (ev.target === d || ev.target.closest("[data-x]")) d.close(); }); 2475 const raw = await rawOf(e.url); 2476 let base = raw && raw.blob, plan = (raw && raw.plan) || e.adPlan; 2477 if (!base) { // the clean picture is gone (other device): extend the finished ad instead, without new words 2478 base = await (await fetch(e.url)).blob(); plan = { ...plan, headline: "", sub: "", price: "", cta: "" }; 2479 } 2480 const done = []; 2481 const name = (f) => "hythm-ad-" + f.id + "-" + f.w + "x" + f.h + ".jpg"; 2482 const save = (blob, fn) => { const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = fn; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 4000); }; 2483 await HC.adFormats(base, plan, (it) => { 2484 done.push(it); 2485 const fig = d.querySelector('.as-item[data-id="' + it.id + '"]'); if (!fig) return; 2486 const url = URL.createObjectURL(it.blob); 2487 fig.querySelector(".as-pv").innerHTML = '<img src="' + url + '" alt="" data-view data-name="' + name(it).replace(".jpg", "") + '">'; 2488 const b = fig.querySelector("button"); b.disabled = false; b.onclick = () => save(it.blob, name(it)); 2489 }); 2490 const z = d.querySelector("[data-zip]"); z.disabled = false;
2491 z.onclick = async () => { 2492 z.disabled = true; z.textContent = T("ع٠جÙÙØ²â¦"); 2493 try { const JSZip = await loadZip(); const zip = new JSZip(); done.forEach((it) => zip.file(name(it), it.blob)); save(await zip.generateAsync({ type: "blob" }), "hythm-ad-all-sizes.zip"); } 2494 catch { done.forEach((it, i) => setTimeout(() => save(it.blob, name(it)), i * 400)); } 2495 z.disabled = false; z.textContent = "⬠" + T("ÙØ²Ù٠اÙÙÙ (ZIP)"); 2496 }; 2497 } 2498 async function openAdVideo(e) { 2499 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 2500 const HC = window.HythmCanvas; if (!HC || !HC.adVideo) return; 2501 const FMTS = [["story", "Story 9:16", 1080, 1920], ["post", "Post 4:5", 1080, 1350], ["square", "Square 1:1", 1080, 1080]]; 2502 const d = document.createElement("dialog"); d.className = "ux-sheet ad-video no-i18n"; 2503 d.innerHTML = '<div class="ux-card"><header><b>ð¬ ' + esc(T("ÙÙØ¯ÙÙ Ø¥Ø¹ÙØ§Ù")) + '</b><button type="button" class="ux-x" data-x>â</button></header>' + 2504 "<p>" + esc(T("6 Ø«ÙØ§ÙÙ Ø¨ØØ±ÙØ© ÙØ§Ù ÙØ±Ø§ ÙØ¥Ø¶Ø§Ø¡Ø© ÙØ¬Ø²ÙØ¦Ø§ØªØ Ø¬Ø§ÙØ² ÙØ³ØªÙØ±Ù Ø¥ÙØ³ØªØºØ±Ø§Ù ÙØªÙ٠تÙÙ. Ù Ø¬Ø§ÙØ§Ù.")) + "</p>" + 2505 '<div class="av-fmts">' + FMTS.map((x, i) => '<button type="button" class="ux-chip' + (i ? "" : " on") + '" data-f="' + i + '">' + x[1] + "</button>").join("") + "</div>" + 2506 '<div class="av-stage"><div class="av-bar"><i></i></div><video playsinline loop muted controls hidden></video></div>' + 2507 '<footer><button type="button" class="ux-btn ghost" data-make>â¶ ' + esc(T("اع٠٠اÙÙÙØ¯ÙÙ")) + '</button><button type="button" class="ux-btn" data-dl disabled>⬠' + esc(T("ØªÙØ²ÙÙ")) + "</button></footer></div>"; 2508 document.body.appendChild(d); d.showModal(); 2509 d.addEventListener("close", () => setTimeout(() => d.remove(), 300)); 2510 d.addEventListener("click", (ev) => { if (ev.target === d || ev.target.closest("[data-x]")) d.close(); }); 2511 let fi = 0, out = null; 2512 d.querySelectorAll("[data-f]").forEach((b) => b.addEventListener("click", () => { fi = +b.dataset.f; d.querySelectorAll("[data-f]").forEach((x) => x.classList.toggle("on", x === b)); })); 2513 const raw = await rawOf(e.url); 2514 let base = raw && raw.blob, plan = (raw && raw.plan) || e.adPlan; 2515 if (!base) { base = await (await fetch(e.url)).blob(); plan = { ...plan, headline: "", sub: "", price: "", cta: "" }; } 2516 const make = d.querySelector("[data-make]"), dl = d.querySelector("[data-dl]"), bar = d.querySelector(".av-bar i"), vid = d.querySelector("video"); 2517 make.addEventListener("click", async () => { 2518 make.disabled = true; dl.disabled = true; vid.hidden = true; d.querySelector(".av-bar").hidden = false; 2519 const [, , w, h] = FMTS[fi]; 2520 try { 2521 out = await HC.adVideo(base, plan, { w, h, seconds: 6, onProgress: (p) => (bar.style.width = Math.round(p * 100) + "%") }); 2522 vid.src = URL.createObjectURL(out.blob); vid.hidden = false; d.querySelector(".av-bar").hidden = true; vid.play().catch(() => {}); 2523 dl.disabled = false; 2524 } catch (err) { toast(T("٠ا ÙØ¯Ø±Øª أع٠٠اÙÙÙØ¯ÙÙ Ø¨ÙØ§ÙÙ ØªØµÙØ") + " â " + String(err.message || err).slice(0, 80)); } 2525 make.disabled = false; make.textContent = "â» " + T("اع٠ÙÙ Ù Ù Ø¬Ø¯ÙØ¯"); 2526 }); 2527 dl.addEventListener("click", () => { if (!out) return; const a = document.createElement("a"); a.href = URL.createObjectURL(out.blob); a.download = "hythm-ad-" + FMTS[fi][0] + "." + out.ext; document.body.appendChild(a); a.click(); a.remove(); }); 2528 } 2529 window.HythmHost2 = { 2530 api: (p, b) => api(p, b), uid: () => uid, me: () => me, toast: (t) => toast(t), send: (t, o) => send(t, o || {}), 2531 setModel: (m) => setModel(m), newChat: () => $("newChat").click(), refreshMe: () => refreshMe(), 2532 setMode: (m) => (typeof setMode === "function" ? setMode(m) : null), attach: () => $("attachInput").click(), 2533 }; 2534 function imageCard(e) { 2535 const card = document.createElement("div"); 2536 card.className = "img-card"; 2537 const ratio = { square: "1/1", portrait: "2/3", landscape: "3/2", wide: "16/9", youtube: "16/9", tiktok: "9/16", story: "9/16", banner: "3/1", post: "4/5", cinema: "12/5" }[e.size] || (e.w && e.h ? e.w + "/" + e.h : "1/1"); 2538 card.innerHTML = ` 2539 <div class="ai-head"><span class="ai-avatar h-mark"><svg class="cloud-logo"><use href="#logo-cloud"/></svg></span>NanoSpark <span class="mc-beta">Beta</span></div> 2540 ${e.url ? "" : imgStatusHTML()} 2541 <div class="img-frame-gen" style="aspect-ratio:${ratio}${["tiktok", "story", "portrait"].includes(e.size) || (e.h > e.w * 1.2) ? ";width:min(100%, 320px)" : ""}">${e.url ? `<img src="${esc(e.url)}" alt="${esc(e.prompt)}" loading="lazy" data-view data-name="micro-cloud-${esc(e.seed)}" data-title="${esc(e.prompt)}">` : forgeHTML()}</div>
2542 ${e.url ? `<div class="img-actions"> 2543 <a class="mini-btn" href="${esc(e.url)}" download="micro-cloud-${e.seed}.jpg"><svg><use href="#i-download"/></svg>ØªÙØ²ÙÙ</a> 2544 <a class="mini-btn" href="${esc(e.url)}" target="_blank" rel="noopener" data-view data-name="micro-cloud-${esc(e.seed)}" data-title="${esc(e.prompt)}"><svg><use href="#i-external"/></svg>ÙØªØ</a> 2545 <button type="button" class="mini-btn" data-variation><svg><use href="#i-sparkles"/></svg>تÙÙÙØ¹Ø© Ø¬Ø¯ÙØ¯Ø©</button> 2546 <button type="button" class="mini-btn" data-edit>â Ø¹Ø¯Ù٠باÙÙÙØ§Ù </button> 2547 <button type="button" class="mini-btn" data-share>ð Ù Ø´Ø§Ø±ÙØ©</button> 2548 ${e.adPlan ? '<button type="button" class="mini-btn mini-hot" data-sizes>ð¦ Ù٠اÙÙ ÙØ§Ø³Ø§Øª</button><button type="button" class="mini-btn mini-hot" data-video>ð¬ ÙÙØ¯ÙÙ Ø¥Ø¹ÙØ§Ù</button>' : ""} 2549 <button type="button" class="mini-btn img-vote" data-vote="up" title="عجبتÙÙ" aria-label="عجبتÙÙ">ð</button> 2550 <button type="button" class="mini-btn img-vote" data-vote="down" title="٠ا عجبتÙÙ" aria-label="٠ا عجبتÙÙ">ð</button> 2551 <span class="img-meta">${esc(e.style || "none")} · ${esc(e.size || "square")} · seed ${e.seed}${e.ms ? " · " + Math.round(e.ms / 1000) + "s" : ""}</span> 2552 </div>` : ""}`; 2553 card.querySelectorAll("[data-vote]").forEach((b) => b.addEventListener("click", async () => { 2554 card.querySelectorAll("[data-vote]").forEach((x) => { x.disabled = true; x.classList.toggle("on", x === b); }); 2555 const r = await api("/api/image-feedback", { uid, url: e.url, vote: b.dataset.vote }); 2556 toast((window.HythmI18n ? HythmI18n.t : (x) => x)(r.ok ? "NanoSpark تعÙÙ٠٠٠رأÙÙ" : "٠ا ÙØ¯Ø±Øª أسجÙ٠رأÙÙ")); 2557 })); 2558 const sz = card.querySelector("[data-sizes]"); 2559 if (sz) sz.addEventListener("click", () => openAdSizes(e)); 2560 const T0 = window.HythmI18n ? HythmI18n.t : (x) => x; 2561 const ed = card.querySelector("[data-edit]"); 2562 if (ed) ed.addEventListener("click", () => { 2563 let box = card.querySelector(".img-edit"); 2564 if (box) { box.querySelector("input").focus(); return; } 2565 box = document.createElement("form"); box.className = "img-edit"; 2566 box.innerHTML = '<input type="text" maxlength="300" placeholder="' + esc(T0("ش٠بد٠تغÙÙØ±Ø Ù Ø«ÙØ§Ù: Ø®ÙÙÙ Ø§ÙØ³Ù ا ØºØ±ÙØ¨Ø Ø£Ù Ø²ÙØ¯ ÙØ·Ø© ثاÙÙØ©")) + '"><button type="submit" class="mini-btn">' + esc(T0("عدÙÙ")) + '</button>'; 2567 card.querySelector(".img-actions").after(box); 2568 const inp = box.querySelector("input"); inp.focus(); 2569 box.addEventListener("submit", (ev) => { 2570 ev.preventDefault(); const change = inp.value.trim(); if (!change) return; 2571 box.remove(); 2572 // the same seed + the original idea + only the requested change â the picture stays close to the one you had 2573 generateImage(change, { prev: e.drawn || e.prompt, seed: e.seed, size: e.size }); 2574 }); 2575 }); 2576 const sh = card.querySelector("[data-share]"); 2577 if (sh) sh.addEventListener("click", async () => { 2578 const pub = confirm(T0("بد٠ÙÙ Ø§Ù ØªÙØ´Ø±Ùا ب٠عرض NanoSpark Ø§ÙØ¹Ø§Ù Ø (Ù ÙØ§ÙÙ = ÙØ¹Ù Ø Ø¥ÙØºØ§Ø¡ = رابط بس)")); 2579 const r = await api("/api/share", { uid, url: e.url, prompt: e.prompt, size: e.size, public: pub }); 2580 if (!r.ok) return toast(T0("٠ا ÙØ¯Ø±Øª أع٠٠رابط")); 2581 const link = location.origin + r.data.link;
2582 try { if (navigator.share) await navigator.share({ title: "NanoSpark", text: e.prompt, url: link }); else { await navigator.clipboard.writeText(link); toast(T0("Ø§ÙØ³Ø® Ø§ÙØ±Ø§Ø¨Ø· â")); } } 2583 catch { try { await navigator.clipboard.writeText(link); toast(T0("Ø§ÙØ³Ø® Ø§ÙØ±Ø§Ø¨Ø· â")); } catch { prompt("Link", link); } } 2584 }); 2585 const vd = card.querySelector("[data-video]"); 2586 if (vd) vd.addEventListener("click", () => openAdVideo(e)); 2587 const v = card.querySelector("[data-variation]"); 2588 if (v) v.addEventListener("click", () => { 2589 imgChoice.style = e.style || "none"; imgChoice.size = e.size || "square"; 2590 if (e.quality && e.quality !== "fast") imgChoice.quality = e.quality; // never downgrade a variation to the small engine 2591 if (imgChoice.quality === "fast") imgChoice.quality = "hd"; 2592 document.querySelectorAll('[data-img-set="quality"] button').forEach((x) => x.classList.toggle("on", x.dataset.v === imgChoice.quality)); 2593 const src = e.refKey && REF_STORE.get(e.refKey); 2594 if (e.refKey && !src) { toast((window.HythmI18n ? HythmI18n.t : (x) => x)("أرÙÙ ØµÙØ±Ø© اÙÙ ÙØªØ¬ ٠رة ثاÙÙØ© ÙØ£Ø¹Ù ٠تÙÙÙØ¹Ø© عÙÙ ÙÙØ³ Ø§ÙØ¥Ø¹ÙاÙ")); $("attachBtn").click(); return; } 2595 generateImage(e.prompt, src ? { refs: src.refs, thumbs: src.thumbs } : {}); 2596 }); 2597 return card; 2598 } 2599 function notifyReady() { 2600 if (!document.hidden) return; 2601 const old = document.title; let n = 0; 2602 const t = setInterval(() => { document.title = n++ % 2 ? old : "â " + (window.HythmI18n ? HythmI18n.t : (x) => x)("ØµÙØ±ØªÙ Ø¬Ø§ÙØ²Ø©"); if (!document.hidden && n > 1) { clearInterval(t); document.title = old; } }, 1000); 2603 } 2604 // ---- images keep being made on the server; after a refresh the page picks them up again ---- 2605 let lastTicket = null, lastDrawn = ""; 2606 const PKEY = "hythm.pendingImgs"; 2607 const pendingAll = () => { try { return JSON.parse(localStorage.getItem(PKEY) || "[]").filter((p) => Date.now() - p.at < 12 * 60_000); } catch { return []; } }; 2608 const pendingSave = (l) => { try { localStorage.setItem(PKEY, JSON.stringify(l.slice(-6))); } catch {} }; 2609 const pendingAdd = (p) => pendingSave([...pendingAll().filter((x) => x.ticket !== p.ticket), p]); 2610 const pendingDel = (t) => pendingSave(pendingAll().filter((x) => x.ticket !== t)); 2611 const resuming = new Set(); 2612 async function resumePending() { 2613 if (!uid) return; 2614 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 2615 for (const p of pendingAll()) { 2616 if (resuming.has(p.ticket)) continue; 2617 resuming.add(p.ticket); 2618 (async () => { 2619 let holder = null; 2620 const showHolder = () => { 2621 if (holder || projectId !== p.pid) return; 2622 holder = document.createElement("div"); holder.className = "msg msg-ai"; holder.dataset.t = Date.now(); 2623 holder.appendChild(imageCard({ prompt: p.text, size: p.size })); 2624 els.messages.appendChild(holder); els.app.classList.remove("is-empty"); scrollDown(); 2625 const m = holder.querySelector(".dream-msg"); if (m) m.textContent = T("بÙÙ ÙÙ ØµÙØ±ØªÙâ¦"); 2626 }; 2627 showHolder(); 2628 let res = null; 2629 for (let i = 0; i < 40; i++) { // ~15 min max 2630 const r = await api("/api/image-result?uid=" + encodeURIComponent(uid) + "&ticket=" + encodeURIComponent(p.ticket)); 2631 if (!r.ok || r.data.none || r.data.used || r.data.error) { res = null; break; } 2632 if (r.data.data) { res = r.data; break; } 2633 showHolder(); 2634 const m = holder && holder.querySelector(".dream-msg"); 2635 if (m && r.data.eta) m.textContent = T("بÙÙ ÙÙ ØµÙØ±ØªÙâ¦") + " · " + T("ØÙاÙÙ") + " " + Math.max(3, Math.round(r.data.eta)) + " " + T("ثاÙÙØ©"); 2636 } 2637 if (holder) holder.remove(); 2638 pendingDel(p.ticket); resuming.delete(p.ticket); 2639 if (!res) return; 2640 const HC = window.HythmCanvas; 2641 let blob = dataURLtoBlob(String(res.data).startsWith("data:") ? res.data : "data:image/jpeg;base64," + res.data); 2642 const ov = HC ? HC.splitOverlay(p.text) : null; 2643 const rawBlob = blob; if (HC) blob = await ((res.plan || p.plan) && HC.composeAd ? HC.composeAd(blob, res.plan || p.plan) : HC.compose(blob, { text: ov ? ov.text : "", style: p.style })).catch(() => blob); 2644 const saved = await api("/api/image-save", { uid, ticket: p.ticket, data: await blobToDataURL(blob) }); 2645 if (!saved.ok) return; 2646 setCredits(saved.data.credits, true); 2647 const entry = { type: "image", url: saved.data.url, prompt: p.text, style: p.style, size: p.size, seed: saved.data.seed }; 2648 if (projectId === p.pid) { const fresh = renderEntry(entry); fresh.classList.add("fresh-img"); pushLog(entry); updateLayout(); scrollDown(); } 2649 else {
2650 try { const k = "hythm.p." + p.pid; const sess = JSON.parse(localStorage.getItem(k) || "null"); if (sess && sess.log) { sess.log.push(entry); localStorage.setItem(k, JSON.stringify(sess)); } } catch {} 2651 toast(T("ØµÙØ±ØªÙ Ø¬Ø§ÙØ²Ø© باÙÙ ØØ§Ø¯Ø«Ø© اÙÙÙ Ø·ÙØ¨ØªÙا Ù ÙÙØ§")); renderProjects(); 2652 } 2653 notifyReady(); 2654 })(); 2655 } 2656 } 2657 setTimeout(() => { try { resumePending(); } catch {} }, 1800); 2658 const REF_STORE = new Map(); // photos behind ads/edits, kept in memory for "new variation" 2659 // NanoSpark is "torn" when the request leaves the picture wide open (very short, or "surprise me / anything nice") 2660 const IMG_TORN = (t) => { const w = String(t || "").replace(/(ارس٠(ÙÙ)?|اع٠Ù(ÙÙ)?|ØµÙØ±[Ø©Ù]|draw|make|image of|picture of|a |an )/gi, " ").trim().split(/\s+/).filter(Boolean); return w.length <= 2 || /(ÙØ§Ø¬Ø¦ÙÙ|Ø£Ù Ø´Ù|ا٠شÙ|Ø´Ù ØÙÙ|اش٠ØÙÙ|Ø§ÙØª اختار|Ø¥ÙØª اختار|عÙ٠ذÙÙÙ|surprise me|anything|something (nice|cool))/i.test(t); }; 2661 async function generateImage(text, extra = {}) { 2662 if (!me) return openLogin(); 2663 const userEntry = { type: "user", text, mode, model: "omni", style: "", styleLabel: "NanoSpark", thumbs: extra.thumbs || [] }; 2664 if (!extra.silent) { renderEntry(userEntry); pushLog(userEntry); } 2665 // remember WHICH chat asked: if the user switches chats while waiting, the result goes back to that chat only 2666 const origin = { pid: projectId, sess: session }; 2667 const here = () => session === origin.sess; 2668 const deliver = (e) => { 2669 if (here()) return false; 2670 origin.sess.log.push(e); origin.sess.log = origin.sess.log.slice(-60); 2671 try { localStorage.setItem("hythm.p." + origin.pid, JSON.stringify(origin.sess)); } catch {} 2672 return true; 2673 }; 2674 const holder = document.createElement("div"); 2675 holder.className = "msg msg-ai"; 2676 holder.dataset.t = Date.now(); 2677 holder.appendChild(imageCard({ prompt: text, size: imgChoice.size })); 2678 els.messages.appendChild(holder); els.app.classList.remove("is-empty"); scrollDown(); 2679 const { ok, status, data } = cfg.imageProvider === "zerogpu" 2680 ? await generateOnZeroGPU(text, holder, extra.refs || [], extra) 2681 : await api("/api/image-gen", { uid, prompt: text, style: imgChoice.style, size: imgChoice.size }); 2682 holder.remove(); 2683 if (lastTicket) { pendingDel(lastTicket); lastTicket = null; } 2684 if (!ok && extra.silent) { if (data && data.credits != null) setCredits(data.credits, true); return null; } 2685 if (!ok) { 2686 if (data.credits != null) setCredits(data.credits, true); 2687 if (!here()) return; // the error belongs to another chat: don't show it here 2688 if (status === 422 && data.blocked) { const w = { type: "warning", title: data.title, reason: data.reason, category: data.category }; renderEntry(w); pushLog(w); enterWarning(); } 2689 else renderEntry({ type: "notice", kind: status === 402 ? "credits" : "error", title: status === 402 ? "Ø®ÙØµ استخدا٠٠ÙÙØ§ÙÙ5 ساعات" : "٠ا ÙØ¯Ø±Øª Ø£ÙÙÙØ¯ Ø§ÙØµÙرة", text: " " + (data.reason || ""), 2690 actions: status === 402 ? "" : '<button type="button" class="btn-soft" data-retry-image="' + esc(text) + '">' + esc((window.HythmI18n ? HythmI18n.t : (x) => x)("Ø¬Ø±ÙØ¨ ٠رة ثاÙÙØ©")) + "</button>" }); 2691 if (data.credits != null) setCredits(data.credits, true); 2692 scrollDown(); return; 2693 } 2694 setCredits(data.credits, true); 2695 const refKey = (extra.refs || []).length ? "r" + Date.now().toString(36) : ""; 2696 if (refKey) { REF_STORE.set(refKey, { refs: extra.refs, thumbs: extra.thumbs || [] }); if (REF_STORE.size > 12) REF_STORE.delete(REF_STORE.keys().next().value); } 2697 const entry = { type: "image", url: data.url, adPlan: data.adPlan || null, prompt: text, drawn: lastDrawn, style: imgChoice.style, size: data.size || imgChoice.size, w: data.w, h: data.h, quality: imgChoice.quality, refKey, seed: data.seed, ms: data.ms, backend: data.backend }; 2698 if (extra.silent) return entry; 2699 // pair mode (the "â ØµÙرتÙÙ" toggle, never for ads or photo edits): draw a second one and let the user pick 2700 if ((imgChoice.pair === "1" || (imgChoice.pair !== "0" && IMG_TORN(text))) && !extra.prev && !data.adPlan && !(extra.refs || []).length && here()) { 2701 const b = await generateImage(text, { ...extra, silent: true }); 2702 if (b) { 2703 const pair = { type: "imgpair", a: entry, b }; 2704 if (deliver(pair)) { renderProjects(); notifyReady(); return; } 2705 renderEntry(pair); pushLog(pair); updateLayout(); scrollDown(); notifyReady(); return; 2706 } 2707 } 2708 if (deliver(entry)) { toast((window.HythmI18n ? HythmI18n.t : (x) => x)("ØµÙØ±ØªÙ Ø¬Ø§ÙØ²Ø© باÙÙ ØØ§Ø¯Ø«Ø© اÙÙÙ Ø·ÙØ¨ØªÙا Ù ÙÙØ§")); renderProjects(); notifyReady(); return; } 2709 const fresh = renderEntry(entry); fresh.classList.add("fresh-img"); pushLog(entry); updateLayout(); scrollDown(); notifyReady(); 2710 } 2711 /* ---- ZeroGPU path: server issues a paid ticket â the VISITOR'S browser runs our Space (their own free quota) â server stores the image ---- */ 2712 let gradioClientMod = null; 2713 // The Gradio client sends cookies and a Content-Type on GETs; HF's CORS preflight doesn't allow credentials, 2714 // so calls to *.hf.space go out cookie-less (the engine is public and never needs them). 2715 if (!window.__hfFetchPatched) { 2716 window.__hfFetchPatched = true; 2717 const nativeFetch = window.fetch.bind(window); 2718 window.fetch = (input, init) => { 2719 const url = String(input && input.url ? input.url : input); 2720 if (/^https:\/\/[^/]+\.hf\.space\//.test(url)) init = { ...(init || {}), credentials: "omit" }; 2721 return nativeFetch(input, init); 2722 }; 2723 } 2724 const loadGradioClient = () => (gradioClientMod ||= import("https://cdn.jsdelivr.net/npm/@gradio/[email protected]/+esm")); 2725 // data: URLs can't be fetched under our CSP â decode them directly 2726 const dataURLtoBlob = (u) => { const [h, b64] = String(u).split(","); const bin = atob(b64); const a = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) a[i] = bin.charCodeAt(i); return new Blob([a], { type: (h.match(/data:([^;
2726]+)/) || [])[1] || "image/jpeg" }); }; 2727 const blobToDataURL = (blob) => new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(r.result); r.onerror = rej; r.readAsDataURL(blob); }); 2728 async function generateOnZeroGPU(text, holder, refs = [], extra = {}) { 2729 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 2730 const HC = window.HythmCanvas; 2731 const ov = HC ? HC.splitOverlay(text) : null; // Arabic words are typeset by us, not drawn by the model 2732 if (ov) HC.loadFonts(); 2733 // "make one like it" / "same butâ¦" â send the previous picture's description too 2734 // also: "regenerate the picture butâ¦", "make itâ¦", "the image, butâ¦" â a follow-up on the last picture, never a brand-new subject 2735 const FOLLOW = /(Ù Ø«Ù(ÙØ§|Ù|Ù)|ÙÙØ³(ÙØ§|Ù|Ù| Ø§ÙØµÙر[Ø©Ù])|زÙ(ÙØ§|Ù|Ù)|Ù٠ا٠(ÙØØ¯Ø©|ØµÙØ±Ø©|ÙØ§ØØ¯Ø©)|ÙØØ¯Ø© (ثاÙÙØ©|تاÙÙØ©)|ÙØ³Ø®Ø© (ثاÙÙØ©|تاÙÙØ©)|(ا|Ø¥)?عاد(Ø©|Ù)? (تÙÙÙØ¯|رس٠|ع٠Ù|Ø§ÙØ´Ø§Ø¡|Ø¥ÙØ´Ø§Ø¡)|(ا|Ø£)?عد(ÙØ§|Ù)|Ø§ÙØµÙر[Ø©Ù] (Ø§ÙØ³Ø§Ø¨Ù[Ø©Ù]|اÙÙÙ ÙÙÙ|Ø§ÙØ§Ø®Ùر[Ø©Ù]|Ø§ÙØ£Ø®Ùر[Ø©Ù])|(ب|ÙÙ)?Ø§ÙØµÙر[Ø©Ù]|Ø®ÙÙ?ÙÙØ§|Ø®ÙÙ?ÙÙ|اجعÙÙØ§|اجعÙÙ|ØØ·Ùا|غÙÙ?ر(ÙØ§|Ù)|عدÙ?Ù(ÙØ§|Ù)|بس (Ø®ÙÙ|اجعÙ|ØØ·|ØºÙØ±|Ø²ÙØ¯)|ÙÙÙ (Ø®ÙÙ|اجعÙ|ÙÙ |ØØ·|ØºÙØ±)|like (it|that|this)|same (one|picture|image)|another one|similar|regenerate|re-?do|again|make (it|her|him)|but (make|with|give)|the (same|previous|last) (image|picture|one))/i; 2736 const lastImg = [...session.log].reverse().find((x) => x.type === "image" && x.prompt && x.prompt !== text); 2737 const prevImg = lastImg && FOLLOW.test(text) && text.length < 220 && !(refs || []).length 2738 ? String(lastImg.prompt || "") : ""; 2739 const wantsAngle = /(Ø«ÙØ§Ø«Ù(Ø©)? Ø§ÙØ£Ø¨Ø¹Ø§Ø¯|Ø«ÙØ§Ø«Ù Ø§ÙØ§Ø¨Ø¹Ø§Ø¯|\b3d\b|زاÙÙ(Ø©|Ù)|Ù Ù ÙÙÙ|Ù Ù ØªØØª|Ù Ù Ø§ÙØ¬Ùب|دÙÙ?ر(Ù|ÙØ§)?|اÙÙØ¨(Ù|ÙØ§)|٠جسÙ?Ù |hero shot|\bangle\b|rotate|isometric)/i.test(text); 2740 const isAd = !wantsAngle && !!(refs && refs.length) && /(Ø§Ø¹ÙØ§Ù|Ø¥Ø¹ÙØ§Ù|Ø¯Ø¹Ø§ÙØ©|Ø¨ÙØ³ØªØ±|Ù ÙØªØ¬|Ù ÙØªØ¬Ù|Ø§Ø¨ÙØ¹|Ø£Ø¨ÙØ¹|Ø¨ÙØ¹|ÙÙØ¨Ùع|سعر|بسعر|Ø´ÙÙÙ|Ø´ÙØ§ÙÙ|دÙÙØ§Ø±|Ø±ÙØ§Ù|درÙÙ |جÙÙÙ|ÙÙØ±Ø©|\$|â¬|خص٠|عرض|تخÙÙØ¶|\b(ad|advert\w*|poster|sale|price|discount)\b)/i.test(text); 2741 const tk = await api("/api/image-ticket", { uid, prompt: isAd ? text : ov ? ov.prompt : text, overlay: isAd ? "" : ov ? ov.text : "", ad: isAd, prev: extra.prev || prevImg, seed: Number.isInteger(extra.seed) ? extra.seed : undefined, intentText: text, style: imgChoice.style, size: extra.size || imgChoice.size, quality: imgChoice.quality, refs }); 2742 if (!tk.ok) return tk; 2743 // the server picked the frame (e.g. "YouTube thumbnail" â 16:9): reshape the waiting card to match 2744 const fr0 = holder && holder.querySelector(".img-frame-gen"); 2745 if (fr0 && tk.data.width && tk.data.height) { fr0.style.aspectRatio = tk.data.width + "/" + tk.data.height; if (tk.data.height > tk.data.width * 1.2) fr0.style.width = "min(100%, 320px)"; } 2746 setCredits(tk.data.credits, true); 2747 pendingAdd({ ticket: tk.data.ticket, pid: projectId, text, style: imgChoice.style, size: tk.data.size || imgChoice.size, plan: tk.data.plan || null, at: Date.now() }); 2748 lastTicket = tk.data.ticket; lastDrawn = tk.data.prompt || ""; 2749 const setDream = (msg) => { const el = holder.querySelector(".dream-msg"); if (el) el.textContent = msg; }; 2750 const t0 = Date.now(); 2751 // Hythm's own PC server (fast, SDXS on CPU). "Fast" quality asks it first; "HD" asks the big Z-Image model first. 2752 async function tryLocal() { 2753 let qTimer = 0; 2754 const pollQueue = async () => { 2755 const q = await api("/api/image-queue?uid=" + encodeURIComponent(uid) + "&ticket=" + encodeURIComponent(tk.data.ticket)); 2756 if (q.ok && q.data && q.data.position != null) { 2757 setDream(q.data.position > 0 2758 ? T("Ø¨Ø§ÙØ¯Ùر") + " #" + q.data.position + (q.data.eta ? " · " + T("ØÙاÙÙ") + " " + Math.max(5, Math.round(q.data.eta)) + " " + T("ثاÙÙØ©") : "") 2759 : T("Ø¬Ø§Ø±Ù Ø§ÙØ¥Ùشاءâ¦") + (q.data.eta ? " · " + T("ØÙاÙÙ") + " " + Math.max(3, Math.round(q.data.eta)) + " " + T("ثاÙÙØ©") : "")); 2760 const bar = holder.querySelector(".dream-bar b"); 2761 if (bar && q.data.position === 0 && !bar.dataset.running) { bar.dataset.running = "1"; bar.style.animationDuration = Math.max(6, (q.data.eta || 20) * 1.1) + "s"; bar.style.animationName = "none"; void bar.offsetWidth; bar.style.animationName = ""; } 2762 } 2763 qTimer = setTimeout(pollQueue, 1500); 2764 };
2765 try { 2766 qTimer = setTimeout(pollQueue, 700); 2767 setDream(T("Ø¬Ø§Ø±Ù Ø§ÙØ¥Ùشاءâ¦")); 2768 const lc = await Shield.deadline(api("/api/image-local", { uid, ticket: tk.data.ticket }), refs.length ? 620000 : 420000, "local timeout").catch(() => ({ ok: false })); 2769 if (lc.ok && lc.data && lc.data.data) { 2770 let blob = dataURLtoBlob(lc.data.data); 2771 const rawBlob = blob; if (HC) blob = await (tk.data.plan && HC.composeAd ? HC.composeAd(blob, tk.data.plan) : HC.compose(blob, { text: ov ? ov.text : "", style: imgChoice.style })).catch(() => blob); 2772 const saved = await api("/api/image-save", { uid, ticket: tk.data.ticket, data: await blobToDataURL(blob) }); 2773 if (saved.ok) { clearTimeout(qTimer); if (tk.data.plan) keepRaw(saved.data.url, rawBlob, tk.data.plan); return { ok: true, status: 200, data: { ...saved.data, adPlan: tk.data.plan || null, size: tk.data.size, w: tk.data.width, h: tk.data.height, ms: Date.now() - t0, backend: "micro-cloud:local" } }; } 2774 } 2775 } catch {} 2776 clearTimeout(qTimer); 2777 return null; 2778 } 2779 if (refs.length) { 2780 const r = await tryLocal(); if (r) return r; 2781 const rf = await api("/api/image-refund", { uid, ticket: tk.data.ticket }); 2782 return { ok: false, status: 503, data: { credits: rf.data && rf.data.credits, reason: T("Ø§ÙØªÙÙÙØ¯ Ù Ù ØµÙØ±Ø© Ø¨ÙØØªØ§Ø¬ خاد٠Hythm (Ø¬ÙØ§Ø² اÙ٠اÙÙ) ÙÙÙÙ Ø´ØºÙØ§Ù. ٠ا Ø§ÙØØ³Ø¨ عÙÙÙØ Ø¬Ø±ÙØ¨ بعد Ø´ÙÙ.") } }; 2783 } 2784 { const r = await tryLocal(); if (r) return r; } // Hythm's PC first: HD = Z-Image on its card, Fast = SDXS on its CPU; no quota 2785 try { 2786 setDream(T("Ø¬Ø§Ø±Ù Ø§ÙØ¥Ùشاءâ¦")); 2787 const { Client } = await Shield.deadline(loadGradioClient(), 20000, "engine load timeout"); 2788 const client = await Shield.deadline(Client.connect(tk.data.space, { events: ["data", "status"] }), 30000, "GPU timeout: engine did not answer"); 2789 const job = client.submit("/generate", { prompt: tk.data.prompt, negative_prompt: "", width: tk.data.width, height: tk.data.height, seed: tk.data.seed, steps: tk.data.steps }); 2790 let result = null; 2791 const killer = setTimeout(() => { try { job.cancel && job.cancel(); } catch {} }, 100000); 2792 const iter = job[Symbol.asyncIterator](); 2793 for (;;) { 2794 const step = await Shield.deadline(iter.next(), 100000, "GPU timeout: no image after 100s"); 2795 if (step.done) break; 2796 const msg = step.value; 2797 if (msg.type === "status") { 2798 if (msg.stage === "error") throw new Error(msg.message || "engine error"); 2799 if (msg.queue && msg.position != null && msg.position > 0) setDream(T("Ø¬Ø§Ø±Ù Ø§ÙØ¥Ùشاءâ¦") + " · " + T("Ø¨Ø§ÙØ¯Ùر") + " #" + (msg.position + 1)); 2800 else if (msg.stage === "pending") setDream(T("Ø¬Ø§Ø±Ù Ø§ÙØ¥Ùشاءâ¦")); 2801 } 2802 if (msg.type === "data") { result = msg.data; break; } 2803 } 2804 clearTimeout(killer); 2805 const file = result && result[0]; 2806 const url = file && (file.url || file.path); 2807 if (!url) throw new Error("no image returned"); 2808 let blob = await (await fetch(url)).blob(); 2809 const rawBlob = blob; if (HC) blob = await (tk.data.plan && HC.composeAd ? HC.composeAd(blob, tk.data.plan) : HC.compose(blob, { text: ov ? ov.text : "", style: imgChoice.style })).catch(() => blob); 2810 blob = await nanoMark(blob).catch(() => blob); 2811 const saved = await api("/api/image-save", { uid, ticket: tk.data.ticket, data: await blobToDataURL(blob) }); 2812 if (!saved.ok) throw new Error("save failed (" + saved.status + ")"); 2813 if (tk.data.plan) keepRaw(saved.data.url, rawBlob, tk.data.plan); 2814 return { ok: true, status: 200, data: { ...saved.data, adPlan: tk.data.plan || null, size: tk.data.size, w: tk.data.width, h: tk.data.height, ms: Date.now() - t0, backend: "micro-cloud:zerogpu" } }; 2815 } catch (e0) { 2816 let e = e0; 2817 if (/quota|exceeded|limit|GPU|timeout/i.test(String(e0 && e0.message))) { 2818 try { 2819 setDream(T("Ø¬Ø§Ø±Ù Ø§ÙØ¥Ùشاءâ¦")); 2820 const sv = await Shield.deadline(api("/api/image-server", { uid, ticket: tk.data.ticket }), 120000, "server timeout").catch((x) => ({ ok: false, data: { reason: x.message } })); 2821 if (sv.ok && sv.data.data) { 2822 let blob = dataURLtoBlob(sv.data.data); 2823 const rawBlob = blob; if (HC) blob = await (tk.data.plan && HC.composeAd ? HC.composeAd(blob, tk.data.plan) : HC.compose(blob, { text: ov ? ov.text : "", style: imgChoice.style })).catch(() => blob); 2824 const saved = await api("/api/image-save", { uid, ticket: tk.data.ticket, data: await blobToDataURL(blob) }); 2825 if (saved.ok) { if (tk.data.plan) keepRaw(saved.data.url, rawBlob, tk.data.plan); return { ok: true, status: 200, data: { ...saved.data, adPlan: tk.data.plan || null, size: tk.data.size, w: tk.data.width, h: tk.data.height, ms: Date.now() - t0, backend: "micro-cloud:server" } }; } 2826 } 2827 e = new Error(sv.data && sv.data.error === "quota" ? "quota exceeded" : (sv.data && sv.data.reason) || String(e0.message)); 2828 } catch (e2) { e = e2; } 2829 } 2830 const rf = await api("/api/image-refund", { uid, ticket: tk.data.ticket }); 2831 const quota = /quota|exceeded|GPU.*(limit|minutes)|ZeroGPU/i.test(String(e && e.message)); 2832 return { ok: false, status: 502, data: { credits: rf.data.credits, reason: quota 2833 ? T("ÙÙ Ù ØØ±Ùات Ø§ÙØµÙر ٠شغÙÙØ© ÙÙÙ (خاد٠Hythm Ù Ø·ÙÙ ÙØ§ÙØØµØµ اÙ٠جاÙÙØ© Ø®ÙØµØª). ٠ا Ø§ÙØØ³Ø¨ عÙÙÙØ Ø¬Ø±ÙØ¨ بعد Ø´ÙÙ.") 2834 : T("اÙÙ ØØ±Ù ٠شغÙÙ Ø£Ù ÙØ§ÙÙ Ø Ø¬Ø±ÙØ¨ Ù٠ا٠٠رة بعد دÙÙÙØ©.") + " (" + String(e && e.message || e).slice(0, 120) + ")" } }; 2835 } 2836 } 2837 // live status of our own engine (NanoSpark Engine on this machine) 2838 let engineTimer = 0; 2839 function pollEngineAuto() { if (!engineTimer) pollEngine(); } 2840 async function pollEngine() { 2841 clearTimeout(engineTimer); 2842 if (task !== "image" && !pollEngine.force) return; 2843 const { data, offline } = await api("/api/image-engine"); 2844 if (offline) data.status = "offline"; 2845 const chip = $("engineChip"), st = data.status || "offline"; 2846 chip.dataset.status = st; 2847 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 2848 $("engineText").textContent = st === "ready" 2849 ? `NanoSpark Engine · ${data.model} · ${data.device === "cuda" ? "GPU " + data.vram_gb + "GB" : data.device}${cfg.imageProvider === "zerogpu" ? " · " + T("ØØµØªÙ اÙ٠جاÙÙØ©") : ""}${data.last_ms ? " · ~" + Math.round(data.last_ms / 1000) + "s" : ""}` 2850 : st === "sleeping" ? T("اÙÙ ØØ±Ù ÙØ§ÙÙ Ø Ø£ÙÙ ØµÙØ±Ø© بتصØÙÙÙ (ØÙاÙ٠دÙÙÙØ©)") : st === "loading" ? T("اÙÙ ØØ±Ù Ø¹Ù ÙØÙ Ù٠اÙÙÙ ÙØ°Ø¬â¦") : st === "offline" ? T("اÙÙ ØØ±Ù ٠تÙÙÙ") : T("اÙÙ ØØ±Ù ÙÙÙ Ù Ø´ÙÙØ©") + (data.error ? ": " + data.error : ""); 2851 // back off when things are not ready; pause entirely while the tab is hidden 2852 pollEngine.fails = st === "ready" ? 0 : (pollEngine.fails || 0) + 1; 2853 const next = st === "ready" ? 15000 : Math.min(60000, 4000 * 2 ** Math.min(4, pollEngine.fails - 1)); 2854 engineTimer = setTimeout(() => (document.hidden ? document.addEventListener("visibilitychange", pollEngine, { once: true }) : pollEngine()), next); 2855 } 2856 function toggleImageMode(on) { 2857 const T = window.HythmI18n ? HythmI18n.t : (x) => x; 2858 on = on === undefined ? task !== "image" : on; 2859 setTask(on ? "image" : "auto"); 2860 if (on) pollEngine(); 2861 els.composer.classList.remove("hint-image"); 2862 toast(T(on ? "ÙØ¶Ø¹ Ø§ÙØµÙر Ø´ØºÙØ§Ù: ÙÙ Ø±Ø³Ø§ÙØ© Ø¨ØªØµÙØ± ØµÙØ±Ø©" : "Ø±Ø¬Ø¹ÙØ§ ÙÙÙØ¶Ø¹ Ø§ÙØ°ÙÙ")); 2863 els.input.focus(); 2864 } 2865 $("mcBtn").addEventListener("click", () => { closeSidebar(); toggleImageMode(true); }); 2866 $("imgModeBtn").addEventListener("click", () => toggleImageMode()); 2867 // ---- options as visible chips: quick buttons next to the input, a blue chip inside it while active, Backspace removes it ---- 2868 function syncChips() { 2869 const web = $("webBtn").getAttribute("aria-pressed") === "true"; 2870 $("webChip").hidden = !web; $("quickWeb").setAttribute("aria-pressed", String(web)); 2871 $("siteChip").hidden = !fullSite; $("quickImg").setAttribute("aria-pressed", String(task === "image")); 2872 } 2873 $("quickWeb").addEventListener("click", () => { if (task === "image") toggleImageMode(false); $("webBtn").click(); syncChips(); els.input.focus(); }); 2874 $("quickImg").addEventListener("click", () => { if ($("webBtn").getAttribute("aria-pressed") === "true") $("webBtn").click(); toggleImageMode(); syncChips(); els.input.focus(); }); 2875 $("webChip").addEventListener("click", () => { $("webBtn").click(); syncChips(); els.input.focus(); }); 2876 $("siteChip").addEventListener("click", () => { $("fullSiteBtn").click(); syncChips(); els.input.focus(); }); 2877 ["webBtn", "fullSiteBtn", "imgModeBtn", "imgModeExit"].forEach((id) => $(id).addEventListener("click", () => setTimeout(syncChips, 0))); 2878 els.input.addEventListener("keydown", (e) => { 2879 if (e.key !== "Backspace" || els.input.value.length || els.input.selectionStart) return; 2880 if (fullSite) { e.preventDefault(); $("fullSiteBtn").click(); } 2881 else if ($("webBtn").getAttribute("aria-pressed") === "true") { e.preventDefault(); $("webBtn").click(); } 2882 else if (task === "image") { e.preventDefault(); toggleImageMode(false); } 2883 else return; 2884 setTimeout(syncChips, 0); 2885 }); 2886 setTimeout(syncChips, 500); 2887 $("imgModeExit").addEventListener("click", () => toggleImageMode(false)); 2888 $("fullSiteBtn").addEventListener("click", () => { 2889 fullSite = !fullSite; 2890 $("fullSiteBtn").setAttribute("aria-pressed", String(fullSite)); 2891 updateSendCost(); 2892 if (fullSite) toast((window.HythmI18n ? HythmI18n.t : (x) => x)("Ù ÙÙØ¹ ÙØ§Ù Ù: ØµÙØØ§Øª ٠تعددة + JavaScript ØÙÙÙÙ (Ã2)")); 2893 }); 2894 setTask(task); 2895 2896 /* ---------------- Settings ---------------- */ 2897 const DEFAULTS = { theme: "dark", dialect: "auto", dual: "auto", tone: "default", format: "default", emoji: "default", instructions: "", sendKey: "enter", density: "comfy", memory: "on", lang: "en", appTheme: "lumen", appFont: "manrope", motion: "full", mode: "balanced", model: "omni", ask: "on", diag: "on", textSize: "m", accent: "", sidebar: "open", device: "desktop" };
2898 var appSettings = Object.assign({}, DEFAULTS, storage.json("hythm.settings") || {}); 2899 if (appSettings.theme === "system" && !appSettings.themeChosen) appSettings.theme = "dark"; // old default â the same look on every phone 2900 const APP_FONTS = { 2901 manrope: ["Manrope", "Readex Pro", "family=Manrope:wght@400;500;600;700;800&family=Readex+Pro:wght@300;400;500;600;700"], 2902 inter: ["Inter", "IBM Plex Sans Arabic", "family=Inter:wght@400;500;600;700;800&family=IBM+Plex+Sans+Arabic:wght@400;500;600;700"], 2903 sora: ["Sora", "Tajawal", "family=Sora:wght@400;600;700;800&family=Tajawal:wght@400;500;700;800"], 2904 grotesk: ["Space Grotesk", "Cairo", "family=Space+Grotesk:wght@400;500;600;700&family=Cairo:wght@400;600;700;800"], 2905 outfit: ["Outfit", "Almarai", "family=Outfit:wght@400;500;600;700;800&family=Almarai:wght@400;700;800"] 2906 }; 2907 function applySettings(first) { 2908 const r = document.documentElement; 2909 r.dataset.appTheme = appSettings.appTheme; 2910 r.dataset.motion = appSettings.motion; 2911 r.dataset.density = appSettings.density; 2912 const dark = appSettings.theme === "dark" || (appSettings.theme !== "light" && !matchMedia("(prefers-color-scheme: light)").matches); 2913 r.dataset.theme = dark ? "dark" : "light"; 2914 r.style.colorScheme = dark ? "dark" : "light"; 2915 const tc = document.querySelector('meta[name="theme-color"]'); if (tc) tc.content = dark ? "#212121" : "#ffffff"; 2916 const cs = document.querySelector('meta[name="color-scheme"]'); if (cs) cs.content = dark ? "dark" : "light"; 2917 const ci = document.getElementById("customInstr"); if (ci && document.activeElement !== ci) ci.value = appSettings.instructions || ""; 2918 document.body.style.fontSize = { s: "14px", m: "15px", l: "16.5px" }[appSettings.textSize] || "15px"; 2919 if (appSettings.accent) { r.style.setProperty("--primary", appSettings.accent); r.style.setProperty("--primary-hover", appSettings.accent); } else { r.style.removeProperty("--primary"); r.style.removeProperty("--primary-hover"); } 2920 const ai = document.getElementById("accentInput"); if (ai) { ai.value = appSettings.accent || "#8b9bff"; document.getElementById("accentHex").textContent = appSettings.accent || "Auto"; } 2921 const af = APP_FONTS[appSettings.appFont] || APP_FONTS.manrope; 2922 r.style.setProperty("--font", `"${af[0]}", "${af[1]}", system-ui, sans-serif`); 2923 const link = document.getElementById("appFontLink"); 2924 const href = `https://fonts.googleapis.com/css2?${af[2]}&family=JetBrains+Mono:wght@400;500&family=Space+Grotesk:wght@600;700&display=swap`; 2925 if (link && link.href !== href) link.href = href; 2926 if (appSettings.ask === "off") storage.set("hythm.noAsk", "1"); else storage.del("hythm.noAsk"); 2927 if (first) { if (appSettings.sidebar === "collapsed" && !storage.get("hythm.side")) els.app.classList.add("side-collapsed"); const dv = document.querySelector(`[data-device="${appSettings.device}"]`); if (dv) setTimeout(() => dv.click(), 0); if (!storage.get("hythm.mode")) mode = appSettings.mode; if (["ultra", "x", "nova"].includes(appSettings.model)) model = appSettings.model; setModel(model); } 2928 document.querySelectorAll("[data-set]").forEach((g) => g.querySelectorAll("button").forEach((b) => b.classList.toggle("on", appSettings[g.dataset.set] === b.dataset.v))); 2929 } 2930 applySettings(true); 2931 matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => { if (appSettings.theme === "system") applySettings(); }); 2932 $("settingsBtn").addEventListener("click", () => { closeSidebar(); applySettings(); if (!document.getElementById("fontPreviewLink")) { const l = document.createElement("link"); l.id = "fontPreviewLink"; l.rel = "stylesheet"; l.href = "https://fonts.googleapis.com/css2?family=Inter:wght@600&family=Sora:wght@600&family=Space+Grotesk:wght@600&family=Outfit:wght@600&display=swap"; document.head.appendChild(l); } $("settingsDialog").showModal(); }); 2933 document.querySelectorAll("[data-set]").forEach((g) => g.querySelectorAll("button").forEach((b) => b.addEventListener("click", () => { 2934 const key = g.dataset.set, prevLang = appSettings.lang;
2935 appSettings[key] = b.dataset.v; if (key === "theme") appSettings.themeChosen = 1; 2936 storage.set("hythm.settings", JSON.stringify(appSettings)); 2937 if (key === "mode") setMode(b.dataset.v); 2938 if (key === "model") setModel(b.dataset.v); 2939 applySettings(); 2940 if (key === "lang" && prevLang !== b.dataset.v) location.reload(); 2941 else toast(window.HythmI18n ? HythmI18n.t("ØªÙ Ø§ÙØÙØ¸") : "ØªÙ Ø§ÙØÙØ¸"); 2942 }))); 2943 $("resetSettings").addEventListener("click", () => { storage.del("hythm.settings"); storage.del("hythm.noAsk"); location.reload(); }); 2944 // tabs 2945 if ($("clearMemory")) $("clearMemory").addEventListener("click", async () => { 2946 const r = await api("/api/memory", { uid, action: "clear" }); 2947 toast((window.HythmI18n ? HythmI18n.t : (x) => x)(r.ok ? "ÙØ³Ùت Ù٠ش٠تعÙÙ٠ت٠عÙÙ" : "٠ا Ø²Ø¨Ø·Ø Ø¬Ø±ÙØ¨ بعدÙÙ")); 2948 }); 2949 // ---- Settings â Usage: the window, this chat's context, what used it, how it is counted, alerts ---- 2950 function kindOf(reason) { 2951 const r = String(reason || ""); 2952 return /ØµÙØ±|image|NanoSpark|Micro Cloud/i.test(r) ? ["ð¨", "ØµÙØ±"] : /اجت٠اع|ØªÙØ±Ùغ/.test(r) ? ["ð", "اجت٠اعات"] : /ØµÙØª|Ù ÙØ§ÙÙ Ø©/.test(r) ? ["ð", "ØµÙØª"] : /تص٠ÙÙ |Ù ÙÙØ¹|تعدÙÙ/.test(r) ? ["ð¥", "تصا٠ÙÙ ÙÙ ÙØ§Ùع"] : /ØªØ¯Ø±ÙØ¨|ÙÙØ¯|استرجاع/.test(r) ? ["ð", "Ø¥Ø¶Ø§ÙØ§Øª"] : ["ð¬", "Ù ØØ§Ø¯Ø«Ø©"]; 2953 } 2954 async function renderUsageSettings() { 2955 const el = document.getElementById("setUsage"); if (!el) return; 2956 try { await refreshMe(); } catch {} 2957 const u = usageState(); const T = window.HythmI18n ? HythmI18n.t : (x) => x; 2958 if (!u) { el.innerHTML = "<p>" + esc(T("سجÙ٠دخÙÙ ÙØªØ´Ù٠استخدا٠Ù.")) + "</p>"; return; } 2959 const win = (me.usage && me.usage.reset) ? me.usage.reset - 5 * 3600000 : 0; 2960 const items = (me.ledger || []).filter((l) => l.t >= win && l.delta < 0); 2961 const by = {}; items.forEach((l) => { const [ic, k] = kindOf(l.reason); by[k] = by[k] || { ic, v: 0 }; by[k].v += -l.delta; }); 2962 const plan = (me.plan && me.plan.name) || "Hythm Free", ctx = contextPct(), pc = (v) => (Math.round((v / u.cap) * 1000) / 10).toLocaleString("en") + "%"; 2963 el.innerHTML = 2964 '<div class="su-top"><div class="su-ring"><svg viewBox="0 0 36 36"><circle class="ur-track" cx="18" cy="18" r="15"/><circle class="ur-fill" cx="18" cy="18" r="15" pathLength="100" style="stroke-dasharray:' + u.leftPct.toFixed(2) + ' 100"/></svg><b>' + Math.round(u.leftPct) + '%</b><small>' + esc(T("٠تبÙÙ")) + '</small></div>' + 2965 '<div class="su-info"><b>' + esc(plan) + '</b><p>' + esc(T("استخد٠ت")) + " <strong>" + u.usedPct.toFixed(1) + "%</strong> " + esc(T("Ù Ù ØØ¯Ù اÙÙ5 ساعات")) + '</p><p>' + esc(T("Ø¨ÙØ±Ø¬Ø¹ ÙØ§Ù ٠بعد")) + " <strong>" + esc(fmtReset(u.reset)) + "</strong> · " + esc(new Date(u.reset).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })) + '</p>' + 2966 (me.plan && me.plan.id !== "free" ? "" : '<button type="button" class="su-btn pri" data-su-plus>' + esc(T("Ø²ÙØ¯ ØØ¯ÙÙ Ã12 ٠ع Plus")) + "</button>") + "</div></div>" + 2967 '<div class="su-card"><div class="su-row"><span>' + esc(T("Ø³ÙØ§Ù ÙØ§ÙÙ ØØ§Ø¯Ø«Ø©")) + '</span><b>' + ctx + '%</b></div><div class="su-bar"><i style="width:' + ctx + '%"></i></div><small>' + esc(T("ÙÙ Ø±Ø³Ø§ÙØ© بتØÙ Ù Ù Ø¹ÙØ§ اÙÙ ØØ§Ø¯Ø«Ø© اÙÙÙ ÙØ¨ÙÙØ§. Ù٠ا ØªÙØµÙ 100% Ø§ÙØªØ Ù ØØ§Ø¯Ø«Ø© Ø¬Ø¯ÙØ¯Ø© ÙØ±Ø¯ أسرع.")) + "</small></div>" + 2968 '<div class="su-card"><b class="su-h">' + esc(T("Ø´Ù Ø§Ø³ØªØ®Ø¯Ù ØØ¯ÙÙ Ø¨ÙØ§ÙÙ5 ساعات")) + "</b>" + (Object.keys(by).length ? Object.entries(by).sort((x, y) => y[1].v - x[1].v).map(([k, o]) => '<div class="su-row"><span>' + o.ic + " " + esc(T(k)) + '</span><b>' + pc(o.v) + '</b></div><div class="su-bar thin"><i style="width:' + Math.min(100, (o.v / u.cap) * 100) + '%"></i></div>').join("") : "<small>" + esc(T("ÙØ³Ø§ ٠ا استخد٠ت Ø´Ù Ø¨ÙØ§ÙÙØ§Ùذة.")) + "</small>") + "</div>" + 2969 '<div class="su-card"><b class="su-h">' + esc(T("آخر Ø§ÙØ·Ùبات")) + '</b><div class="su-list">' + (items.slice(0, 12).map((l) => '<div><span>' + kindOf(l.reason)[0] + " " + esc(l.reason) + '</span><em>' + esc(new Date(l.t).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })) + '</em><b>' + pc(-l.delta) + "</b></div>").join("") || "<small>â</small>") + "</div></div>" + 2970 '<div class="su-card"><b class="su-h">' + esc(T("ÙÙ٠بÙÙØØ³Ø¨Ø")) + '</b><div class="su-grid"><div><span>ð§ </span><b>' + esc(T("اÙÙÙ ÙØ°Ø¬")) + '</b><small>Mini Ã1 · Ultra Ã2 · Code Ã2.5 · X Ã3 · Nova Ã3</small></div><div><span>ð</span><b>' + esc(T("Ø§ÙØªÙÙÙØ±")) + '</b><small>' + esc(T("Ø³Ø±ÙØ¹ Ã0.8 · Ù ØªÙØ³Ø· Ã1 · Ø£ÙØµÙ Ã1.5")) + '</small></div><div><span>ð</span><b>' + esc(T("Ø§ÙØ·ÙÙ")) + '</b><small>' + esc(T("Ù٠تÙÙÙ ÙØ±Ø£Ù Ø£Ù ÙØªØ¨Ù Ø£Ù ÙÙÙØ± ÙÙÙ")) + '</small></div><div><span>ð¨</span><b>' + esc(T("Ø§ÙØµÙر")) + '</b><small>' + esc(T("ØØ³Ø¨ Ø§ÙØ¬Ùدة ÙØ§ÙÙ ÙØºØ§Ø¨ÙسÙ")) + "</small></div></div></div>" + 2971 '<div class="su-card"><b class="su-h">' + esc(T("ØªÙØ¨ÙÙØ§Øª")) + '</b><label class="su-tog"><input type="checkbox" data-su-set="usageAlert"' + (appSettings.usageAlert !== "off" ? " checked" : "") + "><span>" + esc(T("ÙØ¨ÙÙÙÙ Ù٠ا Ø£ÙØµÙ 80% Ù Ù ØØ¯ÙÙ")) + '</span></label><label class="su-tog"><input type="checkbox" data-su-set="usageMeta"' + (appSettings.usageMeta !== "off" ? " checked" : "") + "><span>" + esc(T("اعرض ÙØ¯ÙØ´ أخذ Ù٠رد ØªØØª Ø§ÙØ±Ø¯")) + "</span></label></div>"; 2972 el.querySelector("[data-su-plus]")?.addEventListener("click", () => { $("settingsDialog")?.close?.(); window.HythmPlans && HythmPlans.open && HythmPlans.open("plus"); }); 2973 el.querySelectorAll("[data-su-set]").forEach((c) => c.addEventListener("change", () =>
2973 { appSettings[c.dataset.suSet] = c.checked ? "on" : "off"; saveSettings(); document.documentElement.classList.toggle("no-usage-meta", appSettings.usageMeta === "off"); })); 2974 } 2975 document.querySelectorAll("[data-stab]").forEach((b) => b.addEventListener("click", () => { 2976 if (b.dataset.stab === "usage") renderUsageSettings(); 2977 document.querySelectorAll("[data-stab]").forEach((x) => x.classList.toggle("on", x === b)); 2978 document.querySelectorAll("[data-spanel]").forEach((p) => (p.hidden = p.dataset.spanel !== b.dataset.stab)); 2979 })); 2980 // accent color 2981 const saveSettings = () => storage.set("hythm.settings", JSON.stringify(appSettings)); 2982 document.documentElement.classList.toggle("no-usage-meta", appSettings.usageMeta === "off"); 2983 $("accentInput").addEventListener("input", (e) => { appSettings.accent = e.target.value; saveSettings(); applySettings(); }); 2984 $("accentReset").addEventListener("click", () => { appSettings.accent = ""; saveSettings(); applySettings(); }); 2985 // data 2986 $("exportSettings").addEventListener("click", () => { 2987 const a = document.createElement("a"); 2988 a.href = URL.createObjectURL(new Blob([JSON.stringify({ hythmSettings: appSettings }, null, 2)], { type: "application/json" })); 2989 a.download = "hythm-settings.json"; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1500); 2990 }); 2991 $("importSettings").addEventListener("change", async (e) => { 2992 try { 2993 const j = JSON.parse(await e.target.files[0].text()); 2994 if (!j.hythmSettings) throw 0; 2995 const clean = {}; Object.keys(DEFAULTS).forEach((k) => { if (typeof j.hythmSettings[k] === "string") clean[k] = j.hythmSettings[k]; }); 2996 appSettings = Object.assign({}, DEFAULTS, clean); saveSettings(); location.reload(); 2997 } catch { toast(window.HythmI18n ? HythmI18n.t("Ù ÙÙ ØºÙØ± ØµØ§ÙØ") : "Ù ÙÙ ØºÙØ± ØµØ§ÙØ"); } 2998 }); 2999 $("wipeProjects").addEventListener("click", () => { 3000 const q = "Ù ØªØ£ÙØ¯ Ø¥ÙÙ Ø¨Ø¯Ù ØªØØ°Ù Ù٠اÙÙ Ø´Ø§Ø±ÙØ¹Ø"; 3001 if (!confirm(window.HythmI18n ? HythmI18n.t(q) : q)) return; 3002 projects.forEach((p) => storage.del("hythm.p." + p.id)); projects = []; storage.del("hythm.projects"); storage.del("hythm.currentProject"); 3003 loadProject(null); toast(window.HythmI18n ? HythmI18n.t("Ù٠اÙÙ Ø´Ø§Ø±ÙØ¹ Ø§ÙØØ°ÙØª") : "Ù٠اÙÙ Ø´Ø§Ø±ÙØ¹ Ø§ÙØØ°ÙØª"); 3004 }); 3005 // Ctrl+, (settings) lives in the main shortcut handler 3006 3007 /* ---------------- Diagnostics (errors from the preview) ---------------- */ 3008 let lastDiag = []; 3009 const DIAG_LABEL = { js: "JavaScript", resource: "Resource", icon: "Icon", layout: "Layout", html: "HTML", a11y: "Accessibility" }; 3010 window.addEventListener("message", (e) => { 3011 if (e.source !== els.frame.contentWindow || !e.data?.hythmDiag || appSettings.diag === "off") return; 3012 lastDiag = e.data.hythmDiag; 3013 const serious = lastDiag.filter((d) => d.type !== "a11y"); 3014 $("diagBtn").hidden = !serious.length; 3015 $("diagText").textContent = serious.length; 3016 $("diagBtn").title = serious.map((d) => "⢠" + d.msg).join("\n"); 3017 }); 3018 $("diagBtn").addEventListener("click", () => { 3019 $("diagList").innerHTML = lastDiag.map((d) => `<li class="diag-${d.type}"><span class="diag-type">${DIAG_LABEL[d.type] || d.type}</span><code>${esc(d.msg)}</code></li>`).join("") || "<li>٠ا Ù٠أخطاء</li>"; 3020 $("diagDialog").showModal(); 3021 }); 3022 $("diagFix").addEventListener("click", () => { 3023 $("diagDialog").close(); 3024 if (!lastDiag.length || !isEditing()) return; 3025 const text = "Fix these errors in the page without changing the design:\n" + lastDiag.map((d) => "- [" + d.type + "] " + d.msg).join("\n"); 3026 send(text); 3027 }); 3028 3029 /* ---------------- Mini Omni showcase ---------------- */ 3030 function exploreTab(which) { 3031 document.querySelectorAll("#omniDialog .xp-tab").forEach((b) => b.setAttribute("aria-selected", String(b.dataset.pane === which))); 3032 document.querySelectorAll("#omniDialog .xp-pane").forEach((p) => (p.hidden = p.dataset.pane !== which)); 3033 if (which === "cloud") loadShowcase(); 3034 } 3035 document.querySelectorAll("#omniDialog .xp-tab").forEach((b) => b.addEventListener("click", () => exploreTab(b.dataset.pane))); 3036 document.querySelector("#omniDialog [data-go-plus]")?.addEventListener("click", () => { $("omniDialog").close(); window.HythmPlans && HythmPlans.open && HythmPlans.open("plus"); }); 3037 let showcaseLoaded = false;
3038 async function loadShowcase() { 3039 if (showcaseLoaded) return; showcaseLoaded = true; 3040 const g = $("mcGallery"); if (!g) return; 3041 const { data } = await api("/api/showcase"); 3042 const list = (data && data.images) || []; 3043 if (!list.length) { g.innerHTML = '<p class="mc-empty">' + esc((window.HythmI18n ? HythmI18n.t : (x) => x)("اÙ٠عرض Ø¨ÙØªØ¹Ø¨ÙÙ ÙØ±ÙØ¨Ø§Ù Ø¨ØµÙØ± Ù Ù ØµÙØ¹ NanoSpark.")) + "</p>"; return; } 3044 g.innerHTML = list.map((im) => '<figure class="mc-shot" tabindex="0" data-p="' + esc(im.prompt || "") + '"><img src="' + esc(im.url) + '" alt="' + esc(im.prompt || "NanoSpark") + '" loading="lazy"><figcaption>' + esc(im.prompt || "") + '<button type="button" class="mc-try">' + esc((window.HythmI18n ? HythmI18n.t : (x) => x)("Ø¬Ø±ÙØ¨ اÙÙÙØ±Ø©")) + "</button></figcaption></figure>").join(""); 3045 g.querySelectorAll(".mc-shot").forEach((f) => f.addEventListener("click", () => { $("omniDialog").close(); els.input.value = "ارس٠" + f.dataset.p; autoGrow(); els.input.focus(); els.input.dispatchEvent(new Event("input")); })); 3046 } 3047 const exploreLabels = ["Explore Models", "Explore Hythm"]; 3048 let exploreIdx = 0; 3049 setInterval(() => { 3050 const el = document.getElementById("exploreText"); if (!el || document.hidden) return; 3051 el.classList.add("out"); 3052 setTimeout(() => { exploreIdx = (exploreIdx + 1) % 2; el.textContent = exploreLabels[exploreIdx]; el.classList.remove("out"); }, 320); 3053 }, 3200); 3054 $("omniBtn").addEventListener("click", () => { closeSidebar(); exploreTab(exploreIdx ? "cloud" : "models"); $("omniDialog").showModal(); }); 3055 const mcTry = document.getElementById("mcTry"); 3056 if (mcTry) mcTry.addEventListener("click", () => { $("omniDialog").close(); els.input.value = "ارس٠"; autoGrow(); els.input.focus(); els.input.dispatchEvent(new Event("input")); }); 3057 $("omniStart").addEventListener("click", () => { $("omniDialog").close(); els.input.focus(); }); 3058 document.querySelectorAll("#omniDialog [data-pick-model]").forEach((b) => b.addEventListener("click", () => { setModel(b.dataset.pickModel); $("omniDialog").close(); els.input.focus(); })); 3059 document.querySelectorAll("#omniDialog [data-go-pane]").forEach((b) => b.addEventListener("click", () => { const t = document.querySelector('#omniDialog .xp-tab[data-pane="' + b.dataset.goPane + '"]'); if (t) t.click(); })); 3060 $("omniDialog").addEventListener("click", (e) => { if (e.target.id === "omniDialog") $("omniDialog").close(); }); 3061 3062 /* ---------------- Variations: the current design in every theme (free) ---------------- */ 3063 const THEMES = Object.entries(THEME_META).map(([k, m]) => [k, m[0]]); 3064 const withTheme = (code, t) => /data-theme=["'][\w-]+["']/.test(code) ? code.replace(/data-theme=["'][\w-]+["']/, `data-theme="${t}"`) : code.replace(/<html/i, `<html data-theme="${t}"`); 3065 $("variantsBtn").addEventListener("click", () => { 3066 const v = session.versions[session.current]; 3067 if (!v) return toast("ص٠Ù٠ش٠أÙÙ"); 3068 const cur = (v.code.match(/data-theme=["']([\w-]+)/) || [])[1]; 3069 $("variantsGrid").innerHTML = THEMES.map(([k, n]) => `<button type="button" class="variant${k === cur ? " current" : ""}" data-theme="${k}"><span class="variant-frame"><iframe tabindex="-1" loading="lazy" sandbox="allow-scripts" title="${n}"></iframe></span><span class="variant-name">${n}${k === cur ? " · Ø§ÙØØ§ÙÙ" : ""}</span></button>`).join(""); 3070 $("variantsGrid").querySelectorAll(".variant").forEach((b, i) => { 3071 setTimeout(() => { b.querySelector("iframe").srcdoc = buildDoc(withTheme(v.code, b.dataset.theme)); }, i * 120); 3072 b.addEventListener("click", () => { 3073 $("variantsDialog").close(); 3074 if (b.dataset.theme === cur) return; 3075 $("themeSelect").value = b.dataset.theme; 3076 $("themeSelect").dispatchEvent(new Event("change")); 3077 }); 3078 }); 3079 $("variantsDialog").showModal(); 3080 }); 3081 3082 /* ---------------- Tools ---------------- */ 3083 const exportDoc = () => (lastCode ? buildDoc(lastCode) : ""); 3084 $("copyBtn").addEventListener("click", async () => { 3085 if (!lastCode) return toast("٠ا Ù٠تص٠ÙÙ ÙØ³Ø§"); 3086 try { await navigator.clipboard.writeText(exportDoc()); toast("ØªÙ ÙØ³Ø® اÙÙÙØ¯ ÙØ§Ù Ù"); } catch { toast("٠ا ÙØ¯Ø±Øª Ø£ÙØ³Ø®"); } 3087 }); 3088 $("downloadBtn").addEventListener("click", () => { 3089 if (!lastCode) return toast("٠ا Ù٠تص٠ÙÙ ÙØ³Ø§"); 3090 const a = document.createElement("a"); 3091 a.href = URL.createObjectURL(new Blob([exportDoc()], { type: "text/html" })); 3092 a.download = `hythm-v${session.current + 1 || 1}.html`; 3093 a.click(); 3094 setTimeout(() => URL.revokeObjectURL(a.href), 2000); 3095 }); 3096 $("openBtn").addEventListener("click", () => { 3097 if (!lastCode) return toast("٠ا Ù٠تص٠ÙÙ ÙØ³Ø§"); 3098 window.open(URL.createObjectURL(new Blob([exportDoc()], { type: "text/html" })), "_blank"); 3099 }); 3100 function renderPhotoList() { 3101 const used = showCode.used || []; 3102 const seen = new Set(); 3103 const list = used.filter((u) => !seen.has(u.url) && seen.add(u.url)); 3104 $("photoList").innerHTML = list.length ? list.map((p) => `<li><img src="${esc(p.url)}" alt="" loading="lazy" referrerpolicy="no-referrer"><div><b title="${esc(p.title)}">${esc(p.title)}</b><span>${esc(p.creator || "â")}</span><span>${esc(p.license)} · <a href="${esc(p.link)}" target="_blank" rel="noopener">${esc(p.source)}</a></span><button type="button" class="report-btn" data-report="${esc(p.url)}">${icon("shield")}Ø¥Ø¨ÙØ§Øº ÙØØ°Ù</button></div></li>`).join("") 3105 : `<li style="grid-column:1/-1;padding:16px;text-align:center">ÙØ§Ùتص٠Ù٠٠ا ÙÙÙ ØµÙØ±.</li>`; 3106 $("photoList").querySelectorAll("[data-report]").forEach((b) => b.addEventListener("click", () => reportPhoto(b.dataset.report))); 3107 } 3108 async function reportPhoto(url) { 3109 await api("/api/images/report", { url }); 3110 for (const [q, list] of imageCache) imageCache.set(q, list.filter((x) => x.url !== url));
3111 if (lastCode) showCode(lastCode, { final: true }); 3112 renderPhotoList(); 3113 toast("Ø§ÙØØ¸Ø±Øª Ø§ÙØµÙرة ÙÙØ§Ø¦ÙØ§Ù ÙØªØ¨Ø¯ÙÙØª"); 3114 } 3115 $("photosBtn").addEventListener("click", () => { renderPhotoList(); $("photosDialog").showModal(); }); 3116 document.querySelectorAll("[data-close]").forEach((b) => b.addEventListener("click", () => b.closest("dialog").close())); 3117 document.querySelectorAll("dialog").forEach((d) => d.addEventListener("click", (e) => { if (e.target === d && d.id !== "loginDialog") d.close(); })); 3118 3119 /* ---------------- Account ---------------- */ 3120 function openLogin() { 3121 $("starterAmount").textContent = cfg.starter ?? 40; 3122 const d = $("loginDialog"); 3123 if (!d.open) d.showModal(); 3124 setTimeout(() => $("loginName").focus(), 50); 3125 } 3126 $("loginDialog").addEventListener("cancel", (e) => { if (!me) e.preventDefault(); }); 3127 $("loginForm").addEventListener("submit", async (e) => { 3128 e.preventDefault(); 3129 const name = $("loginName").value.trim(); 3130 if (!name) return; 3131 $("loginSubmit").disabled = true; 3132 const { ok, data } = await api("/api/login", { name }); 3133 $("loginSubmit").disabled = false; 3134 if (!ok) return toast("٠ا ÙØ¯Ø±Øª Ø£ÙØ´Ø¦ Ø§ÙØØ³Ø§Ø¨"); 3135 uid = data.uid; storage.set("hythm.uid", uid); storage.set("hythm.name", name); 3136 applyMe(data.user, data.site); 3137 $("loginDialog").close(); 3138 toast(`Ø£ÙÙØ§Ù ${data.user.name}! Ø§Ø³ØªØ®Ø¯Ø§Ù Ù Ø¨ÙØªØ¬Ø¯Ø¯ ÙÙ 5 ساعات â¦`); 3139 els.input.focus(); 3140 }); 3141 function applyMe(user, site) { 3142 me = user; siteCredits = site; 3143 setGreeting(); 3144 $("avatarLetter").textContent = (user.name || "Ø").trim().charAt(0).toUpperCase(); 3145 setCredits(user.credits); 3146 if (window.HythmPlans) HythmPlans.onMe(user); 3147 } 3148 async function refreshMe(animate) { 3149 if (!uid) return false; 3150 const { ok, data, status } = await api(`/api/me?uid=${encodeURIComponent(uid)}`); 3151 refreshMe.lastStatus = status; 3152 if (!ok) return false; 3153 if (data.user && data.user.name) storage.set("hythm.name", data.user.name); 3154 const prev = me?.credits, fresh = data.user.credits; // keep the new balance: me.credits is reset to prev below for the count-up animation 3155 me = data.user; siteCredits = data.site; 3156 $("avatarLetter").textContent = (me.name || "Ø").trim().charAt(0).toUpperCase(); 3157 setGreeting(); 3158 if (prev != null) me.credits = prev; 3159 setCredits(fresh, animate); 3160 if (window.HythmPlans) HythmPlans.onMe(me); // plan badge, perks, renewal reminder 3161 return true; 3162 } 3163 async function openAccount() { 3164 await refreshMe(); 3165 if (!me) return openLogin(); 3166 $("accCredits").textContent = me.credits; 3167 if ($("accStreak")) { 3168 const hrs = Math.floor((me.nextRefillMs || 0) / 36e5), mins = Math.floor(((me.nextRefillMs || 0) % 36e5) / 6e4); 3169 $("accStreak").textContent = me.streak || 1; 3170 $("accNext").textContent = hrs + "h " + mins + "m"; 3171 $("accBar").style.width = Math.min(100, Math.round((me.credits / (me.refillTo || 40)) * 100)) + "%"; 3172 $("accImages").textContent = me.images || 0; $("accChats").textContent = me.chats || 0; 3173 } 3174 $("accGens").textContent = me.generations; 3175 $("accSpent").textContent = me.spent; 3176 if ($("accSite")) $("accSite").textContent = siteCredits.toLocaleString("en"); 3177 $("accRefill").textContent = `Ø¨ÙØªØ¬Ø¯Ø¯ تÙÙØ§Ø¦Ùا٠ÙÙ 5 ساعات`; 3178 for (const [k, m] of Object.entries(cfg.modes)) { 3179 document.querySelector(`[data-cost="${k}"]`).textContent = m.cost; 3180 document.querySelector(`[data-edit="${k}"]`).textContent = m.editCost; 3181 } 3182 $("ledger").innerHTML = (me.ledger || []).map((l) => `<li><div>${esc(l.reason)}<time>${fmtTime(l.t)}</time></div><span class="delta ${l.delta > 0 ? "plus" : l.delta < 0 ? "minus" : "zero"}">${l.delta > 0 ? "+" : ""}${l.delta}</span></li>`).join("") || `<li>٠ا Ù٠ع٠ÙÙØ§Øª ÙØ³Ø§</li>`; 3183 $("accountDialog").showModal(); 3184 } 3185 els.creditPill.addEventListener("click", openAccount); 3186 // ---------------- shell: user menu, + menu, views ---------------- 3187 const T_ = (x) => (window.HythmI18n ? HythmI18n.t(x) : x); 3188 function popover(btn, menu, open) { 3189 open = open === undefined ? menu.hidden : open; 3190 document.querySelectorAll(".menu.pop-open").forEach((m) => { if (m !== menu) { m.hidden = true; m.classList.remove("pop-open"); } }); 3191 menu.hidden = !open;
3191 menu.classList.toggle("pop-open", open); btn.setAttribute("aria-expanded", String(open)); 3192 } 3193 document.addEventListener("click", (e) => { 3194 document.querySelectorAll(".menu.pop-open").forEach((m) => { 3195 const owner = m.previousElementSibling && m.parentElement; 3196 if (!m.parentElement.contains(e.target)) { m.hidden = true; m.classList.remove("pop-open"); } 3197 }); 3198 }); 3199 $("accountBtn").addEventListener("click", () => { 3200 if (me) { $("umName").textContent = me.name || ""; $("umLetter").textContent = (me.name || "Ø").trim().charAt(0).toUpperCase(); } 3201 popover($("accountBtn"), $("userMenu"), $("userMenu").hidden ? true : false); 3202 }); 3203 els.creditPill.addEventListener("click", (e) => e.stopPropagation(), true); 3204 $("userMenu").querySelectorAll("[data-um]").forEach((b) => b.addEventListener("click", () => { 3205 popover($("accountBtn"), $("userMenu"), false); closeSidebar(); 3206 const a = b.dataset.um; 3207 if (a === "credits") { $("settingsBtn").click(); setTimeout(() => document.querySelector('[data-stab="usage"]')?.click(), 60); } 3208 else if (a === "settings") $("settingsBtn").click(); 3209 else if (a === "personal") { $("settingsBtn").click(); document.querySelector('[data-stab="personal"]')?.click(); } 3210 else if (a === "keys") $("shortcutsDialog").showModal(); 3211 else if (a === "logout") $("logoutBtn").click(); 3212 })); 3213 $("plusMenuBtn").addEventListener("click", (e) => { e.stopPropagation(); popover($("plusMenuBtn"), $("plusMenu")); }); 3214 $("plusMenu").querySelectorAll(".menu-row").forEach((b) => b.addEventListener("click", () => popover($("plusMenuBtn"), $("plusMenu"), false))); 3215 $("imgModeBar").addEventListener("click", () => toggleImageMode(false)); 3216 (function voice() { 3217 const SR = window.SpeechRecognition || window.webkitSpeechRecognition, b = document.getElementById("micBtn"); 3218 if (!b) return; 3219 if (!SR) { b.hidden = true; return; } 3220 let rec = null; 3221 b.addEventListener("click", () => { 3222 if (rec) { rec.stop(); return; } 3223 rec = new SR(); rec.lang = (appSettings.lang === "ar" ? "ar-SA" : "en-US"); rec.interimResults = true; rec.continuous = false; 3224 const base = els.input.value ? els.input.value.replace(/\s*$/, " ") : ""; 3225 rec.onresult = (ev) => { let t = ""; for (const r of ev.results) t += r[0].transcript; els.input.value = base + t; autoGrow(); els.input.dispatchEvent(new Event("input")); }; 3226 rec.onend = () => { rec = null; b.classList.remove("on"); els.input.focus(); }; 3227 rec.onerror = () => { rec = null; b.classList.remove("on"); }; 3228 b.classList.add("on"); rec.start(); 3229 }); 3230 })(); 3231 $("webBtn").addEventListener("click", () => { const on = $("webBtn").getAttribute("aria-pressed") !== "true"; $("webBtn").setAttribute("aria-pressed", String(on)); els.composer.classList.toggle("web-on", on); toast(T_(on ? "DeepSearch Ø´ØºÙØ§Ù â Ø±Ø ÙØ¯ÙÙØ± ÙÙÙØ±Ø£ ØµÙØØ§Øª ÙÙÙØ±Ø¬ÙÙ ØµÙØ±" : "DeepSearch ٠تÙÙÙ")); }); 3232 // bridge for hythm-voice.js (voice conversations) 3233 window.HythmVoiceHost = { 3234 uid: () => uid, 3235 model: () => model, 3236 login: () => openLogin(), 3237 history: () => chatHistory(), 3238 persona: () => ({ tone: appSettings.tone, dialect: appSettings.dialect, format: appSettings.format, emoji: "none", instructions: (appSettings.instructions || "").slice(0, 1000) }), 3239 setCredits: (c) => setCredits(c, true), 3240 toast: (t) => toast(t), 3241 addExchange: (u, a) => { // the spoken turn is saved in the chat like any other message 3242 const ue = { type: "user", text: u, mode, model, styleLabel: "ð" }; renderEntry(ue); pushLog(ue); 3243 const ce = { type: "chat", md: a, meta: { cost: 1 } }; const d = renderEntry(ce); d.classList.add("settled"); pushLog(ce); 3244 els.app.classList.remove("is-empty"); updateLayout(); scrollDown(); 3245 } 3246 }; 3247 // real-time voice (Gemini Live, like ChatGPT/Gemini) when the server has it; otherwise the step-by-step voice 3248 $("voiceBtn").addEventListener("click", () => (cfg.liveVoice && window.HythmLive ? window.HythmLive.open() : window.HythmVoice && window.HythmVoice.open())); 3249 $("thinkBtn").addEventListener("click", () => setMode(mode === "max" ? (storage.get("hythm.calmMode") || "balanced") : "max")); 3250 // header: Chat | Images 3251 function setView(v) { 3252 els.app.classList.toggle("images-view", v === "images"); 3253 document.querySelectorAll("[data-hview]").forEach((b) => b.setAttribute("aria-selected", String(b.dataset.hview === v))); 3254 $("imagesPage").hidden = v !== "images"; 3255 if (v === "images" && task !== "image") toggleImageMode(true); 3256 if (v === "chat" && task === "image") toggleImageMode(false); 3257 } 3258 document.querySelectorAll("[data-hview]").forEach((b) => b.addEventListener("click", () => { if (session.log.length) $("newChat").click(); setView(b.dataset.hview); })); 3259 $("imagesNav").addEventListener("click", () => { closeSidebar(); if (session.log.length) $("newChat").click(); setView("images"); }); 3260 $("newChat").addEventListener("click", () => { if (els.app.classList.contains("images-view") && task !== "image") setView("chat"); }); 3261 // home actions 3262 document.querySelectorAll(".home-action").forEach((b) => b.addEventListener("click", () => { 3263 const a = b.dataset.act; 3264 if (a === "image") return setView("images"); 3265 if (a === "analyze") return $("attachBtn").click(); 3266 els.input.value = a === "design" ? T_("ص٠٠٠ÙÙØ¹ ") : T_("اشرØÙÙ "); 3267 autoGrow(); els.input.focus(); els.input.dispatchEvent(new Event("input")); 3268 })); 3269 // images page templates 3270 // trending cards show real pictures made by NanoSpark (same files as the style previews) 3271 document.querySelectorAll(".ip-card[data-style]").forEach((c) => { 3272 const name = /Ø¨ÙØ³ØªØ±/.test(c.dataset.prompt || "") ? "poster" : c.dataset.style; 3273 const im = new Image(); 3274 im.onload = () => { c.style.backgroundImage = "url(/styles/" + name + ".jpg)"; c.classList.add("has-img"); }; 3275 im.src = "/styles/" + name + ".jpg"; 3276 }); 3277 document.querySelectorAll(".ip-card").forEach((c) => c.addEventListener("click", () => { 3278 if (c.dataset.ip === "upload") return $("attachBtn").click(); 3279 document.querySelector('[data-img-set="style"] button[data-v="' + c.dataset.style + '"]')?.click(); 3280 if (task !== "image") toggleImageMode(true); 3281 els.input.value = T_(c.dataset.prompt || ""); autoGrow(); els.input.focus(); 3282 try { els.input.setSelectionRange(els.input.value.length, els.input.value.length); } catch {} 3283 })); 3284 // settings: search + custom instructions 3285 $("setSearch").addEventListener("input", (e) => { 3286 const q = e.target.value.trim().toLowerCase(); 3287 const panels = document.querySelectorAll("[data-spanel]"); 3288 if (!q) { document.querySelector("[data-stab].on")?.click(); document.querySelectorAll(".set-row,.set-col").forEach((r) => (r.hidden = false)); return; }
3289 panels.forEach((p) => { 3290 let any = false; 3291 p.querySelectorAll(".set-row,.set-col").forEach((r) => { const hit = r.textContent.toLowerCase().includes(q); r.hidden = !hit; any = any || hit; }); 3292 p.hidden = !any; 3293 }); 3294 }); 3295 let instrTimer = 0; 3296 $("customInstr").addEventListener("input", (e) => { 3297 clearTimeout(instrTimer); 3298 instrTimer = setTimeout(() => { appSettings.instructions = e.target.value.slice(0, 1000); storage.set("hythm.settings", JSON.stringify(appSettings)); toast(T_("ØªÙ Ø§ÙØÙØ¸")); }, 600); 3299 }); 3300 $("logoutBtn").addEventListener("click", () => { 3301 if (!confirm(window.HythmI18n ? HythmI18n.t("Ù ØªØ£ÙØ¯Ø ØØ³Ø§Ø¨Ù ÙØ§Ø³ØªØ®Ø¯Ø§Ù Ù Ù Ø±Ø¨ÙØ·ÙÙ Ø¨ÙØ§ÙØ¬ÙØ§Ø²Ø ÙØ¥Ø°Ø§ Ø·ÙØ¹Øª ٠ا Ø±Ø ØªÙØ¯Ø± ترجعÙÙ.") : "")) return; 3302 storage.del("hythm.uid"); projects.forEach((p) => storage.del("hythm.p." + p.id)); storage.del("hythm.projects"); storage.del("hythm.currentProject"); location.reload(); 3303 }); 3304 document.querySelectorAll("[data-acc]").forEach((b) => b.addEventListener("click", () => { 3305 document.querySelectorAll("[data-acc]").forEach((x) => x.setAttribute("aria-selected", String(x === b))); 3306 $("accMe").hidden = b.dataset.acc !== "me"; 3307 $("accAdmin").hidden = b.dataset.acc !== "admin"; 3308 if (b.dataset.acc === "admin" && sessionStorage.getItem("hythm.admin")) loadAdmin(); 3309 })); 3310 3311 /* ---------------- Admin ---------------- */ 3312 const adminToken = () => { try { return sessionStorage.getItem("hythm.admin") || ""; } catch { return ""; } }; 3313 $("adminForm").addEventListener("submit", async (e) => { 3314 e.preventDefault(); 3315 try { sessionStorage.setItem("hythm.admin", $("adminToken").value); } catch {} 3316 loadAdmin(); 3317 }); 3318 async function loadAdmin() { 3319 const { ok, data } = await api(`/api/admin/overview?token=${encodeURIComponent(adminToken())}`); 3320 if (!ok) { toast("ر٠ز Ø§ÙØ¥Ø¯Ø§Ø±Ø© ØºÙØ·"); try { sessionStorage.removeItem("hythm.admin"); } catch {} return; } 3321 $("adminForm").hidden = true; 3322 $("adminPanel").hidden = false; 3323 $("admSite").textContent = data.site.credits.toLocaleString("en"); 3324 $("admStats").innerHTML = `${Object.entries(data.site.models || {}).map(([k, n]) => `<span>${MODEL_NAMES[k] || k}: ${n} Ø·ÙØ¨</span>`).join("")}${Object.entries(data.site.feedback || {}).map(([k, f]) => `<span>${k} · ð${f.up} ð${f.down}</span>`).join("")}<span>${data.site.generations} تص٠ÙÙ ÙØ§Ø¬Ø</span><span>${data.site.spent} ÙØ±Ùدت ٠صرÙÙ</span><span>${data.site.blocked} Ø·ÙØ¨ ٠رÙÙØ¶</span>`; 3325 $("admUsers").innerHTML = data.users.map((u) => `<tr><td>${esc(u.name)}</td><td>${u.credits}</td><td>${u.generations}</td><td>${u.spent}</td><td><button class="btn-soft" data-give="${u.uid}" data-amt="10">+10</button><button class="btn-soft" data-give="${u.uid}" data-amt="50">+50</button></td></tr>`).join(""); 3326 $("admUsers").querySelectorAll("[data-give]").forEach((b) => b.addEventListener("click", () => adminCredit(b.dataset.give, b.dataset.amt))); 3327 } 3328 async function adminCredit(target, amount) { 3329 const { ok } = await api("/api/admin/credits", { token: adminToken(), target, amount: Number(amount) }); 3330 if (!ok) return toast("٠ا زبطت Ø§ÙØ¹Ù ÙÙØ©"); 3331 toast("ت٠ت Ø¥Ø¶Ø§ÙØ© اÙÙØ±Ùدت"); 3332 loadAdmin(); 3333 if (target === uid) refreshMe(true); 3334 } 3335 document.querySelectorAll("[data-site]").forEach((b) => b.addEventListener("click", () => adminCredit("site", b.dataset.site))); 3336 3337 /* ---------------- Boot ---------------- */ 3338 (async function boot() { 3339 const [c, css, rt] = await Promise.all([ 3340 api("/api/config"), 3341 fetch("hythm-base.css").then((r) => r.text()).catch(() => ""), 3342 fetch("hythm-runtime.js").then((r) => r.text()).catch(() => "") 3343 ]); 3344 if (c.ok) cfg = c.data; 3345 baseCss = css; runtimeJs = rt; 3346 // a new version was published while this page was open â reload quietly when the user isn't busy 3347 setInterval(async () => { 3348 if (document.hidden) return; 3349 const r = await api("/api/config"); 3350 if (!r.ok || !r.data.build || !cfg.build || r.data.build === cfg.build) return; 3351 if (controller || els.input.value.trim() || document.querySelector(".img-dream") || document.querySelector("dialog[open]")) return; 3352 location.reload(); 3353 }, 120000); 3354 for (const [k, m] of Object.entries(cfg.modes)) { 3355 const b = document.querySelector(`.mode[data-mode="${k}"]`); 3356 if (b) { b.querySelector(".mode-cost").textContent = m.cost; b.title = `${m.label}: تص٠ÙÙ ${m.cost} · تعدÙÙ ${m.editCost} ÙØ±Ùدت`; } 3357 } 3358 setMode(mode); 3359 3360 // favicon: the same cloud logo as everywhere else (public/hythm-logo.svg) 3361 3362 // restore session 3363 session.log = session.log.filter((e) => { try { renderEntry(e); return true; } catch (err) { Shield.report("restore", err); return false; } }); 3364 if (session.current >= 0 && session.versions[session.current]) selectVersion(session.current, { silent: true }); 3365 updateSendCost(); renderProjects(); updateLayout(); 3366 scrollDown(); 3367 3368 // the Space may still be waking up (503/offline): wait and retry instead of throwing the user out 3369 let okMe = await refreshMe(); 3370 for (let i = 0; !okMe && uid && i < 8 && refreshMe.lastStatus !== 401; i++) { 3371 if (i === 0) toast((window.HythmI18n ? HythmI18n.t : (x) => x)("Hythm Ø¹Ù ÙØµØÙâ¦ ÙØØ¸Ø©")); 3372 await new Promise((r) => setTimeout(r, 2500)); 3373 okMe = await refreshMe(); 3374 } 3375 if (!okMe) { if (!(await recoverAccount())) { uid = null; storage.del("hythm.uid"); openLogin(); } } 3376 })(); 3377})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.