Download index.html from bh-ab/Deepmind: direct link, hf CLI and curl.
- Browser
- Download file 61.6 kB
-
https://huggingface.co/spaces/bh-ab/Deepmind/resolve/main/index.html
- Command line
-
hf download hf://spaces/bh-ab/Deepmind/index.html
-
curl -L -o index.html https://huggingface.co/spaces/bh-ab/Deepmind/resolve/main/index.html
61.6 kB
| <html lang="ar" dir="rtl" data-theme="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"> | |
| <meta name="color-scheme" content="dark light"> | |
| <title>AI Studio — دردشة محلية</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root,[data-theme=dark]{--bg:#0e1014;--s1:#161920;--s2:#1e222b;--s3:#292e39;--bd:#2a2f3a;--tx:#eceef3;--mu:#8d95a5;--ac:#e8785a;--ac2:#f0a06b;--ok:#4ade80;--er:#fb7185;--wr:#fbbf24;--code:#0a0c10;--sh:0 10px 40px rgba(0,0,0,.35);color-scheme:dark} | |
| [data-theme=light]{--bg:#f5f5f2;--s1:#fff;--s2:#efeee9;--s3:#e4e2db;--bd:#d9d7cf;--tx:#1d1e1c;--mu:#6c6f68;--ac:#cf6849;--ac2:#e08a55;--ok:#16a34a;--er:#dc2626;--wr:#d97706;--code:#f3f3ee;--sh:0 10px 40px rgba(0,0,0,.12);color-scheme:light} | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| html,body{height:100%} | |
| body{background:var(--bg);color:var(--tx);font:15px/1.85 "Tajawal","Segoe UI",Tahoma,Arial,sans-serif;display:flex;overflow:hidden} | |
| button,input,select,textarea{font:inherit;color:inherit} | |
| button{cursor:pointer;-webkit-tap-highlight-color:transparent} | |
| ::-webkit-scrollbar{width:7px;height:7px}::-webkit-scrollbar-thumb{background:var(--s3);border-radius:9px} | |
| .ib,.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:.15s} | |
| .ib{width:36px;height:36px;border:1px solid transparent;border-radius:10px;background:none;color:var(--mu);flex:none;font-size:15px} | |
| .ib:hover{background:var(--s2);border-color:var(--bd);color:var(--tx)} | |
| .btn{padding:10px 16px;border:0;border-radius:12px;background:linear-gradient(135deg,var(--ac),var(--ac2));color:#fff;font-weight:700;font-size:13.5px} | |
| .btn:hover{filter:brightness(1.08);transform:translateY(-1px)}.btn.g{background:var(--s2);border:1px solid var(--bd);color:var(--tx);font-weight:500} | |
| #ov{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:40;display:none;backdrop-filter:blur(2px)}#ov.show{display:block} | |
| /* sidebar */ | |
| #sb{width:272px;background:var(--s1);border-left:1px solid var(--bd);display:flex;flex-direction:column;flex:none;z-index:50;transition:transform .25s} | |
| .brand{display:flex;align-items:center;gap:10px;padding:16px 16px 6px;font-weight:700;font-size:16px} | |
| .brand i{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,var(--ac),var(--ac2));display:grid;place-items:center;font-style:normal;color:#fff;font-size:15px} | |
| #sb .top{padding:10px 12px}#sb .top .btn{width:100%} | |
| #ss{flex:1;overflow-y:auto;padding:4px 9px;min-height:0} | |
| .se{display:flex;align-items:center;gap:8px;padding:9px 11px;border-radius:11px;color:var(--mu);font-size:13.5px;margin-bottom:2px;cursor:pointer;transition:.15s} | |
| .se:hover{background:var(--s2)}.se.on{background:var(--s3);color:var(--tx)} | |
| .se span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .se .x{background:none;border:0;color:var(--mu);opacity:0}.se:hover .x{opacity:1}.se .x:hover{color:var(--er)} | |
| #sb .ft{padding:10px 14px;border-top:1px solid var(--bd);display:flex;justify-content:space-between;align-items:center;font-size:11.5px;color:var(--mu)} | |
| /* top bar */ | |
| main{flex:1;display:flex;flex-direction:column;min-width:0;position:relative} | |
| #tb{display:flex;align-items:center;gap:6px;padding:8px 12px;background:color-mix(in srgb,var(--s1) 88%,transparent);backdrop-filter:blur(10px);border-bottom:1px solid var(--bd);flex-wrap:wrap;z-index:5} | |
| #hb{display:none} | |
| #mp{flex:0 1 330px;max-width:46vw;padding:8px 10px;border-radius:11px;border:1px solid var(--bd);background:var(--s2);font-size:12.5px} | |
| .sp{flex:1} | |
| .pill{font-size:11.5px;color:var(--mu);display:flex;align-items:center;gap:6px;background:var(--s2);border:1px solid var(--bd);padding:5px 11px;border-radius:20px;white-space:nowrap} | |
| .led{width:8px;height:8px;border-radius:50%;background:var(--mu)}.led.on{background:var(--ok);box-shadow:0 0 8px var(--ok)}.led.wr{background:var(--wr);animation:pl 1s infinite}.led.er{background:var(--er)} | |
| @keyframes pl{50%{opacity:.4}} | |
| /* load progress */ | |
| #lp{display:none;padding:9px 16px 11px;background:var(--s1);border-bottom:1px solid var(--bd)}#lp.on{display:block} | |
| .lpt{display:flex;justify-content:space-between;gap:10px;font-size:12px;margin-bottom:7px;flex-wrap:wrap} | |
| .lpt b{color:var(--ac)}.lpt span{color:var(--mu);direction:rtl} | |
| .bar{height:7px;background:var(--s3);border-radius:9px;overflow:hidden} | |
| .bar i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--ac),var(--ac2));border-radius:9px;transition:width .6s ease} | |
| #lp.ind .bar i{width:35%!important;animation:ind 1.3s ease-in-out infinite} | |
| @keyframes ind{0%{margin-right:0}50%{margin-right:65%}100%{margin-right:0}} | |
| /* chat */ | |
| #chat{flex:1;overflow-y:auto;padding:20px 14px 8px;min-height:0;scroll-behavior:smooth} | |
| #ci{max-width:820px;margin:0 auto;display:flex;flex-direction:column;gap:16px} | |
| .welcome{text-align:center;color:var(--mu);padding:80px 20px}.welcome h2{color:var(--tx);font-size:24px;margin-bottom:8px} | |
| .sug{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:18px} | |
| .sug button{background:var(--s1);border:1px solid var(--bd);border-radius:20px;padding:7px 14px;font-size:12.5px;color:var(--mu)}.sug button:hover{color:var(--tx);border-color:var(--ac)} | |
| .msg{max-width:94%;word-break:break-word;animation:fi .25s ease} | |
| @keyframes fi{from{opacity:0;transform:translateY(6px)}} | |
| .msg.user{align-self:flex-start;background:var(--s3);padding:10px 16px;border-radius:20px 20px 20px 6px} | |
| .msg.bot{align-self:stretch;padding:2px 4px;position:relative} | |
| .msg .files{font-size:11px;color:var(--mu);margin-top:5px} | |
| .msg h1,.msg h2,.msg h3{margin:12px 0 6px;font-size:1.12em} | |
| .msg ul{margin:6px 22px 6px 0}.msg a{color:var(--ac)} | |
| .msg blockquote{border-right:3px solid var(--ac);padding-right:12px;color:var(--mu);margin:8px 0} | |
| .stt{color:var(--mu);font-size:12.5px;display:flex;align-items:center;gap:8px} | |
| .stt::before{content:"";width:12px;height:12px;border:2px solid var(--s3);border-top-color:var(--ac);border-radius:50%;animation:sp .8s linear infinite} | |
| @keyframes sp{to{transform:rotate(360deg)}} | |
| .wr2{color:var(--mu);font-size:12px;margin:8px 0} | |
| details.think{margin:6px 0;border:1px dashed var(--bd);border-radius:10px;padding:6px 12px;color:var(--mu);font-size:13px} | |
| details.think summary{cursor:pointer;color:var(--ac);font-size:12px} | |
| .stats{font-size:11px;color:var(--mu);margin-top:4px} | |
| .ma{display:flex;gap:4px;margin-top:5px;opacity:.5;flex-wrap:wrap}.ma:hover{opacity:1} | |
| .ma button{background:none;border:1px solid transparent;border-radius:8px;color:var(--mu);font-size:11.5px;padding:2px 9px;display:inline-flex;align-items:center;gap:4px} | |
| .ma button:hover{background:var(--s2);border-color:var(--bd);color:var(--tx)} | |
| .ma button.on{color:var(--ac);border-color:var(--ac)} | |
| .cw{margin:10px 0;border:1px solid var(--bd);border-radius:12px;overflow:hidden;background:var(--code)} | |
| .ch{display:flex;justify-content:space-between;align-items:center;padding:5px 12px;background:var(--s2);font-size:11px;color:var(--mu);direction:ltr;gap:6px;flex-wrap:wrap} | |
| .ch button{background:none;border:0;color:var(--mu);font-size:11px;display:inline-flex;align-items:center;gap:4px} | |
| .ch button.open-btn{color:var(--ac)} | |
| pre{margin:0;padding:13px;overflow-x:auto;direction:ltr;text-align:left;font:12.5px/1.65 Consolas,"Courier New",monospace;color:var(--tx)} | |
| code.in{background:var(--s2);padding:1px 6px;border-radius:6px;font-size:.9em;direction:ltr;display:inline-block} | |
| .artifact{display:flex;gap:12px;align-items:center;width:100%;margin:10px 0;padding:11px 13px;background:var(--s1);border:1px solid var(--bd);border-radius:14px;color:var(--tx);text-align:right;transition:.15s} | |
| .artifact:hover{border-color:var(--ac);transform:translateY(-1px)} | |
| .artifact .ai{width:40px;height:40px;border-radius:11px;background:var(--s2);display:grid;place-items:center;flex:none} | |
| .artifact b{display:block;font-size:13px;direction:ltr;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .artifact small{color:var(--mu);font-size:11px} | |
| /* composer */ | |
| #cmp{padding:8px 12px calc(12px + env(safe-area-inset-bottom))} | |
| #cmp .in{max-width:820px;margin:0 auto;background:var(--s1);border:1px solid var(--bd);border-radius:22px;padding:10px 12px;box-shadow:var(--sh);transition:.15s} | |
| #cmp .in:focus-within{border-color:var(--ac)} | |
| #chips{display:flex;gap:6px;flex-wrap:wrap}#chips:not(:empty){margin-bottom:7px} | |
| .chip{background:var(--s2);border:1px solid var(--bd);border-radius:20px;padding:3px 10px;font-size:11px;display:flex;gap:6px;align-items:center} | |
| .chip button{background:none;border:0;color:var(--mu)} | |
| #inp{width:100%;background:none;border:0;outline:0;resize:none;max-height:170px;line-height:1.6;padding:4px 2px} | |
| .cr{display:flex;align-items:center;gap:6px;margin-top:4px} | |
| #snd,#stp{width:40px;height:40px;border-radius:13px;border:0;background:linear-gradient(135deg,var(--ac),var(--ac2));color:#fff;font-size:17px;align-items:center;justify-content:center} | |
| #stp{display:none;background:var(--s2);border:1px solid var(--bd);color:var(--er)} | |
| #pw{position:relative} | |
| #pp{position:absolute;bottom:48px;right:0;width:min(300px,calc(100vw - 30px));background:var(--s1);border:1px solid var(--bd);border-radius:16px;box-shadow:var(--sh);display:none;z-index:60;overflow:hidden} | |
| #pp.open{display:block} | |
| .pi{display:flex;gap:10px;align-items:center;width:100%;padding:12px 14px;background:none;border:0;text-align:right;font-size:13px}.pi:hover{background:var(--s2)} | |
| .sw{position:relative} | |
| #mic.on{color:var(--er);border-color:var(--er);animation:pl 1.2s infinite} | |
| #spk.on,#vcb.on{color:var(--ac);border-color:var(--ac)} | |
| /* voice */ | |
| #vc{position:fixed;inset:0;z-index:95;background:radial-gradient(circle at 50% 35%,var(--s2),var(--bg) 70%);display:none;flex-direction:column;align-items:center;justify-content:space-between;padding:calc(16px + env(safe-area-inset-top)) 20px calc(26px + env(safe-area-inset-bottom))} | |
| #vc.on{display:flex}.vx{align-self:flex-start} | |
| .vm{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;width:100%;max-width:560px;text-align:center;min-height:0} | |
| #orb{width:150px;height:150px;flex:none;border-radius:50%;background:radial-gradient(circle at 35% 30%,rgba(255,255,255,.35),var(--ac));position:relative;cursor:pointer;box-shadow:0 0 60px rgba(224,111,79,.4)} | |
| #orb i{position:absolute;inset:0;border-radius:50%;border:2px solid var(--ac);opacity:0} | |
| #vc.listening #orb{animation:vb 1.6s ease-in-out infinite} | |
| #vc.listening #orb i{animation:vr 1.8s ease-out infinite}#vc.listening #orb i:nth-child(2){animation-delay:.6s}#vc.listening #orb i:nth-child(3){animation-delay:1.2s} | |
| #vc.thinking #orb{animation:vt 1.1s linear infinite;border-radius:42% 58% 55% 45%} | |
| #vc.speaking #orb{animation:vs .45s ease-in-out infinite alternate} | |
| #vc.muted #orb{opacity:.35;filter:grayscale(1)} | |
| @keyframes vb{50%{transform:scale(1.07)}}@keyframes vr{0%{transform:scale(1);opacity:.6}100%{transform:scale(1.9);opacity:0}}@keyframes vt{to{transform:rotate(360deg)}}@keyframes vs{to{transform:scale(1.15)}} | |
| #vS{font-size:17px;font-weight:700}#vT{color:var(--mu);min-height:26px;font-size:15px}#vR{font-size:14.5px;max-height:26vh;overflow-y:auto;line-height:1.9;width:100%} | |
| .vb{display:flex;gap:12px;align-items:center} | |
| #toasts{position:fixed;bottom:100px;left:50%;transform:translateX(-50%);z-index:100;display:flex;flex-direction:column;gap:7px;align-items:center;pointer-events:none} | |
| .toast{background:var(--s3);border:1px solid var(--bd);padding:9px 16px;border-radius:12px;font-size:13px;box-shadow:var(--sh);animation:fi .2s} | |
| /* modals */ | |
| .md{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;z-index:90;align-items:center;justify-content:center;padding:14px;backdrop-filter:blur(3px)}.md.open{display:flex} | |
| .card{background:var(--s1);border:1px solid var(--bd);border-radius:20px;width:min(560px,100%);max-height:92vh;overflow-y:auto;padding:18px;box-shadow:var(--sh)} | |
| .card h2{font-size:16px;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center;gap:8px} | |
| .sec{border:1px solid var(--bd);border-radius:14px;padding:12px;margin-bottom:11px} | |
| .sec>b{display:block;font-size:12.5px;margin-bottom:9px;color:var(--ac)} | |
| .fd{margin-bottom:9px}.fd label{display:block;font-size:11.5px;color:var(--mu);margin-bottom:4px} | |
| .fd input,.fd select,.fd textarea{width:100%;padding:9px 11px;border-radius:10px;border:1px solid var(--bd);background:var(--s2);font-size:13px} | |
| .fd input[type=checkbox]{width:auto;margin-left:6px} | |
| .fd textarea{resize:vertical;min-height:70px} | |
| .g3{display:grid;grid-template-columns:repeat(3,1fr);gap:8px} | |
| /* code panel */ | |
| #cp{position:fixed;top:0;left:0;bottom:0;width:55vw;max-width:100vw;background:var(--s1);border-right:1px solid var(--bd);display:none;flex-direction:column;z-index:80;box-shadow:var(--sh)} | |
| #cp.open{display:flex}#cp.full{width:100vw} | |
| .cph{display:flex;align-items:center;gap:5px;padding:7px 10px;border-bottom:1px solid var(--bd);flex-shrink:0;overflow-x:auto} | |
| .cph .tt{flex:1;font-size:12.5px;font-weight:700;direction:ltr;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:70px} | |
| .tabs{display:flex;background:var(--s2);border:1px solid var(--bd);border-radius:9px;overflow:hidden;flex:none} | |
| .tabs button{background:none;border:0;color:var(--mu);padding:5px 11px;font-size:11.5px}.tabs button.on{background:var(--ac);color:#fff} | |
| .cpb{flex:1;display:flex;min-height:0;position:relative} | |
| .cc,.cv{flex:1 1 0;min-width:0;min-height:0;display:none} | |
| #cp[data-v=code] .cc,#cp[data-v=preview] .cv,#cp[data-v=split] .cc,#cp[data-v=split] .cv{display:block} | |
| #cp[data-v=split] .cc{border-left:1px solid var(--bd)} | |
| .cc{overflow:auto;background:var(--code);direction:ltr}.cc pre{white-space:pre-wrap;word-break:break-word} | |
| .cv{position:relative;background:repeating-conic-gradient(var(--s2) 0 25%,var(--s1) 0 50%) 0 0/20px 20px;overflow:auto} | |
| .cv iframe{position:absolute;top:0;bottom:0;left:50%;transform:translateX(-50%);width:100%;border:0;background:#fff;transition:width .25s} | |
| #cp[data-d=t] .cv iframe{width:768px;max-width:100%}#cp[data-d=m] .cv iframe{width:390px;max-width:100%} | |
| #cp[data-d=t] .cv iframe,#cp[data-d=m] .cv iframe{box-shadow:0 0 0 1px var(--bd),var(--sh)} | |
| #pe{position:absolute;bottom:0;left:0;right:0;background:#7f1d1d;color:#fecaca;font-size:11px;padding:7px 11px;direction:ltr;text-align:left;max-height:90px;overflow:auto;display:none;z-index:5;cursor:pointer} | |
| .ld{position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--ac),var(--ac2));z-index:6;display:none;animation:ind 1s ease-in-out infinite;width:40%} | |
| #cp.writing .ld{display:block} | |
| @media(max-width:768px){#sb{position:fixed;top:0;bottom:0;right:0;transform:translateX(100%)}#sb.open{transform:none}#hb{display:inline-flex}.msg{max-width:100%}.g3{grid-template-columns:1fr}#mp{flex:1 1 100%;max-width:none;order:2}#cp{width:100vw}.dv{display:none!important}} | |
| @media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="ov" onclick="closeAll()"></div> | |
| <aside id="sb"> | |
| <div class="brand"><i>✦</i>AI Studio</div> | |
| <div class="top"><button class="btn" onclick="newChat()">+ محادثة جديدة</button></div> | |
| <div id="ss"></div> | |
| <div class="ft"><span>محفوظة محلياً في المتصفح</span><button class="ib" id="thb" onclick="toggleTheme()">☀</button></div> | |
| </aside> | |
| <main> | |
| <div id="tb"> | |
| <button class="ib" id="hb" onclick="event.stopPropagation();toggleSb()">☰</button> | |
| <select id="mp" onchange="onModelChange()"></select> | |
| <div class="pill"><span class="led" id="led"></span><span id="ledT">…</span></div> | |
| <div class="sp"></div> | |
| <button class="ib" onclick="doLoad()" title="تحميل النموذج">⬇</button> | |
| <button class="ib" onclick="doUnload()" title="تفريغ النموذج من الذاكرة">⏏</button> | |
| <button class="ib" onclick="openAdd()" title="إضافة نموذج من Hugging Face">➕</button> | |
| <button class="ib" id="spk" onclick="toggleAuto()" title="النطق التلقائي"></button> | |
| <button class="ib" onclick="openDiag()" title="التشخيص">🩺</button> | |
| <button class="ib" onclick="openSet()" title="الإعدادات">⚙</button> | |
| <button class="ib dv" onclick="exportChat()" title="تصدير المحادثة">⤓</button> | |
| <button class="ib dv" onclick="clearChat()" title="مسح المحادثة">🧹</button> | |
| </div> | |
| <div id="lp"><div class="lpt"><b id="lpA"></b><span id="lpB"></span></div><div class="bar"><i id="lpF"></i></div></div> | |
| <div id="chat"><div id="ci"></div></div> | |
| <div id="cmp"><div class="in"> | |
| <div id="chips"></div> | |
| <textarea id="inp" rows="1" placeholder="اكتب رسالتك..." onkeydown="if(event.key==='Enter'&&!event.shiftKey&&!event.isComposing){event.preventDefault();sendMsg()}"></textarea> | |
| <div class="cr"> | |
| <div id="pw"><button class="ib" style="font-size:20px;border:1px solid var(--bd)" onclick="event.stopPropagation();$('pp').classList.toggle('open')">+</button> | |
| <div id="pp" onclick="event.stopPropagation()"><button class="pi" onclick="$('fi').click();closeAll()">📎 إرفاق صور وملفات</button></div> | |
| </div> | |
| <button class="ib" id="mic" onclick="toggleMic()" title="تفريغ صوتي">🎤</button> | |
| <button class="ib" id="vcb" onclick="startVoice()" title="محادثة صوتية">〰</button> | |
| <div class="sp"></div> | |
| <button id="stp" onclick="stopGen()">■</button><button id="snd" onclick="sendMsg()">↑</button> | |
| </div> | |
| </div></div> | |
| </main> | |
| <input type="file" id="fi" multiple hidden onchange="addFiles(this.files)"> | |
| <div id="cp" data-v="code" data-d="d"> | |
| <div class="ld"></div> | |
| <div class="cph"> | |
| <button class="ib" onclick="closePanel()">✕</button><span class="tt" id="cpT"></span> | |
| <div class="tabs"><button data-v="code" class="on" onclick="setView('code')">الكود</button><button data-v="preview" onclick="setView('preview')">معاينة</button><button data-v="split" onclick="setView('split')">مقسّم</button></div> | |
| <div class="tabs dv2"><button data-d="d" class="on" onclick="setDev('d')" title="سطح المكتب">🖥</button><button data-d="t" onclick="setDev('t')" title="تابلت">▭</button><button data-d="m" onclick="setDev('m')" title="جوال">📱</button></div> | |
| <button class="ib" onclick="reloadPrev()" title="تحديث">↻</button> | |
| <button class="ib" onclick="newTab()" title="تبويب جديد">↗</button> | |
| <button class="ib" onclick="copyArt()" title="نسخ">⧉</button> | |
| <button class="ib" onclick="dlArt()" title="تنزيل">↓</button> | |
| <button class="ib" onclick="$('cp').classList.toggle('full')" title="ملء الشاشة">⛶</button> | |
| </div> | |
| <div class="cpb"> | |
| <div class="cv"><iframe id="cf" sandbox="allow-scripts allow-modals allow-forms allow-popups"></iframe><div id="pe" onclick="this.style.display='none'"></div></div> | |
| <div class="cc" id="cpS"><pre id="cpC"></pre></div> | |
| </div> | |
| </div> | |
| <div id="vc"> | |
| <button class="ib vx" onclick="endVoice()">✕</button> | |
| <div class="vm"><div id="orb" onclick="vTap()"><i></i><i></i><i></i></div><div id="vS"></div><div id="vT"></div><div id="vR"></div></div> | |
| <div class="vb"><button class="ib" id="vMute" onclick="vMuteTog()" style="border:1px solid var(--bd);width:46px;height:46px">🎤</button><button class="btn" onclick="endVoice()">إنهاء المحادثة</button></div> | |
| </div> | |
| <div id="toasts"></div> | |
| <div id="set" class="md" onclick="if(event.target===this)this.classList.remove('open')"><div class="card"> | |
| <h2>الإعدادات <button class="ib" onclick="$('set').classList.remove('open')">✕</button></h2> | |
| <div class="sec"><b>🧠 تعليمات النظام</b><div class="fd"><textarea id="cSys" placeholder="You are a helpful assistant..."></textarea></div></div> | |
| <div class="sec"><b>🎛 التوليد</b><div class="g3"> | |
| <div class="fd"><label>Temperature</label><input id="cTemp" type="number" step="0.1" min="0" max="2"></div> | |
| <div class="fd"><label>Max Tokens</label><input id="cMax" type="number" min="32" max="32000"></div> | |
| <div class="fd"><label>Top P</label><input id="cTop" type="number" step="0.05" min="0" max="1"></div></div> | |
| <div class="fd"><label>Max Tokens: <b id="mMax">256</b></label><input id="cMaxRange" type="range" min="32" max="32000" step="32" oninput="$('cMax').value=this.value;$('mMax').textContent=this.value"></div> | |
| </div> | |
| <div class="sec"><b>🔊 النطق</b> | |
| <div class="fd"><label>سرعة النطق: <b id="rV">1.5</b>×</label><input id="cRate" type="range" min="0.8" max="2" step="0.1" oninput="$('rV').textContent=this.value"></div> | |
| <div class="fd"><label>صوت النطق</label><select id="cVoice"></select></div> | |
| <div class="fd"><label><input type="checkbox" id="cAutoSpeak">النطق التلقائي لكل رد</label></div> | |
| <button class="btn g" style="width:100%" onclick="testVoice()">🔊 اختبار الصوت</button> | |
| </div> | |
| <div class="sec"><b>🎤 التفريغ الصوتي</b> | |
| <div class="fd"><label>اللغة</label><select id="cStt"> | |
| <option value="ar-SA">العربية (الفصحى)</option><option value="ar-MA">العربية (المغرب)</option><option value="ar-EG">العربية (مصر)</option><option value="en-US">English</option><option value="fr-FR">Français</option> | |
| </select></div> | |
| </div> | |
| <button class="btn" style="width:100%" onclick="saveSet()">💾 حفظ</button> | |
| </div></div> | |
| <div id="add" class="md" onclick="if(event.target===this)this.classList.remove('open')"><div class="card"> | |
| <h2>➕ إضافة نموذج من Hugging Face <button class="ib" onclick="$('add').classList.remove('open')">✕</button></h2> | |
| <div class="fd"><label>اسم المستودع (user/model)</label><input id="aRepo" dir="ltr" placeholder="Qwen/Qwen2.5-3B-Instruct"></div> | |
| <div class="fd"><label>النوع</label><select id="aKind"><option value="chat">دردشة (chat template)</option><option value="t5">سؤال وجواب (T5)</option><option value="alpaca">إكمال نصي</option></select></div> | |
| <p style="font-size:11.5px;color:var(--mu);margin-bottom:10px">يجب أن يكون النموذج مفتوحاً (غير مقيّد) ويناسب ذاكرة الـ CPU. يُحفظ في متصفحك ويُعاد تسجيله تلقائياً.</p> | |
| <button class="btn" style="width:100%" onclick="addModel()">إضافة وتحديد</button> | |
| </div></div> | |
| <div id="dg" class="md" onclick="if(event.target===this)this.classList.remove('open')"><div class="card"> | |
| <h2>🩺 التشخيص <span><button class="btn g" style="padding:6px 11px;font-size:11px" onclick="copyDiag()">نسخ</button> <button class="ib" onclick="$('dg').classList.remove('open')">✕</button></span></h2> | |
| <pre id="diagB" style="direction:ltr;text-align:left;white-space:pre-wrap;font-size:11px;color:var(--mu);max-height:300px;overflow:auto;background:var(--code);border-radius:8px;padding:8px"></pre> | |
| </div></div> | |
| <script> | |
| const $ = i => document.getElementById(i); | |
| const LS = "ai_studio_gradio_v3"; | |
| const esc = s => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); | |
| const uid = () => (crypto.randomUUID ? crypto.randomUUID() : "id" + Date.now() + Math.random().toString(36).slice(2)); | |
| const J = JSON.stringify, sleep = ms => new Promise(r => setTimeout(r, ms)); | |
| const hash = s => { let h = 5381; for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0; return h.toString(36); }; | |
| const fmtB = n => !n ? "0" : n >= 1e9 ? (n / 1e9).toFixed(2) + "GB" : (n / 1e6).toFixed(n >= 1e8 ? 0 : 1) + "MB"; | |
| const fmtT = s => { s = Math.round(s || 0); return s >= 60 ? Math.floor(s / 60) + "د " + String(s % 60).padStart(2, "0") + "ث" : s + "ث"; }; | |
| /* ==================== Gradio API ==================== */ | |
| let base = "/gradio_api/call"; | |
| async function call(name, args = [], onUpdate) { | |
| const post = () => fetch(`${base}/${name}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: J({ data: args }) }); | |
| let r = await post(); | |
| if (r.status === 404 && base !== "/call") { base = "/call"; r = await post(); } | |
| if (!r.ok) throw new Error(`${name}: ${r.status}`); | |
| const { event_id } = await r.json(); | |
| const res = await fetch(`${base}/${name}/${event_id}`); | |
| if (!res.ok || !res.body) throw new Error(`${name}: ${res.status}`); | |
| const rd = res.body.getReader(), dec = new TextDecoder(); | |
| let buf = "", last; | |
| for (;;) { | |
| const { done, value } = await rd.read(); | |
| if (done) break; | |
| buf += dec.decode(value, { stream: true }); | |
| const parts = buf.split(/\r?\n\r?\n/); buf = parts.pop(); | |
| for (const p of parts) { | |
| let ev = "message", data = ""; | |
| for (const l of p.split(/\r?\n/)) { | |
| if (l.startsWith("event:")) ev = l.slice(6).trim(); | |
| else if (l.startsWith("data:")) data += l.slice(5).trim(); | |
| } | |
| let j = null; try { j = JSON.parse(data); } catch {} | |
| if (ev === "error") throw new Error(typeof j === "string" ? j : "خطأ من الخادم"); | |
| if (ev === "generating" && Array.isArray(j)) { last = j[0]; onUpdate?.(last); } | |
| if (ev === "complete") { const o = Array.isArray(j) ? j[0] : j; if (o !== undefined && o !== null) last = o; return last; } | |
| } | |
| } | |
| if (last === undefined) throw new Error("انتهى الاتصال دون رد"); | |
| return last; | |
| } | |
| /* ==================== Config / Theme / Diag / Toast ==================== */ | |
| let cfg = Object.assign({ | |
| sys: "You are a helpful assistant. Answer concisely, in the same language as the user.", | |
| temp: 0.7, maxTok: 256, topP: 0.9, ttsRate: 1.5, ttsVoice: "", autoSpeak: false, | |
| sttLang: "ar-SA", theme: "dark", model: "qwen25", custom: [], | |
| }, JSON.parse(localStorage.getItem(LS) || "{}")); | |
| const saveCfg = () => localStorage.setItem(LS, J(cfg)); | |
| function applyTheme() { document.documentElement.dataset.theme = cfg.theme; $("thb").textContent = cfg.theme === "dark" ? "☀" : "☾"; } | |
| function toggleTheme() { cfg.theme = cfg.theme === "dark" ? "light" : "dark"; applyTheme(); saveCfg(); } | |
| const DIAG = []; | |
| function logDiag(t) { DIAG.unshift(new Date().toLocaleTimeString("en-GB") + " " + t); if (DIAG.length > 60) DIAG.pop(); } | |
| function openDiag() { $("diagB").textContent = DIAG.join("\n") || "لا سجلات"; $("dg").classList.add("open"); } | |
| function copyDiag() { navigator.clipboard.writeText(DIAG.join("\n")).then(() => toast("📋 تم النسخ"), () => toast("❌ تعذر")); } | |
| function toast(t) { const d = document.createElement("div"); d.className = "toast"; d.textContent = t; $("toasts").appendChild(d); setTimeout(() => d.remove(), Math.max(2500, t.length * 80)); } | |
| /* ==================== Sessions ==================== */ | |
| let S = JSON.parse(localStorage.getItem(LS + "_sessions") || "[]"); | |
| let cid = localStorage.getItem(LS + "_cid") || null; | |
| if (!cid || !S.find(s => s.id === cid)) { | |
| cid = uid(); | |
| S = [{ id: cid, title: "دردشة جديدة", messages: [], artifacts: [], ts: Date.now() }]; | |
| } | |
| const cur = () => S.find(s => s.id === cid); | |
| function persist() { | |
| try { localStorage.setItem(LS + "_sessions", J(S)); localStorage.setItem(LS + "_cid", cid); } | |
| catch (e) { S = S.slice(0, 10); try { localStorage.setItem(LS + "_sessions", J(S)); } catch {} } | |
| } | |
| function renderSessions() { | |
| $("ss").innerHTML = S.slice().sort((a, b) => b.ts - a.ts).map(s => ` | |
| <div class="se ${s.id === cid ? "on" : ""}" onclick="openSession('${s.id}')">💬 <span>${esc(s.title)}</span> | |
| <button class="x" onclick="delSession(event,'${s.id}')">🗑</button></div>`).join(""); | |
| } | |
| function openSession(id) { cid = id; persist(); renderSessions(); renderChat(); closeAll(); } | |
| function delSession(e, id) { | |
| e.stopPropagation(); | |
| if (!confirm("حذف هذه المحادثة؟")) return; | |
| S = S.filter(s => s.id !== id); | |
| if (cid === id) { cid = S[0]?.id; if (!cid) { newChat(); return; } } | |
| persist(); renderSessions(); renderChat(); | |
| } | |
| function newChat() { | |
| const s = { id: uid(), title: "دردشة جديدة", messages: [], artifacts: [], ts: Date.now() }; | |
| S.unshift(s); cid = s.id; persist(); renderSessions(); renderChat(); closeAll(); | |
| } | |
| function clearChat() { | |
| if (!confirm("مسح كل رسائل هذه المحادثة؟")) return; | |
| const s = cur(); if (!s) return; | |
| s.messages = []; s.artifacts = []; s.title = "دردشة جديدة"; | |
| persist(); renderSessions(); renderChat(); | |
| } | |
| /* ==================== Markdown ==================== */ | |
| const qual = (l, n) => l === "html" || l === "svg" || n >= 150; | |
| const prevable = l => l === "html" || l === "svg"; | |
| function md(src, s) { | |
| if (!src) return ""; | |
| const cb = []; | |
| let t = String(src).replace(/```([\w+-]*)\n?([\s\S]*?)```/g, (_, l, c) => { cb.push({ l: (l || "text").toLowerCase(), c }); return "\u0000" + (cb.length - 1) + "\u0000"; }); | |
| let h = esc(t) | |
| .replace(/<think>([\s\S]*?)(?:<\/think>|$)/g, (_, c) => c.trim() ? `<details class="think"><summary>💭 التفكير</summary>${c.trim()}</details>` : "") | |
| .replace(/`([^`\n]+)`/g, '<code class="in">$1</code>') | |
| .replace(/^### (.*)$/gm, "<h3>$1</h3>").replace(/^## (.*)$/gm, "<h2>$1</h2>").replace(/^# (.*)$/gm, "<h1>$1</h1>") | |
| .replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>") | |
| .replace(/^> (.*)$/gm, "<blockquote>$1</blockquote>") | |
| .replace(/^[-*] (.*)$/gm, "<li>$1</li>") | |
| .replace(/(?:<li>.*<\/li>\n?)+/g, m => "<ul>" + m.trim() + "</ul>") | |
| .replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>') | |
| .replace(/\n/g, "<br>") | |
| .replace(/<br>(?=<(?:div|h\d|ul|li|pre|blockquote|button|details))/g, "") | |
| .replace(/(<\/(?:div|h\d|ul|li|pre|blockquote|button|details)>)<br>/g, "$1"); | |
| return h.replace(/(<br>)?\u0000(\d+)\u0000(<br>)?/g, (_, a, i) => { | |
| const { l, c } = cb[+i], pv = prevable(l); | |
| if (s && qual(l, c.length)) { | |
| const id = "a" + hash(c); | |
| let art = (s.artifacts = s.artifacts || []).find(x => x.id === id); | |
| if (!art) { art = { id, lang: l, content: c, title: l === "html" ? "index.html" : l === "svg" ? "image.svg" : "file." + l }; s.artifacts.push(art); } | |
| return `<button class="artifact" onclick="openArtifact('${id}')"><span class="ai">📄</span><span style="flex:1;min-width:0"><b>${esc(art.title)}</b><small>${esc(l.toUpperCase())} · ${c.length} حرف · اضغط للعرض${pv ? " والمعاينة" : ""}</small></span></button>`; | |
| } | |
| return `<div class="cw"><div class="ch"><span>${esc(l)}</span><div style="display:flex;gap:6px"><button onclick="copyCode(this)">نسخ</button>${pv ? `<button class="open-btn" onclick="openCodePanel(this)" data-lang="${esc(l)}">📄 عرض في اللوحة</button>` : ""}</div></div><pre>${esc(c)}</pre></div>`; | |
| }); | |
| } | |
| function copyCode(b) { navigator.clipboard.writeText(b.closest(".cw").querySelector("pre").textContent).then(() => toast("📋 تم النسخ"), () => toast("❌ تعذر")); } | |
| function openCodePanel(b) { | |
| const code = b.closest(".cw").querySelector("pre").textContent, lang = b.dataset.lang || "html", s = cur(); if (!s) return; | |
| const id = "a" + hash(code); | |
| let art = (s.artifacts = s.artifacts || []).find(x => x.id === id); | |
| if (!art) { art = { id, lang, content: code, title: lang === "html" ? "index.html" : "image.svg" }; s.artifacts.push(art); } | |
| persist(); openArtifact(id); | |
| } | |
| // أثناء التوليد: الكتل المغلقة تُعرض عادياً، والكتلة المفتوحة تذهب للوحة مباشرة | |
| function mdStream(t) { | |
| const p = parseLive(t); | |
| if (!p.inside) return md(t, null); | |
| return md(t.slice(0, t.lastIndexOf("```")), null) + '<div class="wr2">✍ جارٍ كتابة الكود في اللوحة…</div>'; | |
| } | |
| /* ==================== Render chat ==================== */ | |
| function addMsg(role, text = "", files = [], idx = null, stats = "") { | |
| $("ci").querySelector(".welcome")?.remove(); | |
| const d = document.createElement("div"); | |
| const bot = role === "assistant"; | |
| d.className = "msg " + (bot ? "bot" : "user"); | |
| d.dataset.raw = text || ""; | |
| d.innerHTML = bot | |
| ? `<div class="bd">${md(text, cur())}</div>${stats}${idx != null ? actions(idx) : ""}` | |
| : esc(text).replace(/\n/g, "<br>") + (files?.length ? `<div class="files">📎 ${files.map(esc).join("، ")}</div>` : ""); | |
| $("ci").appendChild(d); | |
| scrollDown(!bot); | |
| return d; | |
| } | |
| const statsHtml = r => r?.secs ? `<div class="stats">⏱ ${r.secs}ث${r.tokens ? ` · ${r.tokens} توكن · ${(r.tokens / r.secs).toFixed(1)} tok/s` : ""}</div>` : ""; | |
| function actions(idx) { | |
| return `<div class="ma"><button onclick="copyMsg(this)">📋 نسخ</button><button onclick="regenFrom(${idx})">🔄 إعادة</button><button class="spb" onclick="speakMsg(this)">🔊 استمع</button></div>`; | |
| } | |
| function copyMsg(b) { navigator.clipboard.writeText(b.closest(".msg").dataset.raw || "").then(() => toast("📋 تم"), () => toast("❌ تعذر")); } | |
| const SUG = ["اكتب لي صفحة HTML لآلة حاسبة أنيقة", "اشرح لي الذكاء الاصطناعي ببساطة", "لخّص لي فوائد القراءة", "اكتب قصة قصيرة عن رائد فضاء"]; | |
| function renderChat() { | |
| const s = cur(); $("ci").innerHTML = ""; | |
| if (!s || !s.messages.length) { | |
| $("ci").innerHTML = `<div class="welcome"><h2>مرحباً 👋</h2><p>اكتب رسالتك. سيُحمَّل النموذج تلقائياً عند أول رسالة.</p><div class="sug">${SUG.map(x => `<button onclick="$('inp').value=this.textContent;$('inp').focus()">${esc(x)}</button>`).join("")}</div></div>`; | |
| return; | |
| } | |
| s.messages.forEach((m, i) => { | |
| if (m.role === "user") addMsg("user", typeof m.content === "string" ? m.content : "", m._files, i); | |
| else if (m.role === "assistant" && m.content) addMsg("assistant", m.content, null, i, statsHtml(m._st)); | |
| }); | |
| scrollDown(true); | |
| } | |
| async function regenFrom(i) { | |
| if (busy) return; | |
| const s = cur(); let u = i; while (u >= 0 && s.messages[u].role !== "user") u--; | |
| if (u < 0) return; | |
| s.messages = s.messages.slice(0, u + 1); persist(); renderChat(); | |
| await ask(); | |
| } | |
| let follow = true; | |
| $("chat").addEventListener("scroll", () => { const c = $("chat"); follow = c.scrollTop + c.clientHeight >= c.scrollHeight - 80; }); | |
| function scrollDown(f) { const c = $("chat"); if (f || follow) c.scrollTop = c.scrollHeight; } | |
| /* ==================== Code panel + Preview ==================== */ | |
| const L = { t: 0, last: 0, code: "", lang: "html", on: false, closed: false }; | |
| let curArt = null; | |
| function wrap(code, lang) { | |
| const er = `<script>(function(){function r(m){try{parent.postMessage({__pe:1,m:String(m).slice(0,400)},'*')}catch(e){}}addEventListener('error',function(e){r((e.message||'error')+' @'+e.lineno)});addEventListener('unhandledrejection',function(e){r('promise: '+(e.reason&&e.reason.message||e.reason))});var ce=console.error;console.error=function(){r('console.error: '+[].slice.call(arguments).join(' '));ce.apply(console,arguments)}})()<\/script>`; | |
| if (lang === "svg") return '<!doctype html><meta charset="utf-8"><style>html,body{margin:0;height:100%;display:flex;align-items:center;justify-content:center;background:#fff}svg{max-width:100%;max-height:100%}</style>' + code + er; | |
| if (/<head[\s>]/i.test(code)) return code.replace(/<head[^>]*>/i, m => m + er); | |
| if (/<html[\s>]/i.test(code)) return code.replace(/<html[^>]*>/i, m => m + '<head>' + er + '</head>'); | |
| return '<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">' + er + code; | |
| } | |
| addEventListener("message", e => { if (e.data && e.data.__pe) { const b = $("pe"); b.textContent = "⚠ " + e.data.m + " (اضغط للإخفاء)"; b.style.display = "block"; } }); | |
| const prev = (code, lang) => { $("pe").style.display = "none"; $("cf").srcdoc = wrap(code, lang); }; | |
| function setView(v) { $("cp").dataset.v = v; document.querySelectorAll(".tabs button[data-v]").forEach(b => b.classList.toggle("on", b.dataset.v === v)); } | |
| function setDev(d) { $("cp").dataset.d = d; document.querySelectorAll(".tabs button[data-d]").forEach(b => b.classList.toggle("on", b.dataset.d === d)); } | |
| function liveWrite(lang, code) { | |
| const st = !L.on; | |
| L.on = true; L.code = code; L.lang = lang; | |
| if (st) { L.closed = false; L.last = 0; $("cp").classList.add("open"); setView(prevable(lang) ? "split" : "code"); } | |
| $("cp").classList.add("writing"); | |
| $("cpT").textContent = "✍ يكتب (" + lang + ")..."; | |
| const el = $("cpS"), near = el.scrollTop + el.clientHeight >= el.scrollHeight - 40; | |
| $("cpC").textContent = code; | |
| if (near) el.scrollTop = el.scrollHeight; | |
| if (L.closed || !prevable(lang)) return; | |
| clearTimeout(L.t); | |
| if (Date.now() - L.last > 1000) { L.last = Date.now(); prev(code, lang); } | |
| else L.t = setTimeout(() => { L.last = Date.now(); prev(L.code, L.lang); }, 1000); | |
| } | |
| function liveEnd() { | |
| clearTimeout(L.t); $("cp").classList.remove("writing"); | |
| if (L.on && prevable(L.lang) && L.code && !L.closed) prev(L.code, L.lang); | |
| L.on = false; | |
| } | |
| function parseLive(f) { | |
| const p = f.split("```"); | |
| if (p.length % 2 === 0) { | |
| const g = p[p.length - 1], n = g.indexOf("\n"); | |
| if (n < 0) return { inside: true, lang: "text", code: "" }; | |
| return { inside: true, lang: (g.slice(0, n).trim() || "text").toLowerCase(), code: g.slice(n + 1) }; | |
| } | |
| return { inside: false }; | |
| } | |
| function liveCode(t) { const p = parseLive(t); if (p.inside && p.code && (prevable(p.lang) || p.code.length > 150)) liveWrite(p.lang, p.code); else if (L.on) liveEnd(); } | |
| function openArtifact(id) { | |
| const a = cur()?.artifacts?.find(x => x.id === id); | |
| if (!a) return toast("❌ العنصر غير موجود"); | |
| liveEnd(); curArt = a; | |
| $("cpT").textContent = a.title; $("cpC").textContent = a.content; | |
| if (prevable(a.lang)) { prev(a.content, a.lang); setView(innerWidth > 768 ? "split" : "preview"); } | |
| else { $("cf").srcdoc = '<body style="font-family:sans-serif;color:#888;display:flex;align-items:center;justify-content:center;height:100vh">لا معاينة لهذا النوع</body>'; setView("code"); } | |
| $("cp").classList.add("open"); | |
| } | |
| function closePanel() { $("cp").classList.remove("open", "full", "writing"); if (L.on) L.closed = true; clearTimeout(L.t); } | |
| function reloadPrev() { const c = curArt ? curArt.content : L.code, l = curArt ? curArt.lang : L.lang; c ? prev(c, l) : toast("لا محتوى"); } | |
| function newTab() { | |
| const c = curArt ? curArt.content : L.code, l = curArt ? curArt.lang : L.lang; | |
| if (!c) return toast("لا محتوى"); | |
| const w = open("about:blank", "_blank"); if (w) { w.document.write(wrap(c, l)); w.document.close(); } | |
| } | |
| function copyArt() { const c = curArt ? curArt.content : L.code; if (!c) return toast("لا كود"); navigator.clipboard.writeText(c).then(() => toast("📋 تم"), () => toast("❌ تعذر")); } | |
| function dlArt() { | |
| if (!curArt) return toast("لا ملف"); | |
| const a = document.createElement("a"); | |
| a.href = URL.createObjectURL(new Blob([curArt.content], { type: "text/plain;charset=utf-8" })); | |
| a.download = curArt.title; a.click(); toast("⬇ تم"); | |
| } | |
| /* ==================== Models + load progress ==================== */ | |
| let MODELS = {}, MODEL_LIST = [], STATUS = {}, PREV = {}, pollT = null; | |
| async function initModels() { | |
| try { | |
| for (const c of cfg.custom || []) { try { await call("register", [c.repo, c.kind]); } catch {} } | |
| const r = await call("models"); | |
| MODEL_LIST = r.models || []; MODEL_LIST.forEach(m => MODELS[m.id] = m); | |
| renderModelSelect(); await refreshStatus(); | |
| } catch (e) { toast("⚠️ تعذر جلب النماذج: " + e.message); logDiag("initModels: " + e.message); } | |
| } | |
| const ICON = { ready: "✅ ", downloading: "⬇ ", loading: "⚙ ", error: "⚠ " }; | |
| const optLabel = m => (ICON[STATUS[m.id]?.state] || "") + m.repo.split("/").pop() + (m.size ? ` (${m.size})` : ""); | |
| function renderModelSelect() { | |
| const sel = $("mp"), groups = {}; | |
| MODEL_LIST.filter(m => m.group !== "aux").forEach(m => (groups[m.group || "أخرى"] ||= []).push(m)); | |
| sel.innerHTML = ""; | |
| for (const [g, list] of Object.entries(groups)) { | |
| const og = document.createElement("optgroup"); og.label = g; | |
| list.forEach(m => { const o = document.createElement("option"); o.value = m.id; o.textContent = optLabel(m); og.appendChild(o); }); | |
| sel.appendChild(og); | |
| } | |
| sel.value = MODELS[cfg.model] ? cfg.model : MODEL_LIST.find(m => m.group !== "aux")?.id || ""; | |
| onModelChange(); | |
| } | |
| function renderLoad(st) { | |
| const lp = $("lp"); | |
| if (!st || !["downloading", "loading"].includes(st.state)) return lp.classList.remove("on", "ind"); | |
| lp.classList.add("on"); | |
| const el = fmtT(st.elapsed); | |
| if (st.state === "downloading") { | |
| const pct = Math.round(st.pct || 0); | |
| lp.classList.remove("ind"); $("lpF").style.width = pct + "%"; | |
| $("lpA").textContent = `⬇ تنزيل النموذج ${pct}%`; | |
| $("lpB").textContent = `${fmtB(st.done)} / ${fmtB(st.total)} · ${st.speed ? fmtB(st.speed) + "/ث" : "…"} · المتبقي ${st.eta && st.elapsed > 3 ? "~" + fmtT(st.eta) : "…"} · مضى ${el}`; | |
| } else { | |
| lp.classList.add("ind"); | |
| $("lpA").textContent = "⚙ تحميل النموذج في الذاكرة"; $("lpB").textContent = "مضى " + el; | |
| } | |
| } | |
| async function refreshStatus() { | |
| try { | |
| STATUS = await call("status") || {}; | |
| // إشعار عند اكتمال تحميل نموذج مع المدة | |
| for (const [id, st] of Object.entries(STATUS)) { | |
| const p = PREV[id]; | |
| if ((p === "downloading" || p === "loading") && st.state === "ready") toast(`✅ ${MODELS[id]?.repo.split("/").pop() || id} جاهز — استغرق ${fmtT(st.took)}`); | |
| PREV[id] = st.state; | |
| } | |
| const st = STATUS[$("mp").value], led = $("led"), lt = $("ledT"); | |
| renderLoad(st); | |
| if (st?.state === "ready") { led.className = "led on"; lt.textContent = "جاهز" + (st.took ? ` · ${fmtT(st.took)}` : ""); } | |
| else if (st?.state === "downloading") { led.className = "led wr"; lt.textContent = `تنزيل ${Math.round(st.pct || 0)}% · ${fmtT(st.elapsed)}`; } | |
| else if (st?.state === "loading") { led.className = "led wr"; lt.textContent = `تحميل… ${fmtT(st.elapsed)}`; } | |
| else if (st?.state === "error") { led.className = "led er"; lt.textContent = "خطأ"; lt.title = st.error; } | |
| else { led.className = "led"; lt.textContent = "غير محمّل"; } | |
| document.querySelectorAll("#mp option").forEach(o => { const m = MODELS[o.value]; if (m) o.textContent = optLabel(m); }); | |
| const any = Object.values(STATUS).some(s => ["downloading", "loading"].includes(s.state)); | |
| if (any && !pollT) pollT = setInterval(refreshStatus, 1000); | |
| if (!any && pollT) { clearInterval(pollT); pollT = null; } | |
| } catch (e) { logDiag("refreshStatus: " + e.message); } | |
| } | |
| function onModelChange() { const id = $("mp").value; if (!id) return; cfg.model = id; saveCfg(); refreshStatus(); } | |
| async function doLoad() { | |
| const id = $("mp").value; if (!id) return; | |
| try { const r = await call("load", [id]); if (!r?.ok) throw new Error(r?.error || "فشل"); await refreshStatus(); } | |
| catch (e) { toast("❌ " + e.message); logDiag("doLoad: " + e.message); } | |
| } | |
| async function doUnload() { | |
| const id = $("mp").value; if (!id) return; | |
| try { await call("unload", [id]); toast("⏏ فُرّغ من الذاكرة"); await refreshStatus(); } catch (e) { toast("❌ " + e.message); } | |
| } | |
| async function ensureLoaded(id, onTick) { | |
| await refreshStatus(); | |
| if (STATUS[id]?.state === "ready") return; | |
| const r = await call("load", [id]); | |
| if (!r?.ok) throw new Error(r?.error || "فشل بدء التحميل"); | |
| for (let n = 0; n < 1800; n++) { // حتى 30 دقيقة | |
| await sleep(1000); await refreshStatus(); | |
| const st = STATUS[id]; | |
| if (st?.state === "ready") return; | |
| if (st?.state === "error") throw new Error(st.error || "فشل التحميل"); | |
| if (st) onTick?.(st); | |
| } | |
| throw new Error("انتهت مهلة التحميل"); | |
| } | |
| function openAdd() { $("add").classList.add("open"); closeAll(); } | |
| async function addModel() { | |
| const repo = $("aRepo").value.trim(), kind = $("aKind").value; | |
| try { | |
| const r = await call("register", [repo, kind]); | |
| if (!r?.ok) throw new Error(r?.error || "فشل"); | |
| MODELS[r.model.id] = r.model; | |
| if (!MODEL_LIST.find(m => m.id === r.model.id)) MODEL_LIST.push(r.model); | |
| if (!(cfg.custom ||= []).find(c => c.repo === repo)) cfg.custom.push({ repo, kind }); | |
| cfg.model = r.model.id; saveCfg(); renderModelSelect(); | |
| $("add").classList.remove("open"); toast("✅ أُضيف — اضغط ⬇ أو أرسل رسالة للتحميل"); | |
| } catch (e) { toast("❌ " + e.message); } | |
| } | |
| /* ==================== Send / Ask ==================== */ | |
| let busy = false, curReqId = null, files = []; | |
| function setBusy(v) { busy = v; $("snd").style.display = v ? "none" : "flex"; $("stp").style.display = v ? "flex" : "none"; $("mp").disabled = v; } | |
| function chips() { $("chips").innerHTML = files.map((f, i) => `<span class="chip">📎 ${esc(f.name)}<button onclick="files.splice(${i},1);chips()">✕</button></span>`).join(""); } | |
| function addFiles(l) { | |
| [...l].forEach(f => { | |
| const r = new FileReader(); | |
| r.onload = () => { files.push({ name: f.name, type: f.type, data: r.result }); chips(); }; | |
| f.type.startsWith("image/") ? r.readAsDataURL(f) : r.readAsText(f); | |
| }); | |
| $("fi").value = ""; | |
| } | |
| async function sendMsg() { | |
| if (busy) return; | |
| const text = $("inp").value.trim(); | |
| if (!text && !files.length) return; | |
| const images = files.filter(f => f.type.startsWith("image/")); | |
| const docs = files.filter(f => !f.type.startsWith("image/")); | |
| let desc = []; | |
| if (images.length) { | |
| setBusy(true); | |
| try { | |
| toast("🖼 جارٍ تجهيز نموذج وصف الصور…"); | |
| await ensureLoaded("caption"); | |
| for (const img of images) { | |
| try { const r = await call("caption", [img.data.split(",")[1]]); desc.push(r?.ok ? r.text : ""); } catch { desc.push(""); } | |
| } | |
| } catch (e) { toast("⚠️ " + e.message); images.forEach(() => desc.push("")); } | |
| setBusy(false); | |
| } | |
| let content = desc.map((d, i) => `[صورة ${i + 1}: ${d || "بدون وصف"}]`).join("\n"); | |
| content += docs.map(d => `\n\n[ملف ${d.name}]\n${String(d.data).slice(0, 6000)}`).join(""); | |
| content = (content ? content.trim() + "\n\n" : "") + text; | |
| const names = files.map(f => f.name); | |
| files = []; chips(); $("inp").value = ""; $("inp").style.height = "auto"; | |
| await submit(content.trim(), names, text); | |
| } | |
| async function submit(content, names = [], title = "") { | |
| const s = cur(); | |
| if (!s.messages.length) { s.title = (title || content).slice(0, 40) || "ملفات"; renderSessions(); } | |
| s.ts = Date.now(); | |
| s.messages.push({ role: "user", content, _files: names }); | |
| persist(); renderSessions(); renderChat(); | |
| await ask(); | |
| } | |
| async function ask() { | |
| if (busy) return; | |
| setBusy(true); | |
| const s = cur(), bubble = addMsg("assistant", ""), bd = bubble.querySelector(".bd"); | |
| bd.innerHTML = '<span class="stt">تحضير النموذج...</span>'; | |
| curReqId = uid(); L.closed = false; L.on = false; | |
| let raf = 0, pend = null, got = false; | |
| const paint = t => { pend = t; if (!raf) raf = requestAnimationFrame(() => { raf = 0; bd.innerHTML = mdStream(pend); liveCode(pend); scrollDown(); }); }; | |
| try { | |
| await ensureLoaded(cfg.model, st => { if (!got) bd.innerHTML = `<span class="stt">${st.state === "downloading" ? `تنزيل النموذج ${Math.round(st.pct || 0)}%` : "تحميل في الذاكرة"} · مضى ${fmtT(st.elapsed)}</span>`; }); | |
| bd.innerHTML = '<span class="stt">جارٍ التوليد...</span>'; | |
| const msgs = [{ role: "system", content: cfg.sys }, ...s.messages.map(m => ({ role: m.role, content: m.content }))]; | |
| logDiag("generate: " + cfg.model + " · maxTok=" + cfg.maxTok); | |
| const r = await call("generate", [cfg.model, J(msgs), curReqId, parseInt(cfg.maxTok) || 256, parseFloat(cfg.temp) || 0.7, parseFloat(cfg.topP) || 0.9], | |
| o => { if (o && o.ok !== false && typeof o.text === "string" && !o.done) { got = true; paint(o.text); } }); | |
| cancelAnimationFrame(raf); raf = 0; | |
| if (!r || r.ok === false) throw new Error((r && r.error) || "فشل التوليد"); | |
| const answer = (r.text || "").trim(); | |
| bubble.remove(); | |
| if (answer) { | |
| const st = r.secs ? { secs: r.secs, tokens: r.tokens } : null; | |
| s.messages.push({ role: "assistant", content: answer, _st: st }); | |
| const nb = addMsg("assistant", answer, null, s.messages.length - 1, statsHtml(st)); | |
| persist(); liveEnd(); | |
| const cm = [...answer.matchAll(/```([\w+-]*)\n?([\s\S]*?)```/g)], last = cm[cm.length - 1]; | |
| if (last && qual((last[1] || "text").toLowerCase(), last[2].length)) { const id = "a" + hash(last[2]); if (s.artifacts?.find(x => x.id === id)) setTimeout(() => openArtifact(id), 100); } | |
| if (V.on) vSay(ttsClean(answer)); | |
| else if (cfg.autoSpeak && !micOn) ttsSpeak(answer, nb.querySelector(".spb")); | |
| } else addMsg("assistant", "(رد فارغ — جرّب مرة أخرى)"); | |
| } catch (e) { | |
| cancelAnimationFrame(raf); bubble.remove(); liveEnd(); | |
| addMsg("assistant", "⚠️ " + e.message); logDiag("ask: " + e.message); | |
| } finally { curReqId = null; setBusy(false); $("inp").focus(); } | |
| } | |
| function stopGen() { if (curReqId) { call("cancel", [curReqId]).catch(() => {}); logDiag("cancel"); } } | |
| /* ==================== TTS ==================== */ | |
| const hasTTS = "speechSynthesis" in window; | |
| if (hasTTS) { speechSynthesis.getVoices(); speechSynthesis.onvoiceschanged = () => fillVoices(); } | |
| const TTS = { id: 0 }; | |
| function ttsClean(t) { | |
| return t.replace(/```[\s\S]*?```/g, " ").replace(/<think>[\s\S]*?(<\/think>|$)/g, " ").replace(/`([^`]*)`/g, "$1") | |
| .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/https?:\/\/\S+/g, "") | |
| .replace(/[#*_>~|]+/g, " ").replace(/^\s*[-•]\s+/gm, "").replace(/\s+/g, " ").trim(); | |
| } | |
| function ttsChunks(t) { | |
| const o = []; let c = ""; | |
| for (const p of (t.match(/[^.!?؟…\n]+[.!?؟…\n]*/g) || [t])) { if (c && (c + " " + p).length > 180) { o.push(c); c = p; } else c = c ? c + " " + p : p; } | |
| if (c) o.push(c); | |
| return o.flatMap(x => x.length > 220 ? x.match(/.{1,200}(\s|$)/g) : [x]); | |
| } | |
| function pickVoice(t) { | |
| const ar = (t.match(/[\u0600-\u06FF]/g) || []).length > (t.match(/[A-Za-z]/g) || []).length, lg = ar ? "ar" : "en", vs = speechSynthesis.getVoices(); | |
| return { ar, v: vs.find(x => x.name === cfg.ttsVoice && x.lang.toLowerCase().startsWith(lg)) || vs.find(x => x.lang.toLowerCase().startsWith(lg)) || null }; | |
| } | |
| function ttsStop() { | |
| if (!hasTTS) return; | |
| TTS.id++; speechSynthesis.cancel(); | |
| document.querySelectorAll(".spb.on").forEach(b => { b.classList.remove("on"); b.textContent = "🔊 استمع"; }); | |
| } | |
| function ttsSpeak(text, btn) { | |
| if (!hasTTS) return toast("⚠️ المتصفح لا يدعم النطق"); | |
| ttsStop(); | |
| const t = ttsClean(text || ""); if (!t) return; | |
| const my = ++TTS.id, { v } = pickVoice(t), ch = ttsChunks(t); let i = 0; | |
| if (btn) { btn.classList.add("on"); btn.textContent = "⏹ إيقاف"; } | |
| const next = () => { | |
| if (my !== TTS.id) return; | |
| if (i >= ch.length) { if (btn) { btn.classList.remove("on"); btn.textContent = "🔊 استمع"; } return; } | |
| const u = new SpeechSynthesisUtterance(ch[i++]), ar = pickVoice(ch[i - 1]).ar; | |
| u.rate = cfg.ttsRate || 1.5; u.lang = v ? v.lang : (ar ? "ar-SA" : "en-US"); | |
| if (v) u.voice = v; | |
| u.onend = next; u.onerror = next; speechSynthesis.speak(u); | |
| }; | |
| next(); | |
| } | |
| function speakMsg(b) { if (b.classList.contains("on")) return ttsStop(); ttsSpeak(b.closest(".msg").dataset.raw || "", b); } | |
| function fillVoices() { | |
| if (!hasTTS) return; | |
| const sel = $("cVoice"); if (!sel) return; | |
| sel.innerHTML = '<option value="">تلقائي (حسب لغة الرد)</option>' + speechSynthesis.getVoices().map(v => `<option value="${esc(v.name)}" ${v.name === cfg.ttsVoice ? "selected" : ""}>${esc(v.name)} (${esc(v.lang)})</option>`).join(""); | |
| } | |
| function testVoice() { cfg.ttsRate = parseFloat($("cRate").value) || 1.5; cfg.ttsVoice = $("cVoice").value; ttsSpeak("مرحباً، هذا اختبار للصوت والسرعة."); } | |
| function updSpk() { | |
| const b = $("spk"); if (!b) return; | |
| b.classList.toggle("on", !!cfg.autoSpeak); b.textContent = cfg.autoSpeak ? "🔊" : "🔇"; | |
| b.title = cfg.autoSpeak ? "النطق التلقائي: مفعل" : "النطق التلقائي: معطل"; | |
| } | |
| function toggleAuto() { | |
| if (!hasTTS) return toast("⚠️ المتصفح لا يدعم النطق"); | |
| cfg.autoSpeak = !cfg.autoSpeak; saveCfg(); updSpk(); | |
| if (cfg.autoSpeak) { const u = new SpeechSynthesisUtterance(" "); u.volume = 0; speechSynthesis.speak(u); } else ttsStop(); | |
| toast(cfg.autoSpeak ? "🔊 النطق مفعل" : "🔇 النطق معطل"); | |
| } | |
| /* ==================== STT ==================== */ | |
| const SR = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| let rec = null, micOn = false, micBase = ""; | |
| function micUI() { | |
| const b = $("mic"); b.classList.toggle("on", micOn); b.textContent = micOn ? "⏹" : "🎤"; | |
| $("inp").placeholder = micOn ? "🎤 أستمع..." : "اكتب رسالتك..."; | |
| } | |
| async function toggleMic() { | |
| if (!SR) return toast("⚠️ المتصفح لا يدعم التفريغ. استخدم Chrome/Edge"); | |
| if (micOn) { micOn = false; try { rec?.stop(); } catch {} micUI(); return; } | |
| try { const st = await navigator.mediaDevices.getUserMedia({ audio: true }); st.getTracks().forEach(t => t.stop()); } | |
| catch (e) { return toast("❌ تعذر الميكروفون: " + e.message); } | |
| ttsStop(); micOn = true; micUI(); | |
| micBase = $("inp").value; if (micBase && !/\s$/.test(micBase)) micBase += " "; | |
| const r = rec = new SR(); | |
| r.lang = cfg.sttLang || "ar-SA"; r.continuous = false; r.interimResults = true; r.maxAlternatives = 1; | |
| r.onresult = e => { if (rec !== r || !micOn) return; let t = ""; for (let i = 0; i < e.results.length; i++) t += e.results[i][0].transcript; $("inp").value = micBase + t; $("inp").dispatchEvent(new Event("input")); }; | |
| r.onerror = e => { if (rec !== r) return; if (e.error === "no-speech" || e.error === "aborted") return; micOn = false; micUI(); toast("❌ " + (e.error === "not-allowed" ? "الإذن مرفوض" : e.error)); }; | |
| r.onend = () => { if (rec !== r) return; if (micOn) setTimeout(() => { if (micOn && rec === r) { try { r.start(); } catch { micOn = false; micUI(); } } }, 150); else micUI(); }; | |
| try { r.start(); } catch (e) { micOn = false; micUI(); toast("❌ " + e.message); } | |
| } | |
| /* ==================== Voice mode ==================== */ | |
| const V = { on: false, state: "", rec: null, text: "", sil: 0, chk: 0, pend: 0, tok: 0, done: true, muted: false }; | |
| function vSet(st) { | |
| V.state = st; $("vc").className = "on " + st; | |
| $("vS").textContent = ({ listening: "أستمع إليك...", thinking: "أفكر...", speaking: "أتحدث — المس الدائرة للمقاطعة", muted: "الميكروفون مكتوم" })[st] || ""; | |
| } | |
| async function startVoice() { | |
| if (!SR || !hasTTS) return toast("⚠️ المتصفح لا يدعم الصوت"); | |
| if (busy) return toast("⏳ انتظر انتهاء المهمة الحالية"); | |
| const u = new SpeechSynthesisUtterance(" "); u.volume = 0; speechSynthesis.speak(u); | |
| $("vc").className = "on thinking"; $("vS").textContent = "جارٍ تهيئة الميكروفون..."; | |
| try { const st = await navigator.mediaDevices.getUserMedia({ audio: true }); st.getTracks().forEach(t => t.stop()); } | |
| catch (e) { $("vc").className = ""; return toast("❌ " + e.message); } | |
| if (micOn) { micOn = false; try { rec?.stop(); } catch {} micUI(); } | |
| ttsStop(); | |
| Object.assign(V, { on: true, muted: false, done: true, pend: 0, tok: V.tok + 1 }); | |
| vListen(); | |
| } | |
| function vListen() { | |
| if (!V.on || V.muted || busy) return; | |
| V.text = ""; $("vT").textContent = ""; vSet("listening"); | |
| const r = V.rec = new SR(); | |
| r.lang = cfg.sttLang || "ar-SA"; r.continuous = false; r.interimResults = true; | |
| r.onresult = e => { | |
| if (V.rec !== r || V.state !== "listening") return; | |
| let t = "", fin = false; | |
| for (let i = 0; i < e.results.length; i++) { t += e.results[i][0].transcript; if (e.results[i].isFinal) fin = true; } | |
| V.text = t; $("vT").textContent = t; clearTimeout(V.sil); | |
| if (fin) vSend(); else V.sil = setTimeout(vSend, 1200); | |
| }; | |
| r.onerror = e => { if (V.rec !== r) return; if (e.error === "no-speech" || e.error === "aborted") return; toast("❌ " + e.error); }; | |
| r.onend = () => { if (V.rec === r && V.on && V.state === "listening") setTimeout(() => { if (V.rec === r && V.on && V.state === "listening") vListen(); }, 200); }; | |
| try { r.start(); } catch {} | |
| } | |
| function vSend() { | |
| clearTimeout(V.sil); | |
| const t = (V.text || "").trim(); | |
| if (!V.on || V.state !== "listening" || !t || busy) return; | |
| const r = V.rec; V.rec = null; try { r?.abort(); } catch {} | |
| V.done = false; $("vR").textContent = ""; vSet("thinking"); | |
| submit(t).then(() => { V.done = true; const l = cur().messages.at(-1); if (l?.role === "assistant") $("vR").textContent = l.content.slice(0, 400); vCheck(); }); | |
| } | |
| function vSay(t) { | |
| const tok = V.tok; | |
| for (const c of ttsChunks(t).filter(Boolean)) { | |
| const { v, ar } = pickVoice(c), u = new SpeechSynthesisUtterance(c); | |
| u.rate = cfg.ttsRate || 1.5; u.lang = v ? v.lang : (ar ? "ar-SA" : "en-US"); | |
| if (v) u.voice = v; | |
| V.pend++; | |
| u.onstart = () => { if (tok === V.tok && V.on) vSet("speaking"); }; | |
| u.onend = u.onerror = () => { if (tok !== V.tok) return; V.pend--; vCheck(); }; | |
| speechSynthesis.speak(u); | |
| } | |
| } | |
| function vCheck() { if (V.on && V.done && V.pend <= 0 && !V.chk) V.chk = setTimeout(() => { V.chk = 0; if (V.on && V.done && V.pend <= 0) vListen(); }, 300); } | |
| function vTap() { | |
| if (!V.on) return; | |
| if (V.state === "listening") return vSend(); | |
| if (V.state === "speaking" || V.state === "thinking") { | |
| V.tok++; V.pend = 0; speechSynthesis.cancel(); | |
| if (busy && curReqId) call("cancel", [curReqId]).catch(() => {}); | |
| V.done = true; vListen(); | |
| } | |
| } | |
| function vMuteTog() { | |
| if (!V.on) return; | |
| V.muted = !V.muted; $("vMute").textContent = V.muted ? "🔇" : "🎤"; | |
| if (V.muted) { clearTimeout(V.sil); const r = V.rec; V.rec = null; try { r?.abort(); } catch {} if (V.state === "listening") vSet("muted"); } | |
| else { if (V.state === "muted") V.state = ""; if (!busy && V.pend <= 0) vListen(); } | |
| } | |
| function endVoice() { | |
| if (!V.on && !$("vc").classList.contains("on")) return; | |
| V.on = false; V.tok++; V.pend = 0; clearTimeout(V.sil); clearTimeout(V.chk); V.chk = 0; | |
| const r = V.rec; V.rec = null; try { r?.abort(); } catch {} | |
| if (hasTTS) speechSynthesis.cancel(); | |
| $("vc").className = ""; | |
| } | |
| /* ==================== Settings / Export / UI ==================== */ | |
| function openSet() { fillVoices(); $("set").classList.add("open"); closeAll(); } | |
| function saveSet() { | |
| cfg.sys = $("cSys").value; cfg.temp = $("cTemp").value; | |
| cfg.maxTok = Math.min(32000, Math.max(32, parseInt($("cMax").value) || 256)); | |
| cfg.topP = $("cTop").value; cfg.ttsRate = parseFloat($("cRate").value) || 1.5; | |
| cfg.ttsVoice = $("cVoice").value; cfg.autoSpeak = $("cAutoSpeak").checked; cfg.sttLang = $("cStt").value; | |
| saveCfg(); updSpk(); $("set").classList.remove("open"); toast("✅ تم الحفظ"); | |
| } | |
| function exportChat() { | |
| const s = cur(); if (!s.messages.length) return toast("لا رسائل"); | |
| const t = "# " + s.title + "\n\n" + s.messages.map(m => "### " + (m.role === "user" ? "أنت" : "المساعد") + "\n" + (typeof m.content === "string" ? m.content : "")).join("\n\n"); | |
| const a = document.createElement("a"); | |
| a.href = URL.createObjectURL(new Blob([t], { type: "text/markdown" })); a.download = "chat-" + cid.slice(0, 8) + ".md"; a.click(); toast("⬇ تم التصدير"); | |
| } | |
| function toggleSb() { $("sb").classList.toggle("open"); $("ov").classList.toggle("show", $("sb").classList.contains("open")); } | |
| function closeAll() { $("pp").classList.remove("open"); $("sb").classList.remove("open"); $("ov").classList.remove("show"); } | |
| document.addEventListener("click", e => { | |
| if (!$("pw").contains(e.target)) $("pp").classList.remove("open"); | |
| if (innerWidth <= 768 && $("sb").classList.contains("open") && !$("sb").contains(e.target) && e.target.id !== "hb") closeAll(); | |
| }); | |
| $("inp").addEventListener("input", () => { const i = $("inp"); i.style.height = "auto"; i.style.height = Math.min(i.scrollHeight, 170) + "px"; }); | |
| addEventListener("keydown", e => { | |
| if (e.key === "Escape") { endVoice(); closeAll(); document.querySelectorAll(".md.open").forEach(m => m.classList.remove("open")); $("cp").classList.remove("full"); } | |
| }); | |
| $("cMax").addEventListener("input", () => { const v = Math.min(32000, Math.max(32, parseInt($("cMax").value) || 256)); $("cMaxRange").value = v; $("mMax").textContent = v; }); | |
| function boot() { | |
| applyTheme(); updSpk(); fillVoices(); | |
| $("cSys").value = cfg.sys; $("cTemp").value = cfg.temp; $("cMax").value = cfg.maxTok; $("cMaxRange").value = cfg.maxTok; | |
| $("mMax").textContent = cfg.maxTok; $("cTop").value = cfg.topP; $("cRate").value = cfg.ttsRate; $("rV").textContent = cfg.ttsRate; | |
| $("cAutoSpeak").checked = !!cfg.autoSpeak; $("cStt").value = cfg.sttLang || "ar-SA"; | |
| renderSessions(); renderChat(); setBusy(false); initModels(); | |
| if (!SR) { $("mic").style.opacity = .4; $("vcb").style.opacity = .4; } | |
| if (!hasTTS) $("spk").style.opacity = .4; | |
| } | |
| boot(); | |
| </script> | |
| </body> | |
| </html> |