| <!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,maximum-scale=1,user-scalable=no,viewport-fit=cover"> |
| <meta name="mobile-web-app-capable" content="yes"> |
| <meta name="apple-mobile-web-app-capable" content="yes"> |
| <title>g4fpro</title> |
| <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"> |
| <style> |
| :root{ |
| --bg:#0c0c12;--bg1:#141420;--bg2:#1b1b28;--bg3:#222232;--bg4:#2a2a3e;--bgh:#313148; |
| --ln:#1e1e2e;--ln2:#2c2c42;--ln3:#404055; |
| --t0:#e8e8f5;--t1:#b0b0cc;--t2:#6868a0;--t3:#404058; |
| --ac:#7c6fff;--ach:#8f84ff;--acbg:#7c6fff14;--acln:#7c6fff44; |
| --tl:#00d4ff;--tlbg:#00d4ff0e; |
| --gn:#22d48a;--gnbg:#22d48a0e; |
| --am:#f5a623;--ambg:#f5a6230e; |
| --rd:#ff5f6d;--rdbg:#ff5f6d0e; |
| --font:'Cairo',sans-serif;--mono:'JetBrains Mono',monospace; |
| --r1:5px;--r2:10px;--r3:14px;--r4:20px; |
| --s1:0 1px 4px #0009;--s2:0 4px 18px #000b;--s3:0 12px 40px #000e; |
| } |
| [data-theme=light]{ |
| --bg:#f2f2fa;--bg1:#fff;--bg2:#f5f5fd;--bg3:#ebebf8;--bg4:#e2e2f0;--bgh:#d6d6ee; |
| --ln:#e8e8f6;--ln2:#d8d8ee;--ln3:#c0c0dc; |
| --t0:#0d0d1a;--t1:#2a2a44;--t2:#5a5a88;--t3:#9898b8; |
| --s1:0 1px 4px #0002;--s2:0 4px 18px #0003;--s3:0 12px 40px #0004; |
| } |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent} |
| html,body{height:100%;overflow:hidden;font-family:var(--font);background:var(--bg);color:var(--t0);-webkit-font-smoothing:antialiased} |
| button{cursor:pointer;font-family:var(--font);border:none;background:none;color:inherit} |
| input,textarea,select{font-family:var(--font);-webkit-appearance:none;appearance:none} |
| a{color:var(--tl)} |
| ::-webkit-scrollbar{width:3px;height:3px} |
| ::-webkit-scrollbar-thumb{background:var(--ln2);border-radius:2px} |
| ::-webkit-scrollbar-track{background:transparent} |
| #app{display:flex;height:100dvh;overflow:hidden} |
| #sb{width:268px;min-width:268px;background:var(--bg1);border-left:1px solid var(--ln);display:flex;flex-direction:column;height:100dvh;overflow:hidden;flex-shrink:0;transition:width .22s ease,min-width .22s ease} |
| #sb.desk-hide{width:0;min-width:0;border-left:none} |
| @media(max-width:680px){ |
| #sb{position:fixed;right:0;top:0;bottom:0;width:282px!important;min-width:0;transform:translateX(110%);box-shadow:var(--s3);transition:transform .22s ease;z-index:50} |
| #sb.mob-open{transform:translateX(0)} |
| } |
| .sb-head{padding:10px;border-bottom:1px solid var(--ln);flex-shrink:0} |
| .new-btn{width:100%;display:flex;align-items:center;gap:7px;padding:9px 12px;background:var(--ac);color:#fff;border-radius:var(--r2);font-size:.87rem;font-weight:700;transition:.14s} |
| .new-btn:hover{background:var(--ach)}.new-btn:active{transform:scale(.97)} |
| .cl{flex:1;overflow-y:auto;padding:4px} |
| .grp{font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--t3);padding:8px 8px 3px} |
| .ci{display:flex;align-items:center;gap:7px;padding:7px 9px;border-radius:var(--r2);cursor:pointer;transition:.13s;position:relative} |
| .ci:hover{background:var(--bgh)}.ci.on{background:var(--acbg);border:1px solid var(--acln)} |
| .ci-dot{width:6px;height:6px;border-radius:50%;background:var(--t3);flex-shrink:0;transition:.13s} |
| .ci.on .ci-dot{background:var(--ac)} |
| .ci-info{flex:1;min-width:0} |
| .ci-title{font-size:.79rem;font-weight:600;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| .ci.on .ci-title{color:var(--ac)} |
| .ci-sub{font-size:.64rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px} |
| .ci-acts{display:none;gap:1px;flex-shrink:0}.ci:hover .ci-acts{display:flex} |
| .cia{width:22px;height:22px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--t2);transition:.13s} |
| .cia:hover{background:var(--bg4);color:var(--t0)}.cia.del:hover{color:var(--rd)} |
| .sb-bot{border-top:1px solid var(--ln);padding:10px;flex-shrink:0;display:flex;flex-direction:column;gap:7px;overflow-y:auto;max-height:58vh} |
| .lbl{font-size:.63rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--t3);margin-bottom:3px;display:flex;align-items:center;gap:4px} |
| .lbl i{color:var(--ac);font-size:.61rem} |
| .sel{width:100%;padding:7px 24px 7px 7px;background:var(--bg3);border:1px solid var(--ln2);border-radius:var(--r1);color:var(--t0);font-size:.8rem;font-weight:600;cursor:pointer;direction:rtl;transition:.13s;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath fill='%23668' d='M5 6L0 0h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:left 7px center;background-size:8px} |
| .sel:focus{border-color:var(--ac);outline:none} |
| .tog-row{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;background:var(--bg3);border:1px solid var(--ln2);border-radius:var(--r1)} |
| .tog-lbl{font-size:.78rem;font-weight:600;color:var(--t1);display:flex;align-items:center;gap:5px} |
| .tog-lbl i{color:var(--tl);font-size:.7rem} |
| .tog{position:relative;width:34px;height:18px;flex-shrink:0} |
| .tog input{opacity:0;width:0;height:0;position:absolute} |
| .tog-t{position:absolute;inset:0;background:var(--bg4);border:1px solid var(--ln2);border-radius:9px;cursor:pointer;transition:.15s} |
| .tog-t::after{content:'';position:absolute;width:12px;height:12px;right:2px;top:2px;background:var(--t2);border-radius:50%;transition:.15s} |
| .tog input:checked~.tog-t{background:var(--tl);border-color:var(--tl)} |
| .tog input:checked~.tog-t::after{transform:translateX(-16px);background:#fff} |
| .sys-ta{width:100%;padding:6px 8px;background:var(--bg3);border:1px solid var(--ln2);border-radius:var(--r1);color:var(--t0);font-size:.78rem;line-height:1.5;resize:none;min-height:50px;max-height:86px;direction:rtl;transition:.13s} |
| .sys-ta:focus{border-color:var(--ac);outline:none}.sys-ta::placeholder{color:var(--t3)} |
| #main{flex:1;min-width:0;display:flex;flex-direction:column;height:100dvh;background:var(--bg)} |
| #hdr{height:50px;background:var(--bg1);border-bottom:1px solid var(--ln);display:flex;align-items:center;padding:0 12px;gap:8px;flex-shrink:0} |
| .hb{width:32px;height:32px;border-radius:var(--r1);background:var(--bg3);border:1px solid var(--ln2);color:var(--t1);display:flex;align-items:center;justify-content:center;font-size:.82rem;transition:.13s;flex-shrink:0} |
| .hb:hover{background:var(--bgh);color:var(--t0)}.hb:active{transform:scale(.91)} |
| .logo{display:flex;align-items:center;gap:6px;font-weight:900;font-size:.96rem;letter-spacing:-.03em;white-space:nowrap} |
| .logo-mk{width:27px;height:27px;background:linear-gradient(135deg,var(--ac),var(--tl));border-radius:7px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.72rem} |
| .logo-ac{color:var(--ac)} |
| .hdr-m{flex:1;min-width:0;text-align:center} |
| .hdr-t{font-size:.81rem;font-weight:700;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| .hdr-r{display:flex;align-items:center;gap:5px;flex-shrink:0} |
| .hst{display:flex;align-items:center;gap:4px;font-size:.67rem;color:var(--gn);font-weight:600;white-space:nowrap} |
| .hst::before{content:'';width:5px;height:5px;background:var(--gn);border-radius:50%;animation:pu 2s ease infinite} |
| @keyframes pu{0%,100%{opacity:1}50%{opacity:.3}} |
| .ppill{display:flex;align-items:center;gap:4px;padding:3px 8px;background:var(--bg3);border:1px solid var(--ln2);border-radius:14px;font-size:.67rem;font-weight:700;color:var(--t2);white-space:nowrap;max-width:116px;overflow:hidden;text-overflow:ellipsis} |
| .pd{width:5px;height:5px;border-radius:50%;background:var(--ac);flex-shrink:0} |
| @media(max-width:480px){.ppill{display:none}} |
| #mwrap{flex:1;overflow-y:auto;position:relative;-webkit-overflow-scrolling:touch} |
| #msgs{max-width:780px;margin:0 auto;padding:18px 10px 14px;display:flex;flex-direction:column;gap:2px} |
| @media(max-width:680px){#msgs{padding:12px 8px 10px}} |
| #wlc{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:calc(100dvh - 140px);gap:18px;text-align:center;padding:20px;animation:fup .4s ease} |
| @keyframes fup{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}} |
| .wico{width:66px;height:66px;background:linear-gradient(135deg,var(--ac),var(--tl));border-radius:18px;display:flex;align-items:center;justify-content:center;font-size:1.6rem;color:#fff;animation:fl 3.5s ease-in-out infinite} |
| @keyframes fl{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}} |
| .wt{font-size:1.35rem;font-weight:900}.wt span{color:var(--ac)} |
| .ws{font-size:.85rem;color:var(--t2);max-width:340px;line-height:1.65} |
| .sg{display:grid;grid-template-columns:1fr 1fr;gap:6px;width:100%;max-width:420px} |
| @media(max-width:420px){.sg{grid-template-columns:1fr}} |
| .sc{padding:10px 11px;background:var(--bg2);border:1px solid var(--ln2);border-radius:var(--r2);text-align:right;cursor:pointer;transition:.13s} |
| .sc:hover{background:var(--bgh);border-color:var(--acln);transform:translateY(-2px)}.sc:active{transform:scale(.97)} |
| .sc-i{font-size:.95rem;display:block;margin-bottom:3px} |
| .sc-t{font-size:.76rem;color:var(--t1);font-weight:600;line-height:1.3} |
| .sc-s{font-size:.65rem;color:var(--t3);margin-top:1px} |
| .mr{display:flex;flex-direction:column;gap:2px;animation:ms .22s cubic-bezier(.34,1.4,.64,1)} |
| @keyframes ms{from{opacity:0;transform:translateY(5px)scale(.98)}to{opacity:1;transform:none}} |
| .mr.user{align-items:flex-end}.mr.assistant{align-items:flex-start}.mr.err{align-items:flex-start} |
| .mhdr{display:flex;align-items:center;gap:5px;padding:0 3px;font-size:.65rem;color:var(--t3)} |
| .mr.user .mhdr{flex-direction:row-reverse} |
| .mav{width:20px;height:20px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:.6rem;color:#fff;flex-shrink:0} |
| .mav.u{background:var(--ac)}.mav.b{background:linear-gradient(135deg,var(--ac),var(--tl))} |
| .mtag{background:var(--bg3);border:1px solid var(--ln2);padding:1px 5px;border-radius:8px;font-size:.6rem;font-weight:700;color:var(--t2)} |
| .pinbdg{color:var(--am);font-size:.6rem} |
| .mb{max-width:80%;padding:10px 14px;border-radius:var(--r3);font-size:.89rem;line-height:1.78;word-wrap:break-word;overflow-wrap:break-word;overflow:hidden} |
| @media(max-width:680px){.mb{max-width:91%;font-size:.87rem;padding:9px 11px}} |
| .mr.user .mb{background:var(--ac);color:#fff;border-bottom-left-radius:3px} |
| .mr.assistant .mb{background:var(--bg2);border:1px solid var(--ln2);color:var(--t0);border-bottom-right-radius:3px} |
| .mr.err .mb{background:var(--rdbg);border:1px solid #ff5f6d28;color:#ff9faa;border-radius:var(--r2)} |
| /* thinking */ |
| .thinking-block{max-width:80%;background:var(--ambg);border:1px solid #f5a62330;border-radius:var(--r2);overflow:hidden;animation:ms .22s ease} |
| @media(max-width:680px){.thinking-block{max-width:91%}} |
| .thinking-header{display:flex;align-items:center;gap:6px;padding:7px 11px;cursor:pointer;user-select:none;transition:.13s} |
| .thinking-header:hover{background:#f5a62310} |
| .thinking-icon{color:var(--am);font-size:.75rem;flex-shrink:0} |
| .thinking-label{font-size:.73rem;font-weight:700;color:var(--am);flex:1} |
| .thinking-summary{font-size:.67rem;color:var(--t3);margin-right:4px;font-style:italic;max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| .thinking-toggle{font-size:.65rem;color:var(--t3);transition:transform .2s} |
| .thinking-block.open .thinking-toggle{transform:rotate(180deg)} |
| .thinking-body{display:none;padding:6px 11px 9px;border-top:1px solid #f5a62320} |
| .thinking-block.open .thinking-body{display:block} |
| .thinking-line{font-size:.74rem;color:var(--t2);line-height:1.65;padding:2px 0} |
| .thinking-line::before{content:'› ';color:var(--am);opacity:.6;font-family:var(--mono)} |
| .thinking-block.streaming .thinking-icon{animation:spin 1.4s linear infinite} |
| @keyframes spin{to{transform:rotate(360deg)}} |
| .cur{display:inline-block;width:2px;height:.88em;background:var(--ac);border-radius:1px;margin-right:1px;vertical-align:middle;animation:cr .7s step-end infinite} |
| @keyframes cr{0%,100%{opacity:1}50%{opacity:0}} |
| .macts{display:flex;gap:2px;padding:1px 3px;opacity:0;transition:opacity .13s;flex-wrap:wrap} |
| .mr:hover .macts,.mr:focus-within .macts{opacity:1} |
| .mr.user .macts{flex-direction:row-reverse} |
| .ma{display:flex;align-items:center;gap:3px;padding:3px 7px;border-radius:5px;font-size:.65rem;font-weight:600;color:var(--t2);background:var(--bg2);border:1px solid var(--ln2);transition:.13s;white-space:nowrap} |
| .ma:hover{background:var(--bgh);color:var(--t0)}.ma i{font-size:.6rem} |
| .ma.rg{color:var(--ac);border-color:var(--acln)}.ma.rg:hover{background:var(--acbg)} |
| .ma.pn.on{color:var(--am);border-color:#f5a62335} |
| .etx{width:100%;background:var(--bg3);border:2px solid var(--ac);border-radius:var(--r2);padding:9px 10px;color:var(--t0);font-family:var(--font);font-size:.88rem;line-height:1.55;resize:none;direction:rtl;min-height:52px} |
| .etx:focus{outline:none} |
| .ebtns{display:flex;gap:6px;margin-top:5px;justify-content:flex-end} |
| .esv{padding:5px 13px;background:var(--ac);color:#fff;border-radius:5px;font-size:.77rem;font-weight:700;transition:.13s} |
| .esv:hover{background:var(--ach)} |
| .ecn{padding:5px 13px;background:var(--bg4);color:var(--t1);border-radius:5px;font-size:.77rem;font-weight:600;border:1px solid var(--ln2);transition:.13s} |
| .ecn:hover{background:var(--bgh)} |
| .fups{display:flex;flex-wrap:wrap;gap:5px;padding:1px 3px;margin-top:2px;animation:fup .3s ease} |
| .fup{padding:4px 10px;background:var(--bg2);border:1px solid var(--ln2);border-radius:14px;font-size:.72rem;font-weight:600;color:var(--tl);transition:.13s} |
| .fup:hover{background:var(--tlbg);border-color:#00d4ff35;transform:translateY(-1px)} |
| .mb h1,.mb h2,.mb h3{font-weight:800;margin:10px 0 4px;line-height:1.3} |
| .mb h1{font-size:1.1em}.mb h2{font-size:1em}.mb h3{font-size:.95em} |
| .mb p{margin:4px 0} |
| .mb ul,.mb ol{padding-right:16px;margin:5px 0}.mb li{margin:3px 0;line-height:1.65} |
| .mb blockquote{border-right:3px solid var(--ac);padding-right:9px;margin:5px 0;color:var(--t2);font-style:italic} |
| .mb strong{font-weight:700;color:var(--tl)}.mb em{font-style:italic;color:var(--am)} |
| .mb table{width:100%;border-collapse:collapse;margin:7px 0;font-size:.82em;display:block;overflow-x:auto} |
| .mb th{background:var(--bg4);padding:5px 9px;border:1px solid var(--ln2);font-weight:700;text-align:right;white-space:nowrap} |
| .mb td{padding:5px 9px;border:1px solid var(--ln)}.mb tr:nth-child(even){background:var(--bg3)} |
| .mb a{color:var(--tl)}.mb hr{border:none;border-top:1px solid var(--ln2);margin:9px 0} |
| .mb code:not(pre code){background:#fff1;padding:2px 5px;border-radius:4px;font-family:var(--mono);font-size:.8em;border:1px solid var(--ln2)} |
| .mr.user .mb code:not(pre code){background:#fff2;border-color:#fff2} |
| .cbw{position:relative;margin:8px 0;border-radius:var(--r2);overflow:hidden;border:1px solid #30363d;max-width:100%} |
| .cbh{display:flex;align-items:center;justify-content:space-between;padding:7px 12px;background:#161b22;border-bottom:1px solid #30363d;gap:8px} |
| .cb-lang{font-family:var(--mono);font-size:.68rem;color:#58a6ff;font-weight:500;display:flex;align-items:center;gap:5px} |
| .cb-lang::before{content:'';width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.65;flex-shrink:0} |
| .cb-lang.l-html::before{background:#e44d26}.cb-lang.l-css::before{background:#264de4} |
| .cb-lang.l-js::before,.cb-lang.l-javascript::before{background:#f7df1e} |
| .cb-lang.l-python::before{background:#3572A5} |
| .cb-lang.l-bash::before,.cb-lang.l-sh::before{background:#4EAA25} |
| .cb-btns{display:flex;gap:4px} |
| .cb-copy{display:flex;align-items:center;gap:3px;padding:3px 8px;background:#21262d;border:1px solid #30363d;border-radius:5px;color:#8b949e;font-size:.66rem;font-family:var(--font);transition:.13s;cursor:pointer} |
| .cb-copy:hover{background:#30363d;color:#e6edf3;border-color:#6e7681} |
| .cb-run{display:flex;align-items:center;gap:3px;padding:3px 9px;background:#1f6feb18;border:1px solid #1f6feb50;border-radius:5px;color:#58a6ff;font-size:.66rem;font-family:var(--font);transition:.13s;cursor:pointer} |
| .cb-run:hover{background:#1f6feb30;border-color:#1f6feb} |
| .cbw pre{margin:0!important;padding:14px!important;background:#0d1117!important;font-size:.79rem!important;line-height:1.65!important;overflow-x:auto} |
| .cbw pre code{font-family:var(--mono)!important;white-space:pre;word-break:normal} |
| .artifact{display:flex;align-items:center;gap:10px;padding:10px 13px;background:var(--bg2);border:1px solid var(--ln2);border-right:3px solid var(--ac);border-radius:var(--r2);margin-top:8px;max-width:80%;animation:fup .3s ease} |
| @media(max-width:680px){.artifact{max-width:91%}} |
| .art-icon{width:34px;height:34px;border-radius:8px;background:var(--acbg);border:1px solid var(--acln);display:flex;align-items:center;justify-content:center;font-size:.85rem;color:var(--ac);flex-shrink:0} |
| .art-info{flex:1;min-width:0} |
| .art-name{font-size:.8rem;font-weight:700;color:var(--t0);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| .art-meta{font-size:.64rem;color:var(--t3);margin-top:2px} |
| .art-btns{display:flex;gap:4px;flex-shrink:0;flex-wrap:wrap} |
| .ab{display:flex;align-items:center;gap:3px;padding:4px 8px;border-radius:5px;font-size:.66rem;font-weight:600;transition:.13s;white-space:nowrap} |
| .ab i{font-size:.6rem} |
| .ab.view{background:var(--acbg);border:1px solid var(--acln);color:var(--ac)}.ab.view:hover{background:var(--ac);color:#fff} |
| .ab.copy{background:var(--bg3);border:1px solid var(--ln2);color:var(--t2)}.ab.copy:hover{background:var(--bgh);color:var(--t0)} |
| .ab.run{background:#1f6feb18;border:1px solid #1f6feb50;color:#58a6ff}.ab.run:hover{background:#1f6feb;color:#fff} |
| .ab.dl{background:var(--gnbg);border:1px solid #22d48a35;color:var(--gn)}.ab.dl:hover{background:var(--gn);color:#000} |
| /* typing indicator */ |
| .typing-ind{display:flex;align-items:center;gap:8px;padding:9px 13px;background:var(--bg2);border:1px solid var(--ln2);border-radius:var(--r3);border-bottom-right-radius:3px;width:fit-content} |
| .typing-dots{display:flex;gap:4px} |
| .typing-dot{width:7px;height:7px;border-radius:50%;background:var(--ac);animation:td 1.4s ease infinite} |
| .typing-dot:nth-child(2){animation-delay:.2s}.typing-dot:nth-child(3){animation-delay:.4s} |
| @keyframes td{0%,80%,100%{transform:scale(.6);opacity:.3}40%{transform:scale(1.2);opacity:1}} |
| /* scroll button */ |
| #scrbtn{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);display:none;align-items:center;gap:4px;padding:6px 14px;background:var(--bg3);border:1px solid var(--ln3);border-radius:16px;font-size:.73rem;font-weight:700;color:var(--t1);box-shadow:var(--s2);transition:.13s;z-index:5;white-space:nowrap} |
| #scrbtn:hover{background:var(--bgh)} |
| #scrbtn.on{display:flex} |
| #ia{background:var(--bg1);border-top:1px solid var(--ln);padding:9px 10px 11px;padding-bottom:calc(11px + env(safe-area-inset-bottom));flex-shrink:0} |
| .ia-in{max-width:780px;margin:0 auto} |
| .ib{display:flex;align-items:flex-end;gap:6px;background:var(--bg2);border:1.5px solid var(--ln2);border-radius:var(--r4);padding:7px 8px;transition:.13s} |
| .ib:focus-within{border-color:var(--ac);box-shadow:0 0 0 3px #7c6fff16} |
| #ci{flex:1;background:none;border:none;color:var(--t0);font-size:16px;line-height:1.55;resize:none;min-height:24px;max-height:150px;direction:rtl;padding:2px 0;scrollbar-width:none;font-family:var(--font)} |
| @media(min-width:480px){#ci{font-size:.92rem}} |
| #ci::-webkit-scrollbar{display:none} |
| #ci::placeholder{color:var(--t3)} |
| #ci:disabled{opacity:.4} |
| .ibtn{width:36px;height:36px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:.85rem;transition:.13s;flex-shrink:0} |
| #sndbtn{background:var(--ac);color:#fff;box-shadow:0 2px 9px #7c6fff40} |
| #sndbtn:hover:not(:disabled){background:var(--ach)} |
| #sndbtn:active:not(:disabled){transform:scale(.9)} |
| #sndbtn:disabled{opacity:.3;cursor:default;box-shadow:none} |
| #stopbtn{background:var(--rdbg);color:var(--rd);border:1px solid #ff5f6d28;display:none} |
| #stopbtn.on{display:flex} |
| #stopbtn:hover{background:#ff5f6d1e} |
| .ifoot{display:flex;align-items:center;justify-content:space-between;margin-top:5px;padding:0 1px;font-size:.66rem;color:var(--t3)} |
| .ihnts{display:flex;align-items:center;gap:7px} |
| @media(max-width:480px){.ihnts{display:none}} |
| kbd{background:var(--bg3);border:1px solid var(--ln2);border-radius:3px;padding:1px 4px;font-family:var(--mono);font-size:.59rem} |
| .tkc span{color:var(--ac);font-weight:700} |
| #ov{display:none;position:fixed;inset:0;background:#0009;z-index:40} |
| #ov.on{display:block} |
| /* sandbox modal */ |
| #sbmod{position:fixed;inset:0;background:#000c;z-index:200;display:none;align-items:center;justify-content:center;padding:12px} |
| #sbmod.open{display:flex} |
| .sbw{background:var(--bg1);border:1px solid var(--ln2);border-radius:var(--r3);width:100%;max-width:1000px;height:90vh;max-height:90vh;display:flex;flex-direction:column;box-shadow:var(--s3);overflow:hidden} |
| .sbw-head{display:flex;align-items:center;gap:8px;padding:10px 14px;background:var(--bg2);border-bottom:1px solid var(--ln2);flex-shrink:0} |
| .sbw-title{flex:1;font-size:.85rem;font-weight:700;display:flex;align-items:center;gap:7px} |
| .sbw-title i{color:var(--ac)} |
| .sb-tabs{display:flex;gap:3px} |
| .sb-tab{padding:5px 12px;border-radius:5px;font-size:.74rem;font-weight:600;color:var(--t2);transition:.13s;cursor:pointer} |
| .sb-tab.on{background:var(--acbg);color:var(--ac);border:1px solid var(--acln)} |
| .sb-tab:hover:not(.on){background:var(--bgh);color:var(--t1)} |
| .sbw-close{width:28px;height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:.8rem;color:var(--t2);transition:.13s} |
| .sbw-close:hover{background:var(--rdbg);color:var(--rd)} |
| .sbw-body{flex:1;display:flex;overflow:hidden} |
| @media(max-width:600px){.sbw-body{flex-direction:column}} |
| .sb-code-pane{flex:1;min-width:0;display:flex;flex-direction:column;border-left:1px solid var(--ln2)} |
| .sb-prev-pane{flex:1;min-width:0;display:flex;flex-direction:column} |
| .sb-pane-lbl{padding:6px 12px;font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.09em;color:var(--t3);background:var(--bg2);border-bottom:1px solid var(--ln);flex-shrink:0} |
| .sb-pane-lbl i{margin-left:4px} |
| .sb-editor{flex:1;resize:none;background:#0d1117;border:none;color:#e6edf3;font-family:var(--mono);font-size:.8rem;line-height:1.65;padding:14px;overflow:auto;direction:ltr;text-align:left} |
| .sb-editor:focus{outline:none} |
| .sb-frame{flex:1;border:none;background:#fff} |
| .sbw-foot{display:flex;align-items:center;gap:8px;padding:8px 14px;background:var(--bg2);border-top:1px solid var(--ln2);flex-shrink:0} |
| .sb-run{display:flex;align-items:center;gap:5px;padding:6px 14px;background:var(--gn);color:#000;border-radius:var(--r1);font-size:.8rem;font-weight:700;transition:.13s} |
| .sb-run:hover{background:#2fea9e}.sb-run:active{transform:scale(.95)} |
| .sb-stat{font-size:.7rem;color:var(--t3);flex:1} |
| /* rename modal */ |
| .modal{position:fixed;inset:0;background:#0009;z-index:100;display:flex;align-items:center;justify-content:center;padding:20px} |
| .mbox{background:var(--bg2);border:1px solid var(--ln2);border-radius:var(--r3);padding:20px;width:100%;max-width:340px;box-shadow:var(--s3)} |
| .mttl{font-size:.94rem;font-weight:800;margin-bottom:12px} |
| .mi{width:100%;padding:8px 10px;background:var(--bg3);border:1.5px solid var(--ln2);border-radius:var(--r1);color:var(--t0);font-size:.88rem;direction:rtl;transition:.13s} |
| .mi:focus{border-color:var(--ac);outline:none} |
| .mbtns{display:flex;gap:7px;margin-top:12px;justify-content:flex-end} |
| .mok{padding:7px 15px;background:var(--ac);color:#fff;border-radius:var(--r1);font-weight:700;font-size:.82rem;transition:.13s} |
| .mok:hover{background:var(--ach)} |
| .mcc{padding:7px 15px;background:var(--bg4);color:var(--t1);border-radius:var(--r1);font-weight:600;font-size:.82rem;border:1px solid var(--ln2);transition:.13s} |
| .mcc:hover{background:var(--bgh)} |
| /* toast */ |
| #tw{position:fixed;bottom:72px;left:50%;transform:translateX(-50%);z-index:300;display:flex;flex-direction:column;align-items:center;gap:5px;pointer-events:none} |
| .toast{padding:7px 15px;background:var(--bg3);border:1px solid var(--ln2);border-radius:16px;font-size:.77rem;font-weight:600;color:var(--t1);box-shadow:var(--s2);white-space:nowrap;animation:tp .2s ease} |
| .toast.ok{color:var(--gn);border-color:#22d48a35}.toast.er{color:var(--rd);border-color:#ff5f6d35} |
| @keyframes tp{from{opacity:0;transform:translateY(7px)scale(.87)}to{opacity:1;transform:none}} |
| /* show-thinking toggle in sidebar */ |
| .think-tog-row{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;background:var(--bg3);border:1px solid var(--ln2);border-radius:var(--r1)} |
| </style> |
| </head> |
| <body> |
| <div id="app"> |
| <aside id="sb"> |
| <div class="sb-head"> |
| <button class="new-btn" id="newConvBtn"><i class="fa-solid fa-plus"></i> محادثة جديدة</button> |
| </div> |
| <div class="cl" id="cl"></div> |
| <div class="sb-bot"> |
| <div><div class="lbl"><i class="fa-solid fa-plug"></i>المزود</div> |
| <select class="sel" id="psel"><option value="">⏳ جاري التحميل...</option></select></div> |
| <div><div class="lbl"><i class="fa-solid fa-brain"></i>النموذج</div> |
| <select class="sel" id="msel"><option value="">اختر مزوداً</option></select></div> |
| <div> |
| <div class="think-tog-row"> |
| <span class="tog-lbl"><i class="fa-solid fa-brain" style="color:var(--am)"></i>عرض التفكير</span> |
| <label class="tog"><input type="checkbox" id="showThinkTog" checked><div class="tog-t"></div></label> |
| </div> |
| </div> |
| <div class="tog-row"> |
| <span class="tog-lbl"><i class="fa-solid fa-bolt"></i>البث المباشر</span> |
| <label class="tog"><input type="checkbox" id="stog" checked><div class="tog-t"></div></label> |
| </div> |
| <div class="tog-row"> |
| <span class="tog-lbl"><i class="fa-solid fa-lightbulb"></i>اقتراحات المتابعة</span> |
| <label class="tog"><input type="checkbox" id="fupTog" checked><div class="tog-t"></div></label> |
| </div> |
| <div class="tog-row"> |
| <span class="tog-lbl"><i class="fa-solid fa-circle-half-stroke"></i>الوضع الفاتح</span> |
| <label class="tog"><input type="checkbox" id="themeTog"><div class="tog-t"></div></label> |
| </div> |
| <div><div class="lbl"><i class="fa-solid fa-scroll"></i>تعليمات النظام</div> |
| <textarea class="sys-ta" id="sysp" placeholder="أنت مساعد ذكاء اصطناعي مفيد..."></textarea></div> |
| </div> |
| </aside> |
| <div id="ov"></div> |
| <div id="main"> |
| <header id="hdr"> |
| <button class="hb" id="sbToggleBtn"><i class="fa-solid fa-bars"></i></button> |
| <div class="logo"><div class="logo-mk"><i class="fa-solid fa-robot"></i></div>g4f<span class="logo-ac">pro</span></div> |
| <div class="hdr-m"><div class="hdr-t" id="htitle">محادثة جديدة</div></div> |
| <div class="hdr-r"> |
| <div class="hst">متصل</div> |
| <div class="ppill"><div class="pd"></div><span id="ptxt">Auto</span></div> |
| <button class="hb" id="thbtn"><i class="fa-solid fa-moon"></i></button> |
| <button class="hb" id="reloadBtn"><i class="fa-solid fa-rotate" id="rlic"></i></button> |
| </div> |
| </header> |
| <div id="mwrap"> |
| <div id="msgs"></div> |
| <button id="scrbtn"><i class="fa-solid fa-arrow-down"></i> للأسفل</button> |
| </div> |
| <div id="ia"> |
| <div class="ia-in"> |
| <div class="ib"> |
| <textarea id="ci" placeholder="اكتب رسالتك..." rows="1" disabled></textarea> |
| <button class="ibtn" id="stopbtn"><i class="fa-solid fa-stop"></i></button> |
| <button class="ibtn" id="sndbtn" disabled><i class="fa-solid fa-paper-plane"></i></button> |
| </div> |
| <div class="ifoot"> |
| <div class="ihnts"><span><kbd>Enter</kbd> إرسال</span><span><kbd>Shift+Enter</kbd> سطر</span></div> |
| <div class="tkc">~<span id="tkc">0</span> توكن</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div class="modal" id="rmod" style="display:none"> |
| <div class="mbox"> |
| <div class="mttl">إعادة تسمية المحادثة</div> |
| <input class="mi" id="rmi" type="text" placeholder="اسم المحادثة..."> |
| <div class="mbtns"> |
| <button class="mcc" id="renCancelBtn">إلغاء</button> |
| <button class="mok" id="renSaveBtn">حفظ</button> |
| </div> |
| </div> |
| </div> |
| |
| <div id="sbmod"> |
| <div class="sbw"> |
| <div class="sbw-head"> |
| <div class="sbw-title"><i class="fa-solid fa-code"></i><span id="sb-fn">index.html</span></div> |
| <div class="sb-tabs"> |
| <div class="sb-tab on" data-tab="split">تقسيم</div> |
| <div class="sb-tab" data-tab="code">كود</div> |
| <div class="sb-tab" data-tab="preview">معاينة</div> |
| </div> |
| <button class="sbw-close" id="sbCloseBtn"><i class="fa-solid fa-xmark"></i></button> |
| </div> |
| <div class="sbw-body"> |
| <div class="sb-code-pane" id="sb-cpane"> |
| <div class="sb-pane-lbl"><i class="fa-solid fa-code" style="color:var(--ac)"></i>محرر الكود</div> |
| <textarea class="sb-editor" id="sb-ed" spellcheck="false" dir="ltr"></textarea> |
| </div> |
| <div class="sb-prev-pane" id="sb-ppane"> |
| <div class="sb-pane-lbl"><i class="fa-solid fa-eye" style="color:var(--tl)"></i>معاينة حية</div> |
| <iframe class="sb-frame" id="sb-iframe" sandbox="allow-scripts allow-same-origin" title="preview"></iframe> |
| </div> |
| </div> |
| <div class="sbw-foot"> |
| <button class="sb-run" id="sbRunBtn"><i class="fa-solid fa-play"></i> تشغيل</button> |
| <span class="sb-stat" id="sb-stat">جاهز</span> |
| </div> |
| </div> |
| </div> |
| <div id="tw"></div> |
|
|
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.1.1/marked.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> |
| <script> |
| 'use strict'; |
| |
| |
| |
| |
| const Utils = (() => { |
| function esc(s) { |
| return String(s) |
| .replace(/&/g,'&').replace(/</g,'<') |
| .replace(/>/g,'>').replace(/"/g,'"'); |
| } |
| function fmtBytes(n) { return n>1024?(n/1024).toFixed(1)+'KB':n+'B'; } |
| function uid() { return 'c-'+Date.now()+'-'+Math.random().toString(36).slice(2,8); } |
| function groupByDate(convs) { |
| const now=Date.now(); |
| const groups={'اليوم':[],'أمس':[],'هذا الأسبوع':[],'أقدم':[]}; |
| for(const c of convs){ |
| const d=now-c.created; |
| if(d<86400000) groups['اليوم'].push(c); |
| else if(d<172800000) groups['أمس'].push(c); |
| else if(d<604800000) groups['هذا الأسبوع'].push(c); |
| else groups['أقدم'].push(c); |
| } |
| return groups; |
| } |
| function suggestFollowUps(text) { |
| const l=text.toLowerCase(); |
| if(l.includes('html')||l.includes('css')||l.includes('كود')||l.includes('python')||l.includes('javascript')) |
| return ['اشرح هذا الكود','كيف أحسّنه؟','أضف ميزة جديدة']; |
| if(l.includes('شرح')||l.includes('مفهوم')) |
| return ['أعطني مثالاً عملياً','ما الفرق مع البدائل؟']; |
| return ['أخبرني أكثر','اشرح بمثال']; |
| } |
| function wrapForPreview(code,lang) { |
| if(lang==='css') return `<!DOCTYPE html><html><head><style>${code}</style></head><body></body></html>`; |
| if(lang==='js'||lang==='javascript') return `<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><div id="out"></div><script>${code}<\/script></body></html>`; |
| return code; |
| } |
| function extractArtifacts(text) { |
| const WEB = new Set(['html', 'css', 'js', 'javascript']); |
| const results = []; |
| |
| const re = /```(\w+)?\n([\s\S]*?)```/g; |
| let m; |
| while ((m = re.exec(text)) !== null) { |
| const lang = (m[1] || 'text').toLowerCase(); |
| const code = m[2].trim(); |
| |
| if (code.length < 30) continue; |
| results.push({ |
| lang, |
| code, |
| filename: `code.${lang === 'javascript' ? 'js' : lang}` |
| }); |
| } |
| return results; |
| } |
| |
| return {esc,fmtBytes,uid,groupByDate,suggestFollowUps,wrapForPreview,extractArtifacts}; |
| })(); |
| |
| |
| |
| |
| const Store = (() => { |
| const get=(k,d=null)=>{try{const v=localStorage.getItem(k);return v===null?d:JSON.parse(v);}catch{return d;}}; |
| const set=(k,v)=>{try{localStorage.setItem(k,JSON.stringify(v));}catch{}}; |
| return { |
| getConvs: () => get('g4c',[]), |
| setConvs: (v) => set('g4c',v), |
| getActiveId: () => get('g4a',''), |
| setActiveId: (v) => set('g4a',v), |
| getTheme: () => get('g4t','dark'), |
| setTheme: (v) => set('g4t',v), |
| getProvider: () => get('g4p',''), |
| setProvider: (v) => set('g4p',v), |
| getModel: () => get('g4m',''), |
| setModel: (v) => set('g4m',v), |
| getStream: () => get('g4s','1')==='1', |
| setStream: (v) => set('g4s',v?'1':'0'), |
| getShowThink: () => get('g4th','1')==='1', |
| setShowThink: (v) => set('g4th',v?'1':'0'), |
| getShowFup: () => get('g4fu','1')==='1', |
| setShowFup: (v) => set('g4fu',v?'1':'0'), |
| getDraft: () => get('g4d',''), |
| setDraft: (v) => set('g4d',v), |
| }; |
| })(); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const API = (() => { |
| const post=(url,body)=>fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); |
| async function getProviders(){const r=await fetch('/api/providers');return r.json();} |
| async function getModels(provider){const r=await fetch(`/api/models/${provider}`);return r.json();} |
| async function reload(){return post('/api/reload',{});} |
| async function chat(payload){const r=await post('/chat',payload);return r.json();} |
| function streamChat(payload){return post('/chat/stream',payload);} |
| return {getProviders,getModels,reload,chat,streamChat}; |
| })(); |
| |
| |
| |
| |
| const MD = (() => { |
| const RUNNABLE=new Set(['html','css','js','javascript','jsx','tsx','svg']); |
| function _hi(code,lang){ |
| try{ |
| if(lang&&lang!=='text'&&hljs.getLanguage(lang)) return hljs.highlight(code,{language:lang}).value; |
| return hljs.highlightAuto(code).value; |
| }catch{return Utils.esc(code);} |
| } |
| function _buildBlock(lang,raw){ |
| const hi=_hi(raw,lang); |
| const runBtn=RUNNABLE.has(lang) |
| ?`<button class="cb-run" data-action="run"><i class="fa-solid fa-play"></i> تشغيل</button>`:'' |
| return `<div class="cbw"> |
| <div class="cbh"> |
| <span class="cb-lang l-${Utils.esc(lang)}" data-lang="${Utils.esc(lang)}">${Utils.esc(lang)||'text'}</span> |
| <div class="cb-btns">${runBtn}<button class="cb-copy" data-action="copy"><i class="fa-regular fa-copy"></i> نسخ</button></div> |
| </div> |
| <pre><code class="hljs language-${Utils.esc(lang)}">${hi}</code></pre> |
| </div>`; |
| } |
| function render(text){ |
| if(!text)return''; |
| const blocks=[]; |
| const ph=(lang,code)=>{const i=blocks.length;blocks.push({lang:(lang||'text').toLowerCase(),code:code.trim()});return`%%CB${i}%%`;}; |
| const withPH = text.replace(/```(\w+)?\n([\s\S]+?)\n```/g, (_, lang, code) => ph(lang, code)); |
| |
| let html; |
| try{html=marked.parse(withPH);}catch{html=Utils.esc(withPH);} |
| html=html.replace(/%%CB(\d+)%%/g,(_,i)=>{const{lang,code}=blocks[parseInt(i)];return _buildBlock(lang,code);}); |
| return html; |
| } |
| return {render}; |
| })(); |
| |
| |
| |
| |
| const Sandbox = (() => { |
| let _deb=null; |
| const el={ |
| modal:()=>document.getElementById('sbmod'), |
| fname:()=>document.getElementById('sb-fn'), |
| editor:()=>document.getElementById('sb-ed'), |
| frame:()=>document.getElementById('sb-iframe'), |
| status:()=>document.getElementById('sb-stat'), |
| cpane:()=>document.getElementById('sb-cpane'), |
| ppane:()=>document.getElementById('sb-ppane'), |
| }; |
| function _write(code){ |
| try{ |
| const doc=el.frame().contentDocument||el.frame().contentWindow.document; |
| doc.open();doc.write(code);doc.close(); |
| el.status().textContent='✅ تم التشغيل';el.status().style.color='var(--gn)'; |
| }catch(e){el.status().textContent='❌ '+e.message.slice(0,60);el.status().style.color='var(--rd)';} |
| } |
| function open(code,filename){ |
| el.fname().textContent=filename||'code'; |
| el.editor().value=code||''; |
| el.modal().classList.add('open'); |
| setTab('split');_write(code); |
| el.editor().oninput=()=>{clearTimeout(_deb);_deb=setTimeout(()=>_write(el.editor().value),400);}; |
| } |
| function close(){el.modal().classList.remove('open');el.editor().oninput=null;clearTimeout(_deb);} |
| function run(){_write(el.editor().value);} |
| function setTab(tab){ |
| document.querySelectorAll('.sb-tab').forEach(t=>t.classList.toggle('on',t.dataset.tab===tab)); |
| const show=(pane,vis)=>{pane.style.display=vis?'flex':'none';}; |
| show(el.cpane(),tab!=='preview');show(el.ppane(),tab!=='code'); |
| } |
| return {open,close,run,setTab}; |
| })(); |
| |
| |
| |
| |
| const UI = (() => { |
| function msgHeader(role,model,time,pinned){ |
| const hdr=document.createElement('div');hdr.className='mhdr'; |
| const av=document.createElement('div');av.className=role==='user'?'mav u':'mav b'; |
| av.innerHTML=role==='user'?'<i class="fa-solid fa-user"></i>':'<i class="fa-solid fa-robot"></i>'; |
| const ts=document.createElement('span');ts.textContent=time; |
| if(role==='user'){ |
| if(pinned)hdr.appendChild(_pin());hdr.appendChild(ts);hdr.appendChild(av); |
| } else { |
| hdr.appendChild(av); |
| if(model){const tag=document.createElement('span');tag.className='mtag';tag.textContent=model;hdr.appendChild(tag);} |
| hdr.appendChild(ts);if(pinned)hdr.appendChild(_pin()); |
| } |
| return hdr; |
| } |
| function _pin(){const s=document.createElement('span');s.className='pinbdg';s.innerHTML='<i class="fa-solid fa-thumbtack"></i>';return s;} |
| function msgActions(role,idx,pinned){ |
| const div=document.createElement('div');div.className='macts'; |
| if(role==='user'){ |
| div.innerHTML=` |
| <button class="ma" data-action="copy" data-idx="${idx}"><i class="fa-regular fa-copy"></i> نسخ</button> |
| <button class="ma" data-action="edit" data-idx="${idx}"><i class="fa-solid fa-pen"></i> تعديل</button> |
| <button class="ma pn${pinned?' on':''}" data-action="pin" data-idx="${idx}"><i class="fa-solid fa-thumbtack"></i></button>`; |
| } else { |
| div.innerHTML=` |
| <button class="ma" data-action="copy" data-idx="${idx}"><i class="fa-regular fa-copy"></i> نسخ</button> |
| <button class="ma rg" data-action="regen" data-idx="${idx}"><i class="fa-solid fa-rotate-right"></i> إعادة</button> |
| <button class="ma pn${pinned?' on':''}" data-action="pin" data-idx="${idx}"><i class="fa-solid fa-thumbtack"></i></button>`; |
| } |
| return div; |
| } |
| function thinkingBlock(lines,summary){ |
| const div=document.createElement('div');div.className='thinking-block'; |
| const sumText=summary||(lines.length?lines[0].slice(0,60)+'…':''); |
| const linesHTML=lines.map(l=>`<div class="thinking-line">${Utils.esc(l)}</div>`).join(''); |
| div.innerHTML=` |
| <div class="thinking-header"> |
| <i class="thinking-icon fa-solid fa-brain"></i> |
| <span class="thinking-label">تفكير النموذج (${lines.length} سطر)</span> |
| <span class="thinking-summary">${Utils.esc(sumText)}</span> |
| <i class="thinking-toggle fa-solid fa-chevron-down"></i> |
| </div> |
| <div class="thinking-body">${linesHTML}</div>`; |
| div.querySelector('.thinking-header').addEventListener('click',()=>div.classList.toggle('open')); |
| return div; |
| } |
| function artifactCard(art){ |
| const ICONS={html:'fa-file-code',css:'fa-palette',js:'fa-square-js',javascript:'fa-square-js',jsx:'fa-atom',tsx:'fa-atom',svg:'fa-image'}; |
| const icon=ICONS[art.lang]||'fa-code'; |
| const div=document.createElement('div');div.className='artifact';div._artData=art; |
| div.innerHTML=` |
| <div class="art-icon"><i class="fa-solid ${icon}"></i></div> |
| <div class="art-info"> |
| <div class="art-name">${Utils.esc(art.filename)}</div> |
| <div class="art-meta">${art.lang.toUpperCase()} · ${Utils.fmtBytes(art.bytes)}</div> |
| </div> |
| <div class="art-btns"> |
| <button class="ab view" data-action="art-view"><i class="fa-solid fa-eye"></i> عرض</button> |
| <button class="ab copy" data-action="art-copy"><i class="fa-regular fa-copy"></i> نسخ</button> |
| <button class="ab run" data-action="art-run"><i class="fa-solid fa-play"></i> تشغيل</button> |
| <button class="ab dl" data-action="art-dl"><i class="fa-solid fa-download"></i></button> |
| </div>`; |
| return div; |
| } |
| function typingRow(id,label){ |
| const row=document.createElement('div');row.className='mr assistant';row.id=id; |
| const lbl=label||'يكتب...'; |
| row.innerHTML=` |
| <div class="mhdr"><div class="mav b"><i class="fa-solid fa-robot"></i></div></div> |
| <div class="typing-ind"> |
| <div class="typing-dots"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div> |
| <span style="font-size:.73rem;color:var(--t2)">${Utils.esc(lbl)}</span> |
| </div>`; |
| return row; |
| } |
| function welcomeScreen(){ |
| const div=document.createElement('div');div.id='wlc'; |
| div.innerHTML=` |
| <div class="wico"><i class="fa-solid fa-robot"></i></div> |
| <div class="wt">مرحباً في <span>g4fpro</span></div> |
| <div class="ws">واجهة AI تدعم جميع مزودي g4f تلقائياً — بدون مفاتيح API</div> |
| <div class="sg"> |
| <div class="sc" data-prompt="اشرح الذكاء الاصطناعي بأسلوب بسيط"><span class="sc-i">🤖</span><div class="sc-t">اشرح الذكاء الاصطناعي</div><div class="sc-s">تفسير مبسط</div></div> |
| <div class="sc" data-prompt="اكتب صفحة HTML احترافية مع CSS"><span class="sc-i">💻</span><div class="sc-t">صفحة HTML + CSS</div><div class="sc-s">مع معاينة مباشرة</div></div> |
| <div class="sc" data-prompt="اكتب رسالة بريد إلكتروني احترافية لطلب اجتماع"><span class="sc-i">✉️</span><div class="sc-t">رسالة احترافية</div><div class="sc-s">بريد عمل</div></div> |
| <div class="sc" data-prompt="ما أفضل 5 مكتبات Python لعام 2025 مع أمثلة؟"><span class="sc-i">📊</span><div class="sc-t">أفضل مكتبات 2025</div><div class="sc-s">Python</div></div> |
| </div>`; |
| return div; |
| } |
| function followUpRow(suggestions){ |
| const div=document.createElement('div');div.className='fups'; |
| for(const s of suggestions){ |
| const btn=document.createElement('button'); |
| btn.className='fup';btn.textContent=s;btn.dataset.prompt=s; |
| div.appendChild(btn); |
| } |
| return div; |
| } |
| function toast(container,msg,type=''){ |
| const el=document.createElement('div');el.className=`toast ${type}`.trim();el.textContent=msg; |
| container.appendChild(el); |
| setTimeout(()=>{el.style.transition='opacity .3s';el.style.opacity='0';setTimeout(()=>el.remove(),300);},2400); |
| } |
| return {msgHeader,msgActions,thinkingBlock,artifactCard,typingRow,welcomeScreen,followUpRow,toast}; |
| })(); |
| |
| |
| |
| |
| const App = (() => { |
| |
| const state = { |
| convs: Store.getConvs(), |
| activeId: Store.getActiveId(), |
| provs: {}, |
| sbOpen: window.innerWidth > 680, |
| busy: false, |
| stopReq: false, |
| renId: null, |
| userScrolled:false, |
| }; |
| const active=()=>state.convs.find(c=>c.id===state.activeId)||null; |
| function persist(){Store.setConvs(state.convs);Store.setActiveId(state.activeId);} |
| function getServerId(conv){if(!conv.serverId){conv.serverId=Utils.uid();persist();}return conv.serverId;} |
| |
| |
| const $=id=>document.getElementById(id); |
| const DOM={ |
| msgs: ()=>$('msgs'), mwrap: ()=>$('mwrap'), |
| ci: ()=>$('ci'), sndbtn: ()=>$('sndbtn'), |
| stopbtn:()=>$('stopbtn'),psel: ()=>$('psel'), |
| msel: ()=>$('msel'), stog: ()=>$('stog'), |
| sysp: ()=>$('sysp'), htitle: ()=>$('htitle'), |
| tw: ()=>$('tw'), cl: ()=>$('cl'), |
| ptxt: ()=>$('ptxt'), scrbtn: ()=>$('scrbtn'), |
| rmod: ()=>$('rmod'), rmi: ()=>$('rmi'), |
| rlic: ()=>$('rlic'), sb: ()=>$('sb'), |
| ov: ()=>$('ov'), thbtn: ()=>$('thbtn'), |
| showThinkTog: ()=>$('showThinkTog'), |
| fupTog: ()=>$('fupTog'), |
| themeTog: ()=>$('themeTog'), |
| }; |
| |
| |
| function scrollBot(force=false){ |
| if(!force&&state.userScrolled)return; |
| const w=DOM.mwrap(); |
| w.scrollTo({top:w.scrollHeight,behavior:'smooth'}); |
| } |
| function _onScroll(){ |
| const w=DOM.mwrap(); |
| const atBottom=w.scrollHeight-w.scrollTop-w.clientHeight<120; |
| DOM.scrbtn().classList.toggle('on',!atBottom); |
| state.userScrolled=!atBottom; |
| } |
| |
| |
| function toast(msg,type=''){UI.toast(DOM.tw(),msg,type);} |
| |
| |
| function setBusy(on){ |
| state.busy=on; |
| DOM.sndbtn().disabled=on;DOM.ci().disabled=on; |
| DOM.stopbtn().classList.toggle('on',on); |
| if(!on){state.stopReq=false;DOM.ci().focus();} |
| } |
| function enableInput(){ |
| DOM.ci().disabled=false;DOM.sndbtn().disabled=false;DOM.ci().focus(); |
| const draft=Store.getDraft(); |
| if(draft){DOM.ci().value=draft;_resizeTa(DOM.ci());_updTok(draft);} |
| } |
| |
| |
| function _resizeTa(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,150)+'px';} |
| function _updTok(text){$('tkc').textContent=Math.round(text.length/4);} |
| function _onInput(){const ci=DOM.ci();_resizeTa(ci);_updTok(ci.value);Store.setDraft(ci.value);} |
| |
| |
| function newConv(){ |
| const id='c'+Date.now(); |
| const conv={id,title:'محادثة جديدة',msgs:[],created:Date.now(),pins:[]}; |
| state.convs.unshift(conv);setActive(id);persist();renderConvList(); |
| toast('✨ محادثة جديدة','ok'); |
| if(window.innerWidth<=680)closeSidebar(); |
| } |
| function setActive(id){ |
| state.activeId=id;persist();renderConvList();renderMessages(); |
| const c=state.convs.find(x=>x.id===id); |
| DOM.htitle().textContent=c?c.title:'محادثة'; |
| } |
| function deleteConv(id){ |
| if(!confirm('حذف هذه المحادثة نهائياً؟'))return; |
| state.convs=state.convs.filter(c=>c.id!==id); |
| if(state.activeId===id)state.activeId=state.convs[0]?.id||''; |
| persist();renderConvList();renderMessages(); |
| } |
| function startRename(id){ |
| state.renId=id;const c=state.convs.find(x=>x.id===id); |
| DOM.rmi().value=c?.title||'';DOM.rmod().style.display='flex'; |
| setTimeout(()=>DOM.rmi().focus(),40); |
| } |
| function confirmRename(){ |
| const v=DOM.rmi().value.trim();if(!v||!state.renId){closeModal();return;} |
| const c=state.convs.find(x=>x.id===state.renId);if(c)c.title=v; |
| persist();renderConvList(); |
| if(state.activeId===state.renId)DOM.htitle().textContent=v; |
| closeModal(); |
| } |
| function closeModal(){DOM.rmod().style.display='none';state.renId=null;} |
| |
| |
| function renderConvList(){ |
| const el=DOM.cl(); |
| if(!state.convs.length){ |
| el.innerHTML='<div style="padding:22px 10px;text-align:center;color:var(--t3);font-size:.77rem"><i class="fa-regular fa-comment" style="font-size:1.3rem;display:block;margin-bottom:8px;opacity:.28"></i>لا توجد محادثات</div>'; |
| return; |
| } |
| const frag=document.createDocumentFragment(); |
| const groups=Utils.groupByDate(state.convs); |
| for(const [label,list] of Object.entries(groups)){ |
| if(!list.length)continue; |
| const grpEl=document.createElement('div');grpEl.className='grp';grpEl.textContent=label;frag.appendChild(grpEl); |
| for(const conv of list){ |
| const lm=conv.msgs[conv.msgs.length-1]; |
| const prev=lm?lm.content.slice(0,32)+(lm.content.length>32?'…':''):'لا توجد رسائل'; |
| const item=document.createElement('div'); |
| item.className=`ci${conv.id===state.activeId?' on':''}`;item.dataset.cid=conv.id; |
| item.innerHTML=` |
| <div class="ci-dot"></div> |
| <div class="ci-info"> |
| <div class="ci-title">${Utils.esc(conv.title)}</div> |
| <div class="ci-sub">${Utils.esc(prev)}</div> |
| </div> |
| <div class="ci-acts"> |
| <button class="cia" data-action="rename" data-cid="${conv.id}" title="تسمية"><i class="fa-solid fa-pen"></i></button> |
| <button class="cia del" data-action="delete" data-cid="${conv.id}" title="حذف"><i class="fa-solid fa-trash"></i></button> |
| </div>`; |
| frag.appendChild(item); |
| } |
| } |
| el.innerHTML='';el.appendChild(frag); |
| } |
| |
| |
| function renderMessages(){ |
| const conv=active();const wrap=DOM.msgs(); |
| if(!conv||!conv.msgs.length){wrap.innerHTML='';wrap.appendChild(UI.welcomeScreen());return;} |
| const frag=document.createDocumentFragment(); |
| conv.msgs.forEach((m,i)=>frag.appendChild(buildMsgRow(m,i))); |
| wrap.innerHTML='';wrap.appendChild(frag); |
| state.userScrolled=false;scrollBot(true); |
| } |
| |
| function buildMsgRow(msg,idx){ |
| const conv=active(); |
| const pinned=conv?.pins?.includes(idx)||false; |
| const t=new Date(msg.ts||Date.now()).toLocaleTimeString('ar-EG',{hour:'2-digit',minute:'2-digit'}); |
| const row=document.createElement('div');row.className=`mr ${msg.role}`;row.dataset.i=idx; |
| if(msg.role!=='err') row.appendChild(UI.msgHeader(msg.role,msg.model||'',t,pinned)); |
| |
| |
| const showThink=DOM.showThinkTog().checked; |
| if(showThink&&msg.thinking&&msg.thinking.length){ |
| row.appendChild(UI.thinkingBlock(msg.thinking)); |
| } |
| |
| |
| const bub=document.createElement('div');bub.className='mb'; |
| if(msg.role==='assistant'){ |
| bub.innerHTML=MD.render(msg.content); |
| } else { |
| const span=document.createElement('span'); |
| const lines=msg.content.split('\n'); |
| lines.forEach((line,i)=>{ |
| span.appendChild(document.createTextNode(line)); |
| if(i<lines.length-1)span.appendChild(document.createElement('br')); |
| }); |
| bub.appendChild(span); |
| } |
| row.appendChild(bub); |
| if(msg.role==='user'||msg.role==='assistant') row.appendChild(UI.msgActions(msg.role,idx,pinned)); |
| if(msg.role==='assistant') Utils.extractArtifacts(msg.content).forEach(art=>row.appendChild(UI.artifactCard(art))); |
| return row; |
| } |
| |
| |
| function appendUserMsg(msg,idx){ |
| $('wlc')?.remove(); |
| DOM.msgs().appendChild(buildMsgRow(msg,idx)); |
| } |
| |
| |
| async function send(text){ |
| text=(text||DOM.ci().value).trim(); |
| if(!text||state.busy)return; |
| const prov=DOM.psel().value;const model=DOM.msel().value; |
| if(!prov){toast('⚠️ اختر مزوداً أولاً','er');return;} |
| if(!state.activeId||!active())newConv(); |
| const conv=active();if(!conv)return; |
| $('wlc')?.remove(); |
| const um={role:'user',content:text,ts:Date.now()}; |
| conv.msgs.push(um); |
| if(conv.msgs.length===1){conv.title=text.slice(0,40)+(text.length>40?'…':'');DOM.htitle().textContent=conv.title;} |
| persist();renderConvList(); |
| appendUserMsg(um,conv.msgs.length-1); |
| DOM.ci().value='';Store.setDraft('');_resizeTa(DOM.ci());_updTok(''); |
| setBusy(true);state.userScrolled=false;scrollBot(true); |
| const lid='tp'+Date.now(); |
| const typRow=UI.typingRow(lid,'يفكر...'); |
| DOM.msgs().appendChild(typRow);scrollBot(); |
| const payload={ |
| message:text,model,provider:prov, |
| system_prompt:DOM.sysp().value.trim(), |
| conversation_id:getServerId(conv), |
| }; |
| try{ |
| if(DOM.stog().checked) await _doStream(payload,lid,conv,model); |
| else await _doSync(payload,lid,conv,model); |
| }catch(e){ |
| document.getElementById(lid)?.remove(); |
| const errRow=document.createElement('div');errRow.className='mr err'; |
| errRow.innerHTML=`<div class="mb">❌ ${Utils.esc(e.message)}</div>`; |
| DOM.msgs().appendChild(errRow);setBusy(false); |
| } |
| } |
| |
| |
| async function _doSync(payload,lid,conv,model){ |
| const data=await API.chat(payload); |
| document.getElementById(lid)?.remove(); |
| if(data.conversation_id)conv.serverId=data.conversation_id; |
| const content=data.ok?data.reply:'❌ '+(data.error||'خطأ'); |
| |
| const thinking=data.ok?(data.thinking||[]):[]; |
| const bm={role:'assistant',content,thinking,model,ts:Date.now()}; |
| conv.msgs.push(bm);persist(); |
| DOM.msgs().appendChild(buildMsgRow(bm,conv.msgs.length-1)); |
| if(DOM.fupTog().checked) _appendFollowUps(content); |
| setBusy(false);scrollBot(); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| async function _doStream(payload,lid,conv,model){ |
| state.stopReq=false; |
| const res=await API.streamChat(payload); |
| document.getElementById(lid)?.remove(); |
| |
| const bm={role:'assistant',content:'',thinking:[],model,ts:Date.now()}; |
| conv.msgs.push(bm); |
| const idx=conv.msgs.length-1; |
| |
| const row=document.createElement('div');row.className='mr assistant';row.dataset.i=idx; |
| const hdr=document.createElement('div');hdr.className='mhdr'; |
| hdr.innerHTML=`<div class="mav b"><i class="fa-solid fa-robot"></i></div>${model?`<span class="mtag">${Utils.esc(model)}</span>`:''}`; |
| row.appendChild(hdr); |
| |
| |
| let thinkBlockEl=null; |
| let thinkBodyEl=null; |
| |
| |
| const bub=document.createElement('div');bub.className='mb'; |
| bub.innerHTML='<span class="cur"></span>'; |
| row.appendChild(bub); |
| DOM.msgs().appendChild(row);scrollBot(); |
| |
| const reader=res.body.getReader();const dec=new TextDecoder(); |
| let full='',sseBuffer='',raf=null; |
| const schedRender=()=>{ |
| if(raf)return; |
| raf=requestAnimationFrame(()=>{ |
| bub.innerHTML=MD.render(full)+'<span class="cur"></span>'; |
| scrollBot();raf=null; |
| }); |
| }; |
| |
| try{ |
| while(true){ |
| if(state.stopReq){reader.cancel();break;} |
| const{value,done}=await reader.read();if(done)break; |
| sseBuffer+=dec.decode(value,{stream:true}); |
| const lines=sseBuffer.split('\n');sseBuffer=lines.pop(); |
| for(const line of lines){ |
| if(!line.startsWith('data: '))continue; |
| let ev;try{ev=JSON.parse(line.slice(6));}catch{continue;} |
| |
| if(ev.type==='start'){ |
| if(ev.conversation_id)conv.serverId=ev.conversation_id; |
| } |
| else if(ev.type==='thinking'&&ev.lines?.length){ |
| |
| bm.thinking=ev.lines; |
| const showThink=DOM.showThinkTog().checked; |
| if(showThink&&!thinkBlockEl){ |
| thinkBlockEl=UI.thinkingBlock(ev.lines); |
| thinkBlockEl.classList.add('streaming'); |
| row.insertBefore(thinkBlockEl,bub); |
| thinkBodyEl=thinkBlockEl.querySelector('.thinking-body'); |
| } else if(showThink&&thinkBlockEl){ |
| |
| if(thinkBodyEl){ |
| thinkBodyEl.innerHTML=ev.lines.map(l=>`<div class="thinking-line">${Utils.esc(l)}</div>`).join(''); |
| } |
| } |
| } |
| else if(ev.type==='chunk'&&ev.content){ |
| |
| if(thinkBlockEl)thinkBlockEl.classList.remove('streaming'); |
| full+=ev.content;schedRender(); |
| } |
| else if(ev.type==='error'){ |
| full+='\n❌ '+ev.content;schedRender(); |
| } |
| else if(ev.type==='done'){ |
| break; |
| } |
| } |
| } |
| }catch(e){} |
| |
| |
| cancelAnimationFrame(raf); |
| bub.innerHTML=MD.render(full||'(لا يوجد رد)'); |
| if(thinkBlockEl)thinkBlockEl.classList.remove('streaming'); |
| |
| bm.content=full;persist(); |
| |
| |
| const finalRow=buildMsgRow(bm,idx); |
| row.replaceWith(finalRow); |
| if(DOM.fupTog().checked) _appendFollowUps(full); |
| setBusy(false);scrollBot(); |
| } |
| |
| function _appendFollowUps(text){ |
| if(!text||text.startsWith('❌'))return; |
| const row=UI.followUpRow(Utils.suggestFollowUps(text)); |
| DOM.msgs().appendChild(row);scrollBot(); |
| } |
| |
| |
| function copyMsg(idx){ |
| const conv=active(); |
| navigator.clipboard?.writeText(conv?.msgs[idx]?.content||''); |
| toast('✅ تم النسخ','ok'); |
| } |
| |
| function copyCode(btn){ |
| const code=btn.closest('.cbw')?.querySelector('code'); |
| if(!code)return; |
| navigator.clipboard?.writeText(code.textContent); |
| const orig=btn.innerHTML; |
| btn.innerHTML='<i class="fa-solid fa-check"></i> تم'; |
| setTimeout(()=>{btn.innerHTML=orig;},1500); |
| toast('✅ تم نسخ الكود','ok'); |
| } |
| |
| function runCodeBlock(btn){ |
| const cbw=btn.closest('.cbw'); |
| const code=cbw?.querySelector('code')?.textContent||''; |
| const lang=cbw?.querySelector('.cb-lang')?.dataset.lang||'html'; |
| Sandbox.open(Utils.wrapForPreview(code,lang),`code.${lang==='javascript'?'js':lang}`); |
| } |
| |
| function editMsg(idx){ |
| const conv=active();const msg=conv?.msgs[idx];if(!msg)return; |
| const row=DOM.msgs().querySelector(`.mr[data-i="${idx}"]`);if(!row)return; |
| const bub=row.querySelector('.mb');const orig=msg.content; |
| bub.innerHTML=''; |
| const ta=document.createElement('textarea');ta.className='etx';ta.value=orig;bub.appendChild(ta); |
| const btns=document.createElement('div');btns.className='ebtns'; |
| const cancelBtn=document.createElement('button');cancelBtn.className='ecn';cancelBtn.textContent='إلغاء'; |
| cancelBtn.addEventListener('click',()=>cancelEdit(idx,orig,bub)); |
| const saveBtn=document.createElement('button');saveBtn.className='esv';saveBtn.textContent='إرسال'; |
| saveBtn.addEventListener('click',()=>saveEdit(idx,ta.value.trim())); |
| btns.appendChild(cancelBtn);btns.appendChild(saveBtn);bub.appendChild(btns); |
| ta.focus();ta.selectionStart=ta.value.length; |
| } |
| |
| function cancelEdit(idx,orig,bub){ |
| bub.innerHTML=''; |
| const span=document.createElement('span'); |
| const lines=orig.split('\n'); |
| lines.forEach((line,i)=>{ |
| span.appendChild(document.createTextNode(line)); |
| if(i<lines.length-1)span.appendChild(document.createElement('br')); |
| }); |
| bub.appendChild(span); |
| } |
| |
| async function saveEdit(idx,newText){ |
| if(!newText)return; |
| const conv=active();if(!conv)return; |
| conv.msgs[idx].content=newText;conv.msgs[idx].ts=Date.now(); |
| conv.msgs=conv.msgs.slice(0,idx+1); |
| persist();renderMessages();await _resend(newText); |
| } |
| |
| async function regen(idx){ |
| const conv=active();if(!conv)return; |
| let ui=idx-1; |
| while(ui>=0&&conv.msgs[ui].role!=='user')ui--; |
| if(ui<0)return; |
| const text=conv.msgs[ui].content; |
| conv.msgs=conv.msgs.slice(0,idx); |
| persist();renderMessages();await _resend(text); |
| } |
| |
| async function _resend(text){ |
| const conv=active();if(!conv)return; |
| const payload={ |
| message:text, |
| model:DOM.msel().value, |
| provider:DOM.psel().value, |
| system_prompt:DOM.sysp().value.trim(), |
| conversation_id:getServerId(conv), |
| }; |
| setBusy(true); |
| const lid='tp'+Date.now(); |
| const typRow=UI.typingRow(lid,'يعيد التوليد...'); |
| DOM.msgs().appendChild(typRow);scrollBot(); |
| try{ |
| if(DOM.stog().checked) await _doStream(payload,lid,conv,DOM.msel().value); |
| else await _doSync(payload,lid,conv,DOM.msel().value); |
| }catch{document.getElementById(lid)?.remove();setBusy(false);} |
| } |
| |
| function togglePin(idx){ |
| const conv=active();if(!conv)return; |
| if(!conv.pins)conv.pins=[]; |
| const i=conv.pins.indexOf(idx); |
| if(i===-1){conv.pins.push(idx);toast('📌 تم التثبيت','ok');} |
| else{conv.pins.splice(i,1);toast('تم إلغاء التثبيت','');} |
| persist();renderMessages(); |
| } |
| |
| |
| function artAction(type,el){ |
| const art=el.closest('.artifact')?._artData;if(!art)return; |
| if(type==='art-view'||type==='art-run'){ |
| Sandbox.open(Utils.wrapForPreview(art.code,art.lang),art.filename); |
| } else if(type==='art-copy'){ |
| navigator.clipboard?.writeText(art.code); |
| const orig=el.innerHTML;el.innerHTML='<i class="fa-solid fa-check"></i> تم'; |
| setTimeout(()=>{el.innerHTML=orig;},1500);toast('✅ تم النسخ','ok'); |
| } else if(type==='art-dl'){ |
| const a=document.createElement('a'); |
| a.href=URL.createObjectURL(new Blob([art.code],{type:'text/plain'})); |
| a.download=art.filename;a.click();URL.revokeObjectURL(a.href); |
| toast('⬇️ جاري التحميل','ok'); |
| } |
| } |
| |
| |
| async function loadProviders(){ |
| try{ |
| const data=await API.getProviders(); |
| if(!data.ok)throw new Error(); |
| state.provs=data.providers; |
| _buildProviderSelect(data.providers); |
| enableInput(); |
| toast(`✅ ${data.total} مزود`,'ok'); |
| _restoreProvider(); |
| }catch{ |
| toast('❌ فشل الاتصال','er'); |
| enableInput(); |
| } |
| } |
| |
| async function reloadProviders(){ |
| const ic=DOM.rlic();ic.classList.add('fa-spin'); |
| try{await API.reload();await loadProviders();}catch{} |
| ic.classList.remove('fa-spin'); |
| } |
| |
| function _buildProviderSelect(providers){ |
| const sel=DOM.psel();const frag=document.createDocumentFragment(); |
| |
| if(providers['Auto']){ |
| const o=document.createElement('option');o.value='Auto';o.textContent='⚡ Auto — تلقائي';frag.appendChild(o); |
| } |
| const tg=document.createElement('optgroup');tg.label='── بدون مصادقة ──'; |
| const ag=document.createElement('optgroup');ag.label='── تحتاج تسجيل ──'; |
| for(const[k,p] of Object.entries(providers)){ |
| if(k==='Auto')continue; |
| const o=document.createElement('option');o.value=k;o.textContent=k; |
| (p.needs_auth?ag:tg).appendChild(o); |
| } |
| if(tg.children.length)frag.appendChild(tg); |
| if(ag.children.length)frag.appendChild(ag); |
| sel.innerHTML='';sel.appendChild(frag); |
| } |
| |
| async function onProviderChange(){ |
| const pv=DOM.psel().value; |
| Store.setProvider(pv); |
| DOM.ptxt().textContent=pv||'—'; |
| const ms=DOM.msel();ms.innerHTML='<option>⏳ جاري...</option>'; |
| try{ |
| const data=await API.getModels(pv); |
| const frag=document.createDocumentFragment(); |
| (data.models||['gpt-4o']).forEach(m=>{ |
| const o=document.createElement('option');o.value=m;o.textContent=m;frag.appendChild(o); |
| }); |
| ms.innerHTML='';ms.appendChild(frag); |
| _restoreModel(); |
| }catch{ms.innerHTML='<option value="gpt-4o">gpt-4o</option>';} |
| } |
| |
| function _restoreProvider(){ |
| const p=Store.getProvider(); |
| if(p&&[...DOM.psel().options].some(o=>o.value===p)){DOM.psel().value=p;onProviderChange();} |
| else onProviderChange(); |
| } |
| |
| function _restoreModel(){ |
| const m=Store.getModel(); |
| if(m&&[...DOM.msel().options].some(o=>o.value===m))DOM.msel().value=m; |
| } |
| |
| |
| function toggleSidebar(){ |
| state.sbOpen=!state.sbOpen; |
| const sb=DOM.sb(); |
| if(window.innerWidth>680){sb.classList.toggle('desk-hide',!state.sbOpen);} |
| else{sb.classList.toggle('mob-open',state.sbOpen);DOM.ov().classList.toggle('on',state.sbOpen);} |
| } |
| function closeSidebar(){ |
| state.sbOpen=false;DOM.sb().classList.remove('mob-open');DOM.ov().classList.remove('on'); |
| } |
| |
| |
| function toggleTheme(){ |
| const next=Store.getTheme()==='dark'?'light':'dark'; |
| Store.setTheme(next); |
| document.documentElement.dataset.theme=next; |
| _updateThemeIcon(next); |
| DOM.themeTog().checked=next==='light'; |
| } |
| function _updateThemeIcon(theme){ |
| DOM.thbtn().querySelector('i').className=theme==='dark'?'fa-solid fa-sun':'fa-solid fa-moon'; |
| } |
| |
| |
| function _delegateMsgs(e){ |
| const btn=e.target.closest('[data-action]');if(!btn)return; |
| const action=btn.dataset.action; |
| const idx=parseInt(btn.dataset.idx??'-1'); |
| if(action==='copy'&&btn.classList.contains('cb-copy')){copyCode(btn);return;} |
| if(action==='run'&&btn.classList.contains('cb-run')){runCodeBlock(btn);return;} |
| switch(action){ |
| case 'copy': copyMsg(idx);break; |
| case 'edit': editMsg(idx);break; |
| case 'pin': togglePin(idx);break; |
| case 'regen': regen(idx);break; |
| case 'art-view':case 'art-copy':case 'art-run':case 'art-dl': artAction(action,btn);break; |
| } |
| } |
| |
| function _delegateConvList(e){ |
| const btn=e.target.closest('[data-action]'); |
| if(btn){ |
| e.stopPropagation(); |
| const action=btn.dataset.action;const cid=btn.dataset.cid; |
| if(action==='rename')startRename(cid); |
| if(action==='delete')deleteConv(cid); |
| return; |
| } |
| const item=e.target.closest('.ci[data-cid]'); |
| if(item)setActive(item.dataset.cid); |
| } |
| |
| function _delegateWelcome(e){ |
| const card=e.target.closest('[data-prompt]');if(card)send(card.dataset.prompt); |
| } |
| |
| function _delegateFollowUps(e){ |
| const btn=e.target.closest('.fup[data-prompt]');if(btn)send(btn.dataset.prompt); |
| } |
| |
| |
| function init(){ |
| if(window.marked)marked.setOptions({breaks:true,gfm:true}); |
| |
| |
| const theme=Store.getTheme(); |
| document.documentElement.dataset.theme=theme; |
| _updateThemeIcon(theme); |
| DOM.themeTog().checked=theme==='light'; |
| |
| |
| DOM.showThinkTog().checked=Store.getShowThink(); |
| DOM.fupTog().checked=Store.getShowFup(); |
| DOM.stog().checked=Store.getStream(); |
| |
| |
| if(!state.sbOpen)DOM.sb().classList.add('desk-hide'); |
| |
| |
| DOM.ci().addEventListener('input',_onInput); |
| DOM.ci().addEventListener('keydown',e=>{ |
| if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();} |
| }); |
| |
| |
| DOM.mwrap().addEventListener('scroll',_onScroll,{passive:true}); |
| |
| |
| DOM.msel().addEventListener('change',()=>Store.setModel(DOM.msel().value)); |
| DOM.stog().addEventListener('change',()=>Store.setStream(DOM.stog().checked)); |
| DOM.showThinkTog().addEventListener('change',()=>{Store.setShowThink(DOM.showThinkTog().checked);renderMessages();}); |
| DOM.fupTog().addEventListener('change',()=>Store.setShowFup(DOM.fupTog().checked)); |
| DOM.themeTog().addEventListener('change',toggleTheme); |
| |
| |
| DOM.rmi().addEventListener('keydown',e=>{if(e.key==='Enter')confirmRename();if(e.key==='Escape')closeModal();}); |
| DOM.rmod().addEventListener('click',e=>{if(e.target===DOM.rmod())closeModal();}); |
| |
| |
| $('newConvBtn').addEventListener('click',newConv); |
| $('sbToggleBtn').addEventListener('click',toggleSidebar); |
| DOM.ov().addEventListener('click',closeSidebar); |
| DOM.thbtn().addEventListener('click',toggleTheme); |
| $('reloadBtn').addEventListener('click',reloadProviders); |
| DOM.sndbtn().addEventListener('click',()=>send()); |
| $('stopbtn').addEventListener('click',()=>{state.stopReq=true;setBusy(false);toast('تم إيقاف الرد','');}); |
| $('scrbtn').addEventListener('click',()=>{state.userScrolled=false;scrollBot(true);}); |
| |
| |
| $('renCancelBtn').addEventListener('click',closeModal); |
| $('renSaveBtn').addEventListener('click',confirmRename); |
| |
| |
| $('sbCloseBtn').addEventListener('click',Sandbox.close); |
| $('sbRunBtn').addEventListener('click',Sandbox.run); |
| document.querySelectorAll('.sb-tab').forEach(tab=>tab.addEventListener('click',()=>Sandbox.setTab(tab.dataset.tab))); |
| document.addEventListener('keydown',e=>{if(e.key==='Escape')Sandbox.close();}); |
| |
| |
| DOM.msgs().addEventListener('click',e=>{_delegateMsgs(e);_delegateWelcome(e);_delegateFollowUps(e);}); |
| DOM.cl().addEventListener('click',_delegateConvList); |
| |
| |
| DOM.psel().addEventListener('change',onProviderChange); |
| |
| |
| loadProviders(); |
| renderConvList(); |
| |
| if(state.activeId&&active()){renderMessages();} |
| else if(state.convs.length){setActive(state.convs[0].id);} |
| else{DOM.msgs().innerHTML='';DOM.msgs().appendChild(UI.welcomeScreen());} |
| } |
| |
| return {init}; |
| })(); |
| |
| App.init(); |
| </script> |
| </body> |
| </html |