Deepmind / index.html
bh-ab's picture
Update index.html
59bf6d1 verified
Raw History Blame Contribute Delete
61.6 kB
<!DOCTYPE html>
<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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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(/&lt;think&gt;([\s\S]*?)(?:&lt;\/think&gt;|$)/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(/^&gt; (.*)$/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>