PageSourceSearch

https://adam21234-hythm.hf.space/app.js

js adam21234-hythm.hf.space collected 2026-10-03 16:37:06 UTC 262,167 bytes, 3,377 lines download raw bytes

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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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.