gen / index.html
AnesKAM's picture
Update index.html
614f7c1 verified
Raw
History Blame Contribute Delete
66.4 kB
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Genisi</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
:root{
--sidebar-bg:rgba(248,249,251,0.9);
--sidebar-hover:rgba(0,0,0,0.055);
--sidebar-active:rgba(124,58,237,0.10);
--bg:#ffffff;
--text:#1c1c1e;
--text2:#48484a;
--muted:#7a7d80;
--divider:#e7e9ec;
--white:#fff;
--purple:#7c3aed;
--purple-dark:#6d28d9;
--purple-grad:linear-gradient(135deg,#8b5cf6,#7c3aed);
--upgrade-bg:#f4ecff;
--card:#ffffff;
--code-bg:#0d1117;
--input-shadow:0 2px 10px rgba(20,20,30,0.06);
--menu-shadow:0 8px 24px rgba(0,0,0,0.14);
--sidebar-w:264px;
--transition:cubic-bezier(0.2,0,0,1)
}
html[data-theme="dark"]{
--sidebar-bg:rgba(24,24,26,0.9);
--sidebar-hover:rgba(255,255,255,0.06);
--sidebar-active:rgba(124,58,237,0.18);
--bg:#121213;
--text:#ececec;
--text2:#c7c7c9;
--muted:#96999c;
--divider:#2e2f31;
--white:#1e1f21;
--upgrade-bg:#2b2338;
--card:#1b1c1e;
--code-bg:#0a0b0d;
}
html,body{height:100%;font-family:'Cairo','Inter',sans-serif;background:var(--bg);color:var(--text);overflow:hidden;overscroll-behavior:none}
.shell{display:flex;height:100vh;height:100dvh;position:relative;overflow:hidden}
/* ===== SIDEBAR ===== */
.sidebar{width:var(--sidebar-w);min-width:var(--sidebar-w);background:var(--sidebar-bg);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);display:flex;flex-direction:column;position:relative;z-index:20;border-inline-end:1px solid var(--divider);margin-inline-start:0;opacity:1;transition:margin-inline-start .28s var(--transition), opacity .2s;overflow:hidden}
.sidebar.closed{margin-inline-start:calc(var(--sidebar-w) * -1);opacity:0;pointer-events:none}
.sb-head{display:flex;align-items:center;padding:12px 8px 6px 16px;gap:4px;height:56px;flex-shrink:0}
.sb-logo{display:flex;align-items:center;gap:10px;flex:1;min-width:0;cursor:default}
.sb-logo img{border-radius:50%;object-fit:cover;flex-shrink:0;box-shadow:0 2px 8px rgba(124,58,237,0.25)}
.sb-logo-text{font-size:18px;font-weight:800;color:var(--text);white-space:nowrap;letter-spacing:-.2px}
.sb-ico-btn{width:38px;height:38px;border:none;background:transparent;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text2);transition:background .15s}
.sb-ico-btn:hover{background:var(--sidebar-hover)}
.sb-scroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:6px 0 8px;padding-inline-end:4px;-webkit-overflow-scrolling:touch}
.sb-scroll::-webkit-scrollbar{width:4px}
.sb-scroll::-webkit-scrollbar-thumb{background:rgba(128,128,128,0.3);border-radius:4px}
.sb-nav{padding:8px}
.sb-item{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:14px;cursor:pointer;font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;transition:all .18s var(--transition);user-select:none;margin-bottom:4px}
.sb-item:hover{background:var(--sidebar-hover)}
.sb-item svg{flex-shrink:0;color:var(--text2);transition:color .2s}
.sb-item:hover svg{color:var(--purple)}
.sb-item:first-child{background:var(--upgrade-bg);color:var(--purple);border:1px solid rgba(124,58,237,0.12)}
.sb-item:first-child svg{color:var(--purple)}
.sb-item:first-child:hover{background:#ecd9ff}
.sb-section{font-size:11px;font-weight:700;color:var(--muted);padding:18px 16px 6px;letter-spacing:.6px;text-transform:uppercase;white-space:nowrap}
.recent-wrap{padding:0 8px}
.recent-item{display:flex;align-items:center;padding:9px 14px;border-radius:12px;cursor:pointer;font-size:13.5px;color:var(--text);white-space:nowrap;gap:8px;transition:all .15s var(--transition);border-inline-start:3px solid transparent;margin-bottom:2px}
.recent-item:hover{background:var(--sidebar-hover)}
.recent-item.active{background:var(--sidebar-active);border-inline-start-color:var(--purple);color:var(--purple);font-weight:700}
.recent-item .ri-text{flex:1;overflow:hidden;text-overflow:ellipsis}
.recent-item .ri-more{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;opacity:0;transition:opacity .15s,background .15s;color:var(--text2)}
.recent-item:hover .ri-more{opacity:1}
.recent-item .ri-more:hover{background:rgba(220,50,50,0.12);color:#e03e3e}
.sb-bottom{flex-shrink:0;padding:8px;border-top:1px solid var(--divider);background:var(--sidebar-bg)}
.sb-settings{display:flex;align-items:center;gap:12px;padding:10px 14px;border-radius:14px;cursor:pointer;font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;transition:background .15s}
.sb-settings:hover{background:var(--sidebar-hover)}
.sb-settings svg{color:var(--text2)}
.sb-user{display:flex;align-items:center;gap:10px;padding:9px 14px;border-radius:14px;cursor:pointer;transition:background .15s;margin-top:4px}
.sb-user:hover{background:var(--sidebar-hover)}
.avatar{width:32px;height:32px;border-radius:50%;background:var(--purple-grad);color:#fff;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;flex-shrink:0;box-shadow:0 2px 8px rgba(124,58,237,0.35)}
.sb-user-name{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.main{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;background:var(--bg)}
/* ===== TOPBAR ===== */
.topbar{position:absolute;top:0;inset-inline:0;height:56px;z-index:15;display:flex;align-items:center;justify-content:space-between;padding:0 10px;pointer-events:none;opacity:0;transition:opacity .2s, background .2s}
.topbar.show{opacity:1;pointer-events:all}
.tb-left,.tb-right{display:flex;align-items:center;gap:4px}
.tb-btn{width:40px;height:40px;border:none;background:transparent;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text2);transition:background .15s}
.tb-btn:hover{background:var(--sidebar-hover);color:var(--text)}
.model-btn-h{display:flex;align-items:center;gap:5px;padding:8px 14px;border-radius:20px;border:1.5px solid var(--divider);background:var(--card);font-family:inherit;font-size:13px;color:var(--text);font-weight:700;cursor:pointer;white-space:nowrap;transition:all .15s}
.model-btn-h:hover{border-color:var(--purple);color:var(--purple)}
.model-menu-h{position:absolute;top:calc(100% + 8px);inset-inline-end:0;background:var(--card);border:1px solid var(--divider);border-radius:16px;box-shadow:var(--menu-shadow);padding:6px;min-width:190px;z-index:200;display:none}
.model-menu-h.open{display:block}
.mm-item{display:flex;flex-direction:column;padding:10px 14px;border-radius:11px;cursor:pointer;transition:background .15s}
.mm-item:hover{background:var(--sidebar-hover)}
.mm-item b{font-size:14px}
.mm-item small{font-size:12px;color:var(--muted)}
/* ===== HOME ===== */
.home{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 20px 80px;position:relative;overflow:hidden;overflow-y:auto}
.home::before{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:700px;height:600px;background:radial-gradient(ellipse 50% 45% at 50% 50%,rgba(168,85,247,.28) 0%,transparent 70%);pointer-events:none;z-index:0}
.greeting{font-size:34px;font-weight:800;color:var(--text);margin-bottom:28px;text-align:center;position:relative;z-index:1;animation:greet .4s ease both;letter-spacing:-.5px}
@keyframes greet{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
.input-wrap{width:100%;max-width:720px;position:relative;z-index:1}
.input-stack{display:flex;flex-direction:column;width:100%}
.attachments{display:flex;flex-wrap:wrap;gap:8px;padding:0 4px 8px}
.att-chip{display:flex;align-items:center;gap:8px;background:var(--upgrade-bg);color:var(--purple);border-radius:12px;padding:6px 10px;font-size:13px;max-width:220px;border:1px solid rgba(124,58,237,0.12)}
.att-chip .att-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.att-chip .att-x{cursor:pointer;border:none;background:transparent;color:inherit;font-weight:700;font-size:16px;line-height:1}
.att-chip.loading{opacity:.6}
.input-box{background:var(--card);border-radius:26px;box-shadow:var(--input-shadow);display:flex;align-items:flex-end;padding:10px 14px;gap:4px;border:1.5px solid var(--divider);transition:border-color .2s,box-shadow .2s}
.input-box:focus-within{border-color:var(--purple);box-shadow:0 0 0 4px rgba(124,58,237,.14)}
.input-plus,.mic-btn{width:36px;height:36px;border:none;background:transparent;border-radius:50%;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;color:var(--text2);transition:background .15s,color .15s}
.input-plus:hover,.mic-btn:hover{background:var(--sidebar-hover);color:var(--purple)}
.mic-btn.recording{background:#e8453c;color:#fff;animation:pulse-mic 1.5s infinite}
@keyframes pulse-mic{0%,100%{box-shadow:0 0 0 0 rgba(232,69,60,0.4)}50%{box-shadow:0 0 0 8px rgba(232,69,60,0)}}
.input-ta{flex:1;border:none;outline:none;background:transparent;font-family:inherit;font-size:16px;color:var(--text);line-height:1.5;resize:none;max-height:180px;min-height:24px;padding:7px 6px;scrollbar-width:none}
.input-ta::-webkit-scrollbar{display:none}
.input-ta::placeholder{color:var(--muted)}
.input-right{display:flex;align-items:center;gap:2px;flex-shrink:0;padding-bottom:2px;position:relative}
.send-btn{width:36px;height:36px;border:none;background:var(--purple-grad);border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;transition:transform .1s,opacity .15s;box-shadow:0 2px 8px rgba(124,58,237,.35)}
.send-btn:hover{opacity:.9}
.send-btn:active{transform:scale(.92)}
.send-btn:disabled{background:#c4c7c5;box-shadow:none;cursor:default}
.send-btn.stop{background:#1f1f1f;box-shadow:none}
html[data-theme="dark"] .send-btn.stop{background:#e3e3e3;color:#1f1f1f}
.search-pane{position:absolute;inset:0;background:var(--bg);z-index:30;display:none;flex-direction:column;padding:12px 60px 20px}
.search-pane.open{display:flex}
.sp-top{display:flex;align-items:center;gap:12px;height:56px;flex-shrink:0}
.sp-input-wrap{flex:1;display:flex;align-items:center;gap:10px;background:var(--card);border:1.5px solid var(--divider);border-radius:26px;padding:10px 16px;box-shadow:var(--input-shadow)}
.sp-input{flex:1;border:none;outline:none;background:transparent;font-size:16px;font-family:inherit;color:var(--text)}
.sp-x{border:none;background:transparent;cursor:pointer;color:var(--muted);display:flex}
.sp-sect{font-size:12px;font-weight:700;color:var(--muted);padding:16px 4px 6px;text-transform:uppercase}
.sp-results{flex:1;overflow-y:auto}
.sp-row{display:flex;align-items:center;gap:14px;padding:11px 12px;border-radius:12px;cursor:pointer;font-size:14px;color:var(--text);transition:background .12s}
.sp-row:hover{background:var(--sidebar-hover)}
.sp-row .sr-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sp-row .sr-date{font-size:13px;color:var(--muted);white-space:nowrap}
.sp-row.hide{display:none}
.settings-panel{position:fixed;top:0;inset-inline-end:0;bottom:0;width:380px;max-width:90vw;background:var(--card);box-shadow:-2px 0 16px rgba(0,0,0,0.15);z-index:100;transform:translateX(100%);transition:transform .3s var(--transition);display:flex;flex-direction:column;overflow:hidden}
html[dir="rtl"] .settings-panel{transform:translateX(-100%)}
.settings-panel.open{transform:translateX(0)!important}
.settings-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--divider)}
.settings-title{font-size:16px;font-weight:800}
.settings-close{width:36px;height:36px;border:none;background:transparent;border-radius:50%;cursor:pointer;color:var(--text2);display:flex;align-items:center;justify-content:center}
.settings-close:hover{background:var(--sidebar-hover)}
.settings-content{flex:1;overflow-y:auto;padding:20px}
.settings-group{margin-bottom:24px}
.settings-label{font-size:14px;font-weight:700;margin-bottom:12px;display:block}
.settings-option{display:flex;align-items:center;gap:12px;padding:12px;border-radius:12px;cursor:pointer;transition:background .15s}
.settings-option:hover{background:var(--sidebar-hover)}
.settings-option input{width:18px;height:18px;cursor:pointer;accent-color:var(--purple)}
.option-text{font-size:14px;flex:1}
.settings-divider{height:1px;background:var(--divider);margin:16px 0}
.btn-purple{width:100%;padding:11px;background:var(--upgrade-bg);color:var(--purple);border:none;border-radius:12px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s}
.btn-purple:hover{background:#e9d5ff}
.overlay{position:fixed;inset:0;background:rgba(0,0,0,0);z-index:50;opacity:0;pointer-events:none;transition:opacity .3s}
.overlay.active{opacity:1;pointer-events:all;background:rgba(0,0,0,0.4)}
/* ===== CHAT VIEW ===== */
.chat-view{flex:1;display:none;flex-direction:column;min-height:0}
.shell.chat .home{display:none}
.shell.chat .chat-view{display:flex}
.msgs{flex:1;overflow-y:auto;padding:72px 0 12px}
.msgs::-webkit-scrollbar{width:6px}
.msgs::-webkit-scrollbar-thumb{background:var(--divider);border-radius:3px}
.msgs-inner{max-width:760px;width:100%;margin:0 auto;padding:0 20px;display:flex;flex-direction:column;gap:26px}
@keyframes msgIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
/* User messages */
.msg-user{align-self:flex-start;background:var(--upgrade-bg);color:var(--text);padding:12px 18px;border-radius:18px 18px 18px 4px;max-width:78%;font-size:15px;line-height:1.65;word-break:break-word;white-space:pre-wrap;box-shadow:0 1px 3px rgba(0,0,0,0.04);animation:msgIn .25s ease both}
/* AI messages — flat text with avatar */
.msg-ai{display:flex;gap:12px;align-self:flex-end;max-width:96%;width:100%;animation:msgIn .25s ease both}
.msg-ai-avatar{width:30px;height:30px;border-radius:50%;overflow:hidden;flex-shrink:0;background:var(--purple-grad);display:flex;align-items:center;justify-content:center;color:#fff;font-size:13px;font-weight:700;margin-top:3px;box-shadow:0 2px 6px rgba(124,58,237,.3)}
.msg-ai-avatar img{width:100%;height:100%;object-fit:cover;display:block}
.msg-ai-col{flex:1;min-width:0}
.msg-ai-body{background:transparent;padding:2px 0;font-size:15.5px;line-height:1.85;color:var(--text);word-break:break-word}
.msg-ai-body p{margin:0 0 14px}
.msg-ai-body ul,.msg-ai-body ol{margin:0 0 14px;padding-inline-start:24px}
.msg-ai-body h1,.msg-ai-body h2,.msg-ai-body h3{margin:16px 0 8px;font-weight:700}
.msg-ai-body a{color:var(--purple)}
.msg-ai-body blockquote{border-inline-start:3px solid var(--purple);padding-inline-start:12px;color:var(--muted);margin:0 0 14px}
.msg-ai-body table{border-collapse:collapse;width:100%;margin:14px 0;font-size:14px;border-radius:10px;overflow:hidden;display:block;overflow-x:auto}
.msg-ai-body th,.msg-ai-body td{border:1px solid var(--divider);padding:8px 12px;text-align:start}
.msg-ai-body th{background:var(--upgrade-bg);color:var(--purple);font-weight:700}
.msg-ai-body tr:nth-child(even) td{background:rgba(124,58,237,0.04)}
.gen-img{max-width:100%;border-radius:14px;margin:8px 0;display:block}
.img-actions{display:flex;gap:8px;margin:4px 0 8px;flex-wrap:wrap}
.stream-cursor{display:inline-block;width:2px;height:1.1em;background:var(--purple);margin-left:2px;vertical-align:text-bottom;animation:blink 1s step-end infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
.code-wrap{position:relative;margin:14px 0;border-radius:12px;overflow:hidden;border:1px solid var(--divider);direction:ltr}
.code-bar{display:flex;align-items:center;justify-content:space-between;background:#161b22;color:#c9d1d9;padding:7px 12px;font-size:12px;font-family:monospace}
.copy-btn{background:transparent;border:1px solid #30363d;color:#c9d1d9;border-radius:6px;padding:3px 10px;font-size:12px;cursor:pointer;font-family:inherit;transition:background .15s}
.copy-btn:hover{background:#21262d}
.code-wrap pre{margin:0}
.code-wrap pre code{display:block;padding:14px;background:var(--code-bg);overflow-x:auto;font-family:'Consolas',monospace;font-size:13.5px;line-height:1.6}
.suggest{background:var(--card);border:1px solid var(--divider);color:var(--text);border-radius:18px;padding:8px 16px;font-size:14px;cursor:pointer;font-family:inherit;transition:background .15s;text-decoration:none;display:inline-block}
.suggest:hover{background:var(--sidebar-hover)}
.chat-input-bar{padding:10px 20px 18px;display:none;justify-content:center;flex-shrink:0}
.shell.chat .chat-input-bar{display:flex}
.modal{position:fixed;inset:0;z-index:300;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);padding:20px;overflow-y:auto;backdrop-filter:blur(4px)}
.modal.open{display:flex}
.modal-card{background:var(--card);border-radius:24px;max-width:460px;width:100%;padding:32px;box-shadow:0 20px 60px rgba(0,0,0,0.3);max-height:90vh;overflow-y:auto}
.modal-card h2{font-size:24px;font-weight:800;margin-bottom:8px;text-align:center}
.modal-card p.sub{color:var(--muted);text-align:center;margin-bottom:24px;font-size:14px}
.field{margin-bottom:16px}
.field label{display:block;font-size:14px;font-weight:600;margin-bottom:6px}
.field input,.field select{width:100%;padding:12px 14px;border:1.5px solid var(--divider);border-radius:12px;background:var(--bg);color:var(--text);font-family:inherit;font-size:15px;outline:none;transition:border-color .2s}
.field input:focus,.field select:focus{border-color:var(--purple)}
.gender-row{display:flex;gap:8px}
.gender-opt{flex:1;text-align:center;padding:12px 6px;border:1.5px solid var(--divider);border-radius:12px;cursor:pointer;font-size:13px;font-weight:600;transition:all .15s}
.gender-opt.sel{border-color:var(--purple);background:var(--upgrade-bg);color:var(--purple)}
.modal-btn{width:100%;padding:14px;background:var(--purple-grad);color:#fff;border:none;border-radius:14px;font-size:16px;font-weight:700;cursor:pointer;font-family:inherit;margin-top:8px;transition:opacity .15s;box-shadow:0 4px 14px rgba(124,58,237,.3)}
.modal-btn:hover{opacity:.92}
.howto h3{font-size:16px;font-weight:700;margin:16px 0 6px;color:var(--purple)}
.howto ul{padding-inline-start:20px;line-height:1.9;font-size:14px}
.brand-star{display:block;margin:0 auto 12px;border-radius:50%;object-fit:cover}
.doc-preview{border:1px solid var(--divider);border-radius:14px;background:var(--card);padding:28px 32px;margin:12px 0;box-shadow:0 2px 14px rgba(124,58,237,.08);max-height:520px;overflow-y:auto}
.doc-paper{font-family:'Cairo','Inter',sans-serif;color:var(--text);line-height:1.7}
.doc-title{font-size:26px;font-weight:700;color:#7c3aed;text-align:center;border-bottom:3px solid #7c3aed;padding-bottom:12px;margin-bottom:18px}
.doc-paper h2{font-size:20px;color:#7c3aed;margin:16px 0 8px}
.doc-paper h3{font-size:17px;color:#7c3aed;margin:14px 0 6px}
.doc-paper p{margin:0 0 10px;font-size:15px}
.doc-paper ul{padding-inline-start:22px;margin:0 0 10px}
.doc-paper li{margin:4px 0;font-size:15px}
.doc-paper table{border-collapse:collapse;width:100%;margin:12px 0}
.doc-paper th{background:#7c3aed;color:#fff;padding:8px 12px;text-align:start}
.doc-paper td{border:1px solid var(--divider);padding:8px 12px}
.doc-slide{border:1px solid #eadcff;border-radius:12px;padding:16px 18px;margin:12px 0;position:relative;background:linear-gradient(135deg,var(--upgrade-bg),var(--card))}
.doc-slide-n{position:absolute;top:12px;inset-inline-end:14px;width:24px;height:24px;border-radius:50%;background:#7c3aed;color:#fff;font-size:12px;display:flex;align-items:center;justify-content:center;font-weight:700}
.doc-dl{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:6px 0 4px}
.doc-dl-label{font-size:13px;color:var(--muted);font-weight:600}
.doc-dl-btn{background:var(--upgrade-bg);color:var(--purple);border:1px solid transparent;border-radius:14px;padding:7px 16px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .15s}
.doc-dl-btn:hover{background:#e9d5ff}
.doc-dl-btn:disabled{opacity:.6;cursor:default}
svg{user-select:none}button svg{pointer-events:none}
.ai-content-target{min-height:1px}
/* ===== RESPONSIVE ===== */
@media (max-width:768px){
.sidebar{position:fixed;top:0;bottom:0;inset-inline-start:0;z-index:100;margin-inline-start:0;opacity:1;overflow:hidden}
.sidebar.closed{margin-inline-start:calc(var(--sidebar-w) * -1);opacity:0;pointer-events:none}
.topbar{opacity:1;pointer-events:all;position:fixed;background:rgba(255,255,255,0.6);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-bottom:1px solid rgba(0,0,0,0.04)}
html[data-theme="dark"] .topbar{background:rgba(18,18,19,0.6);border-bottom:1px solid rgba(255,255,255,0.04)}
.home{padding:60px 16px 100px}
.greeting{font-size:25px;margin-bottom:20px}
.input-wrap{max-width:100%}
.input-box{padding:8px 12px;gap:2px}
.input-ta{font-size:16px;padding:8px 4px}
.msgs{padding:62px 0 10px}
.msgs-inner{padding:0 14px;gap:22px}
.msg-user{max-width:85%;padding:10px 14px;font-size:14px}
.msg-ai{max-width:100%;gap:9px}
.msg-ai-avatar{width:26px;height:26px}
.msg-ai-body{font-size:14.5px;line-height:1.75}
.code-wrap pre code{font-size:12.5px;padding:10px}
.chat-input-bar{padding:8px 12px 14px}
.search-pane{padding:12px 16px 20px}
.settings-panel{width:100%;max-width:100vw}
.modal{padding:12px}
.modal-card{padding:24px 20px;border-radius:20px}
.doc-preview{padding:20px 16px}
.home::before{width:500px;height:400px;opacity:1}
}
@media (max-width:480px){
.greeting{font-size:21px}
.input-box{border-radius:22px}
.send-btn,.mic-btn,.input-plus{width:34px;height:34px}
.msg-user{max-width:90%;font-size:13.5px}
.msg-ai-body{font-size:14px}
.modal-card{padding:20px 16px}
.home::before{width:400px;height:350px}
.model-btn-h{font-size:12.5px;padding:6px 10px}
}
@supports (padding-top:env(safe-area-inset-top)){
.topbar{padding-top:env(safe-area-inset-top)}
.home{padding-top:calc(60px + env(safe-area-inset-top))}
.chat-input-bar{padding-bottom:calc(18px + env(safe-area-inset-bottom))}
.sidebar{padding-top:env(safe-area-inset-top)}
}
@media (hover:none){
.sb-item,.sb-settings,.sb-user,.recent-item,.sp-row,.settings-option,.mm-item,.doc-dl-btn,.suggest,.copy-btn{min-height:44px;display:flex;align-items:center}
.recent-item .ri-more{opacity:1}
}
@media (max-width:768px){input[type="text"],input[type="date"],input[type="number"],input[type="email"],textarea,select{font-size:16px!important}}
</style>
<base target="_blank">
</head>
<body>
<div class="shell" id="shell">
<div class="sidebar" id="sidebar">
<div class="sb-head">
<div class="sb-logo"><img src="https://qph.cf2.poecdn.net/main-thumb-pb-6867722-200-mqrjiwnfubtpxjfdovmmfgdaicgosphq.jpeg" alt="Genisi" width="30" height="30" onerror="this.style.display='none'"><span class="sb-logo-text">Genisi</span></div>
<button class="sb-ico-btn" onclick="closeSidebar()" aria-label="Close sidebar"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="18" height="18" rx="2.5"/><line x1="9" y1="3" x2="9" y2="21"/></svg></button>
</div>
<div class="sb-scroll">
<div class="sb-nav">
<div class="sb-item" onclick="newChat()"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg><span data-i18n="newChat">محادثة جديدة</span></div>
<div class="sb-item" onclick="openSearch()"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg><span data-i18n="searchChats">البحث</span></div>
</div>
<div class="sb-section" data-i18n="recent">الأخيرة</div>
<div class="recent-wrap" id="recentWrap"></div>
</div>
<div class="sb-bottom">
<div class="sb-settings" onclick="openSettings()"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg><span data-i18n="settings">الإعدادات</span></div>
<div class="sb-user" onclick="openHowto()"><div class="avatar" id="userAvatar">G</div><span class="sb-user-name" id="userName">Genisi user</span></div>
</div>
</div>
<div class="main" id="main">
<div class="topbar" id="topbar">
<div class="tb-left">
<button class="tb-btn" onclick="openSidebar()" aria-label="Open sidebar"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="18" height="18" rx="2.5"/><line x1="9" y1="3" x2="9" y2="21"/></svg></button>
<button class="tb-btn" onclick="newChat()" aria-label="New chat"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
<button class="tb-btn" onclick="openSearch()" aria-label="Search"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg></button>
</div>
<div class="tb-right" style="position:relative">
<button class="model-btn-h" onclick="toggleModelMenu(event,'modelMenuHeader')"><span class="modelLabel">Flash</span><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="6 9 12 15 18 9"/></svg></button>
<div class="model-menu-h" id="modelMenuHeader">
<div class="mm-item" onclick="setModel('flash')"><b>Flash</b><small data-i18n="flashDesc">سريع وذكي</small></div>
<div class="mm-item" onclick="setModel('pro')"><b>Pro</b><small data-i18n="proDesc">إجابات احترافية دقيقة</small></div>
</div>
</div>
</div>
<div class="home" id="homeView">
<h1 class="greeting" id="greeting">كيف يمكنني مساعدتك؟</h1>
<div class="input-wrap"><div class="input-stack">
<div class="attachments" id="homeAtts"></div>
<div class="input-box">
<button class="input-plus" onclick="uploadFiles()" aria-label="Upload"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
<textarea class="input-ta" id="homeTA" data-ph="askGenisi" placeholder="اسأل Genisi" rows="1" oninput="onTaInput(this,'homeSend')" onkeydown="onTaKey(event,'home')"></textarea>
<div class="input-right">
<button class="mic-btn" id="homeMic" onclick="toggleMic('homeTA',this)" aria-label="Voice input"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
<button class="send-btn" id="homeSend" onclick="send('home')" disabled aria-label="Send"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="20" x2="12" y2="5"/><polyline points="6 11 12 5 18 11"/></svg></button>
</div>
</div>
</div></div>
</div>
<div class="chat-view" id="chatView">
<div class="msgs" id="msgs"><div class="msgs-inner" id="msgsInner"></div></div>
<div class="chat-input-bar"><div class="input-wrap"><div class="input-stack">
<div class="attachments" id="chatAtts"></div>
<div class="input-box">
<button class="input-plus" onclick="uploadFiles()" aria-label="Upload"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
<textarea class="input-ta" id="chatTA" data-ph="askGenisi" placeholder="اسأل Genisi" rows="1" oninput="onTaInput(this,'chatSend')" onkeydown="onTaKey(event,'chat')"></textarea>
<div class="input-right">
<button class="mic-btn" id="chatMic" onclick="toggleMic('chatTA',this)" aria-label="Voice input"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
<button class="send-btn" id="chatSend" onclick="send('chat')" disabled aria-label="Send"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="20" x2="12" y2="5"/><polyline points="6 11 12 5 18 11"/></svg></button>
</div>
</div>
</div></div></div>
</div>
<div class="search-pane" id="searchPane">
<div class="sp-top"><div class="sp-input-wrap">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input class="sp-input" id="spInput" data-ph="searchChats" placeholder="البحث" oninput="filterSP(this.value)" autocomplete="off">
<button class="sp-x" onclick="closeSearch()" aria-label="Close search"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
</div></div>
<div class="sp-sect" data-i18n="recent">الأخيرة</div>
<div class="sp-results" id="spResults"></div>
</div>
</div>
<div class="settings-panel" id="settingsPanel">
<div class="settings-header"><h2 class="settings-title" data-i18n="settings">الإعدادات</h2>
<button class="settings-close" onclick="closeSettings()" aria-label="Close settings"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
</div>
<div class="settings-content">
<div class="settings-group"><label class="settings-label" data-i18n="theme">المظهر</label>
<label class="settings-option"><input type="radio" name="theme" value="light" onchange="setTheme('light')"><span class="option-text" data-i18n="light">فاتح</span></label>
<label class="settings-option"><input type="radio" name="theme" value="dark" onchange="setTheme('dark')"><span class="option-text" data-i18n="dark">داكن</span></label>
<label class="settings-option"><input type="radio" name="theme" value="auto" onchange="setTheme('auto')"><span class="option-text" data-i18n="auto">تلقائي</span></label>
</div>
<div class="settings-divider"></div>
<div class="settings-group"><label class="settings-label" data-i18n="language">اللغة</label>
<label class="settings-option"><input type="radio" name="lang" value="en" onchange="setLang('en')"><span class="option-text">English</span></label>
<label class="settings-option"><input type="radio" name="lang" value="ar" onchange="setLang('ar')"><span class="option-text">العربية</span></label>
<label class="settings-option"><input type="radio" name="lang" value="fr" onchange="setLang('fr')"><span class="option-text">Français</span></label>
</div>
<div class="settings-divider"></div>
<div class="settings-group"><label class="settings-label" data-i18n="defaultModel">النموذج</label>
<label class="settings-option"><input type="radio" name="dmodel" value="flash" onchange="setModel('flash')"><span class="option-text">Flash</span></label>
<label class="settings-option"><input type="radio" name="dmodel" value="pro" onchange="setModel('pro')"><span class="option-text">Pro</span></label>
</div>
<div class="settings-divider"></div>
<div class="settings-group"><label class="settings-label" data-i18n="voice">الصوت</label>
<label class="settings-option"><input type="checkbox" id="ttsAuto"><span class="option-text" data-i18n="autoTts">تشغيل تلقائي</span></label>
</div>
<div class="settings-divider"></div>
<div class="settings-group"><button class="btn-purple" onclick="openHowto()" data-i18n="howtoBtn">كيف تستخدم Genisi؟</button></div>
<div class="settings-group"><button class="btn-purple" style="background:#fde8e8;color:#d33" onclick="clearAll()" data-i18n="clearBtn">مسح الكل</button></div>
</div>
</div>
<div class="overlay" id="overlay" onclick="closeSettings()"></div>
<div class="modal" id="welcomeModal"><div class="modal-card">
<img class="brand-star" src="https://qph.cf2.poecdn.net/main-thumb-pb-6867722-200-mqrjiwnfubtpxjfdovmmfgdaicgosphq.jpeg" alt="Genisi" width="56" height="56" onerror="this.style.display='none'">
<h2 data-i18n="welcome">مرحباً بك في Genisi</h2>
<p class="sub" data-i18n="welcomeSub">لنخصّص تجربتك. بياناتك تُحفظ في متصفحك فقط.</p>
<div class="field"><label data-i18n="name">الاسم</label><input id="obName" data-ph="yourName" placeholder="اسمك"></div>
<div class="field"><label data-i18n="dob">تاريخ الميلاد</label><input id="obDob" type="date"></div>
<div class="field"><label data-i18n="gender">الجنس</label>
<div class="gender-row">
<div class="gender-opt" data-g="Male" onclick="pickGender(this)" data-i18n="male">ذكر</div>
<div class="gender-opt" data-g="Female" onclick="pickGender(this)" data-i18n="female">أنثى</div>
<div class="gender-opt" data-g="" onclick="pickGender(this)" data-i18n="noSay">أفضّل عدم الإجابة</div>
</div>
</div>
<button class="modal-btn" onclick="confirmOnboard()" data-i18n="confirm">تأكيد</button>
</div></div>
<div class="modal" id="howtoModal"><div class="modal-card howto">
<img class="brand-star" src="https://qph.cf2.poecdn.net/main-thumb-pb-6867722-200-mqrjiwnfubtpxjfdovmmfgdaicgosphq.jpeg" alt="Genisi" width="56" height="56" onerror="this.style.display='none'">
<h2 data-i18n="howtoTitle">كيف تستخدم Genisi</h2>
<p class="sub" id="howtoIntro"></p>
<div id="howtoBody"></div>
<button class="modal-btn" onclick="closeHowto()" data-i18n="getStarted">ابدأ</button>
</div></div>
<script>
const API = "";
const MAX_ATTACHMENT_BYTES = 15 * 1024 * 1024;
const AVATAR_URL = "https://qph.cf2.poecdn.net/main-thumb-pb-6867722-200-mqrjiwnfubtpxjfdovmmfgdaicgosphq.jpeg";
const I18N = {
en:{ newChat:"New chat", searchChats:"Search chats", recent:"Recent", settings:"Settings", askGenisi:"Ask Genisi", theme:"Theme", light:"Light", dark:"Dark", auto:"Auto", language:"Language", defaultModel:"Default model", voice:"Voice", autoTts:"Auto-play audio", howtoBtn:"How to use Genisi?", clearBtn:"Clear all chats and data", flashDesc:"Fast & smart", proDesc:"Professional & precise", welcome:"Welcome to Genisi", welcomeSub:"Let's personalize your experience.", name:"Name", yourName:"Your name", dob:"Date of birth", gender:"Gender", male:"Male", female:"Female", noSay:"Prefer not to say", confirm:"Confirm & continue", howtoTitle:"How to use Genisi", getStarted:"Get started", downloadDoc:"Download as", downloadImg:"Download image", errConnect:"Connection failed", stopped:"Response stopped", newConvo:"New conversation", micUnsupported:"Your browser doesn't support voice input.", confirmClear:"Clear all chats and data?", highLoad:"Genisi is under high load. Please try again later.", fileTooBig:"File too large (max 15MB)", fileReadErr:"Could not read file", intro:"Genisi is an Algerian AI developed by the <b>AnesNT</b> project, owned by <b>Anes Kameche</b>.", howto:"<h3>Chat</h3><ul><li><b>Flash</b>: fast direct answers.</li><li><b>Pro</b>: professional, detailed answers.</li></ul><h3>Files</h3><ul><li>Attach images, PDFs, or text/code files — Genisi reads them directly.</li></ul><h3>Images & Docs</h3><ul><li>Generate images and create PDF/Word/PPT/Excel.</li></ul>" },
ar:{ newChat:"محادثة جديدة", searchChats:"البحث", recent:"الأخيرة", settings:"الإعدادات", askGenisi:"اسأل Genisi", theme:"المظهر", light:"فاتح", dark:"داكن", auto:"تلقائي", language:"اللغة", defaultModel:"النموذج", voice:"الصوت", autoTts:"تشغيل تلقائي", howtoBtn:"كيف تستخدم Genisi؟", clearBtn:"مسح الكل", flashDesc:"سريع وذكي", proDesc:"إجابات احترافية دقيقة", welcome:"مرحباً بك في Genisi", welcomeSub:"لنخصّص تجربتك. بياناتك تُحفظ في متصفحك فقط.", name:"الاسم", yourName:"اسمك", dob:"تاريخ الميلاد", gender:"الجنس", male:"ذكر", female:"أنثى", noSay:"أفضّل عدم الإجابة", confirm:"تأكيد", howtoTitle:"كيف تستخدم Genisi", getStarted:"ابدأ", downloadDoc:"تحميل بصيغة", downloadImg:"تحميل الصورة", errConnect:"تعذّر الاتصال", stopped:"تم الإيقاف", newConvo:"محادثة جديدة", micUnsupported:"متصفحك لا يدعم الإملاء الصوتي.", confirmClear:"مسح كل البيانات؟", highLoad:"جينيسي تحت ضغط عالٍ. حاول لاحقاً.", fileTooBig:"الملف كبير جداً (الحد 15 ميجا)", fileReadErr:"تعذّرت قراءة الملف", intro:"Genisi ذكاء اصطناعي جزائري من مشروع <b>AnesNT</b>، يملكه <b>Anes Kameche</b>.", howto:"<h3>المحادثة</h3><ul><li><b>Flash</b>: إجابات سريعة.</li><li><b>Pro</b>: إجابات احترافية ومفصّلة.</li></ul><h3>الملفات</h3><ul><li>أرفق صوراً أو PDF أو ملفات نصية/برمجية — يقرأها Genisi مباشرة.</li></ul><h3>الصور والمستندات</h3><ul><li>ولّد صوراً وأنشئ PDF/Word/PPT/Excel.</li></ul>" },
fr:{ newChat:"Nouvelle discussion", searchChats:"Rechercher", recent:"Récents", settings:"Paramètres", askGenisi:"Demandez à Genisi", theme:"Thème", light:"Clair", dark:"Sombre", auto:"Auto", language:"Langue", defaultModel:"Modèle", voice:"Voix", autoTts:"Lecture auto", howtoBtn:"Comment utiliser Genisi ?", clearBtn:"Effacer tout", flashDesc:"Rapide et intelligent", proDesc:"Réponses pro et précises", welcome:"Bienvenue sur Genisi", welcomeSub:"Personnalisons votre expérience.", name:"Nom", yourName:"Votre nom", dob:"Date de naissance", gender:"Genre", male:"Homme", female:"Femme", noSay:"Préfère ne pas dire", confirm:"Confirmer", howtoTitle:"Comment utiliser Genisi", getStarted:"Commencer", downloadDoc:"Télécharger en", downloadImg:"Télécharger l'image", errConnect:"Échec de connexion", stopped:"Arrêté", newConvo:"Nouvelle discussion", micUnsupported:"Dictée non supportée.", confirmClear:"Effacer toutes les données ?", highLoad:"Genisi est sous forte charge.", fileTooBig:"Fichier trop volumineux (max 15 Mo)", fileReadErr:"Impossible de lire le fichier", intro:"Genisi est une IA algérienne du projet <b>AnesNT</b>, détenue par <b>Anes Kameche</b>.", howto:"<h3>Discussion</h3><ul><li><b>Flash</b> : rapide.</li><li><b>Pro</b> : réponses professionnelles et détaillées.</li></ul><h3>Fichiers</h3><ul><li>Joignez images, PDF ou fichiers texte/code — Genisi les lit directement.</li></ul><h3>Images & Docs</h3><ul><li>Générez des images et créez PDF/Word/PPT/Excel.</li></ul>" }
};
const t = k => (I18N[state.settings.lang]||I18N.en)[k] || I18N.en[k] || k;
let state = {
user: JSON.parse(localStorage.getItem("genisi_user")||"null"),
settings: JSON.parse(localStorage.getItem("genisi_settings")||'{"theme":"light","lang":"ar","model":"flash","ttsAuto":false}'),
sessions: JSON.parse(localStorage.getItem("genisi_sessions")||"[]"),
currentId:null, busy:false, abort:null,
};
let pendingAttachments = [];
const save = () => { localStorage.setItem("genisi_user",JSON.stringify(state.user)); localStorage.setItem("genisi_settings",JSON.stringify(state.settings)); localStorage.setItem("genisi_sessions",JSON.stringify(state.sessions)); };
const $ = s => document.querySelector(s);
const cur = () => state.sessions.find(s => s.id === state.currentId);
function init(){
applyTheme(); applyLang();
document.querySelectorAll('input[name="theme"]').forEach(r=>r.checked=r.value===state.settings.theme);
document.querySelectorAll('input[name="lang"]').forEach(r=>r.checked=r.value===state.settings.lang);
document.querySelectorAll('input[name="dmodel"]').forEach(r=>r.checked=r.value===state.settings.model);
const ttsEl = $("#ttsAuto");
if(ttsEl) ttsEl.checked = !!state.settings.ttsAuto;
if(state.settings.model === "codex"){ state.settings.model = "pro"; save(); }
setModel(state.settings.model, true);
buildRecents();
if(state.user) refreshUserUI(); else $("#welcomeModal").classList.add("open");
marked.setOptions({ breaks:true, gfm:true });
if(window.innerWidth <= 768){ $("#sidebar").classList.add("closed"); $("#topbar").classList.add("show"); }
setTimeout(()=>{ if(window.renderMathInElement) renderMathAll(); }, 1200);
}
function applyLang(){
const l=state.settings.lang, rtl=l==="ar";
document.documentElement.lang=l; document.documentElement.dir=rtl?"rtl":"ltr";
document.querySelectorAll("[data-i18n]").forEach(el=>{ const k=el.getAttribute("data-i18n"); const v=t(k); (k==="intro"||k==="howto")?el.innerHTML=v:el.textContent=v; });
document.querySelectorAll("[data-ph]").forEach(el=> el.placeholder=t(el.getAttribute("data-ph")));
$("#howtoIntro").innerHTML=t("intro"); $("#howtoBody").innerHTML=t("howto");
$("#greeting").textContent = state.user?greetText():t("askGenisi");
}
function setLang(l){ state.settings.lang=l; save(); applyLang(); }
let pickedGender=null;
function pickGender(el){ document.querySelectorAll(".gender-opt").forEach(o=>o.classList.remove("sel")); el.classList.add("sel"); pickedGender=el.dataset.g; }
function confirmOnboard(){ const name=$("#obName").value.trim(); if(!name){ $("#obName").focus(); return; } state.user={name,dob:$("#obDob").value||"",gender:pickedGender||""}; save(); refreshUserUI(); $("#welcomeModal").classList.remove("open"); $("#howtoModal").classList.add("open"); }
function refreshUserUI(){ $("#userName").textContent=state.user.name; $("#userAvatar").textContent=(state.user.name[0]||"G").toUpperCase(); $("#greeting").textContent=greetText(); }
function greetText(){ const n=state.user?.name||""; const l=state.settings.lang; if(l==="ar") return `كيف يمكنني مساعدتك${n?"، "+n:""}؟`; if(l==="fr") return `Comment puis-je vous aider${n?", "+n:""} ?`; return `How can I help you${n?", "+n:""}?`; }
function openHowto(){ $("#howtoModal").classList.add("open"); }
function closeHowto(){ $("#howtoModal").classList.remove("open"); }
function setTheme(th){ state.settings.theme=th; save(); applyTheme(); }
function applyTheme(){ let th=state.settings.theme; if(th==="auto") th=matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light"; document.documentElement.setAttribute("data-theme",th); }
function closeSidebar(){ $("#sidebar").classList.add("closed"); $("#topbar").classList.add("show"); }
function openSidebar(){ $("#sidebar").classList.remove("closed"); $("#topbar").classList.remove("show"); }
function openSettings(){ $("#settingsPanel").classList.add("open"); $("#overlay").classList.add("active"); }
function closeSettings(){ $("#settingsPanel").classList.remove("open"); $("#overlay").classList.remove("active"); }
function openSearch(){ $("#searchPane").classList.add("open"); buildSP(); setTimeout(()=>$("#spInput").focus(),50); }
function closeSearch(){ $("#searchPane").classList.remove("open"); $("#spInput").value=""; }
function toggleModelMenu(e,id){ e.stopPropagation(); document.querySelectorAll(".model-menu-h").forEach(m=>{ if(m.id!==id) m.classList.remove("open"); }); $("#"+id).classList.toggle("open"); }
document.addEventListener("click", ()=>{ document.querySelectorAll(".model-menu-h").forEach(m=>m.classList.remove("open")); });
function setModel(m, silent){
if(m === "codex") m = "pro";
state.settings.model=m; save();
const labels={flash:"Flash",pro:"Pro"};
document.querySelectorAll(".modelLabel").forEach(e=>e.textContent=labels[m]||"Flash");
document.querySelectorAll('input[name="dmodel"]').forEach(r=>r.checked=r.value===m);
if(!silent) document.querySelectorAll(".model-menu-h").forEach(mm=>mm.classList.remove("open"));
}
function newChat(){
state.currentId=null;
$("#shell").classList.remove("chat");
$("#msgsInner").innerHTML="";
$("#homeTA").value=""; $("#chatTA").value="";
onTaInput($("#homeTA"),"homeSend"); onTaInput($("#chatTA"),"chatSend");
clearAttachments();
$("#greeting").textContent=greetText();
buildRecents();
closeSearch();
}
function buildRecents(){ $("#recentWrap").innerHTML = state.sessions.map(s=>`<div class="recent-item ${s.id===state.currentId?'active':''}" onclick="loadSession('${s.id}')"><span class="ri-text">${escapeHtml(s.title||t("newConvo"))}</span><span class="ri-more" onclick="event.stopPropagation();deleteSession('${s.id}')"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></span></div>`).join(""); }
function buildSP(){ $("#spResults").innerHTML = state.sessions.map(s=>`<div class="sp-row" data-t="${escapeHtml((s.title||'').toLowerCase())}" onclick="loadSession('${s.id}');closeSearch();"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg><span class="sr-text">${escapeHtml(s.title||t("newConvo"))}</span><span class="sr-date">${s.date||""}</span></div>`).join(""); }
function filterSP(q){ q=q.toLowerCase(); document.querySelectorAll("#spResults .sp-row").forEach(r=>{ const tt = r.dataset.t || ""; r.classList.toggle("hide", !!(q && !tt.includes(q))); }); }
function deleteSession(id){ state.sessions=state.sessions.filter(s=>s.id!==id); if(state.currentId===id) newChat(); save(); buildRecents(); }
function loadSession(id){
state.currentId=id;
const s=cur();
if(!s) return;
$("#shell").classList.add("chat");
$("#msgsInner").innerHTML="";
$("#homeTA").value=""; $("#chatTA").value="";
onTaInput($("#homeTA"),"homeSend"); onTaInput($("#chatTA"),"chatSend");
s.messages.forEach(m=> m.role==="user"?addUser(m.displayText||m.content):addAI(m.content,false));
buildRecents();
renderMathAll();
scrollDown();
}
function onTaInput(el,sendId){ el.style.height="auto"; el.style.height=Math.min(el.scrollHeight,180)+"px"; if(!state.busy) $("#"+sendId).disabled=!el.value.trim() && pendingAttachments.length===0; }
function onTaKey(e,mode){ if(e.key==="Enter"&&!e.shiftKey){ e.preventDefault(); send(mode); } }
/* ===== FILE UPLOAD ===== */
function uploadFiles(){
const input=document.createElement("input");
input.type="file"; input.multiple=true;
input.onchange=e=>{ for(const f of e.target.files) readFileAsBase64(f); input.value=""; };
input.click();
}
function readFileAsBase64(file){
if(file.size > MAX_ATTACHMENT_BYTES){ alert(`${file.name}: ${t("fileTooBig")}`); return; }
const entry = {name:file.name, mime:file.type||"", size:file.size, data:"", loading:true};
pendingAttachments.push(entry); renderAttachments();
const reader = new FileReader();
reader.onload = () => {
const result = reader.result || "";
const base64 = typeof result === "string" && result.includes(",") ? result.split(",")[1] : "";
entry.data = base64; entry.loading = false; renderAttachments();
onTaInput($("#homeTA"),"homeSend"); onTaInput($("#chatTA"),"chatSend");
};
reader.onerror = () => {
alert(`${file.name}: ${t("fileReadErr")}`);
const i = pendingAttachments.indexOf(entry);
if(i !== -1) pendingAttachments.splice(i,1); renderAttachments();
};
reader.readAsDataURL(file);
}
function renderAttachments(){
[$("#homeAtts"),$("#chatAtts")].forEach(w=>{
if(!w) return;
w.innerHTML = pendingAttachments.map((a,i)=>
`<div class="att-chip ${a.loading?'loading':''}"><span class="att-name">${escapeHtml(a.name)}${a.loading?' …':''}</span><button class="att-x" onclick="removeAttachment(${i})">×</button></div>`
).join("");
});
const homeSend = $("#homeSend"), chatSend = $("#chatSend");
if(!state.busy){
if(homeSend) homeSend.disabled = !$("#homeTA").value.trim() && pendingAttachments.length===0;
if(chatSend) chatSend.disabled = !$("#chatTA").value.trim() && pendingAttachments.length===0;
}
}
function removeAttachment(i){ pendingAttachments.splice(i,1); renderAttachments(); }
function clearAttachments(){ pendingAttachments=[]; renderAttachments(); }
function escapeHtml(s){ return (s||"").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c])); }
function addUser(text){ const el=document.createElement("div"); el.className="msg-user"; el.textContent=text; $("#msgsInner").appendChild(el); scrollDown(); return el; }
function addAI(md, animate){
const wrap=document.createElement("div"); wrap.className="msg-ai";
const avatar=document.createElement("div"); avatar.className="msg-ai-avatar";
avatar.innerHTML = `<img src="${AVATAR_URL}" alt="G" onerror="this.parentElement.textContent='G'">`;
const col=document.createElement("div"); col.className="msg-ai-col";
const body=document.createElement("div"); body.className="msg-ai-body";
col.appendChild(body);
wrap.appendChild(avatar);
wrap.appendChild(col);
$("#msgsInner").appendChild(wrap);
const contentTarget = document.createElement('div');
contentTarget.className = 'ai-content-target';
body.appendChild(contentTarget);
if(md!==undefined && md!==null && md!==""){ renderInto(contentTarget, md); }
scrollDown(); return {wrap, body, contentTarget};
}
function renderInto(target, md){
const temp = document.createElement('div');
temp.innerHTML = marked.parse(md || "");
while(temp.firstChild){ target.appendChild(temp.firstChild); }
enhanceCode(target); renderMath(target);
}
function enhanceCode(scope){
scope.querySelectorAll("pre code").forEach(code=>{
if(code.dataset.done) return; code.dataset.done="1";
const pre=code.parentElement, lang=(code.className.match(/language-(\w+)/)||[])[1]||"code";
const wrap=document.createElement("div"); wrap.className="code-wrap";
const bar=document.createElement("div"); bar.className="code-bar"; bar.innerHTML=`<span>${lang}</span>`;
const btn=document.createElement("button"); btn.className="copy-btn"; btn.textContent="Copy";
btn.onclick=()=>{ navigator.clipboard.writeText(code.innerText); btn.textContent="Copied"; setTimeout(()=>btn.textContent="Copy",1500); };
bar.appendChild(btn); pre.parentNode.insertBefore(wrap,pre); wrap.appendChild(bar); wrap.appendChild(pre);
try{ hljs.highlightElement(code); }catch(e){}
});
}
function renderMath(scope){ if(window.renderMathInElement){ try{ renderMathInElement(scope,{delimiters:[{left:"$$",right:"$$",display:true},{left:"\\[",right:"\\]",display:true},{left:"\\(",right:"\\)",display:false},{left:"$",right:"$",display:false}]}); }catch(e){} } }
function renderMathAll(){ document.querySelectorAll(".msg-ai-body").forEach(renderMath); }
function scrollDown(){ const m=$("#msgs"); if(m) m.scrollTop=m.scrollHeight; }
function setSendButtons(stop){
[["#homeSend","home"],["#chatSend","chat"]].forEach(([sel,mode])=>{
const b=$(sel); if(!b) return;
if(stop){ b.classList.add("stop"); b.disabled=false; b.innerHTML=`<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="5" y="5" width="14" height="14" rx="2"/></svg>`; b.onclick=stopResponse; }
else { b.classList.remove("stop"); b.innerHTML=`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="20" x2="12" y2="5"/><polyline points="6 11 12 5 18 11"/></svg>`; b.onclick=()=>send(mode); }
});
}
function stopResponse(){ if(state.abort) state.abort.abort(); }
/* ===== STREAM ENGINE (simple, no thinking box) ===== */
class StreamEngine {
constructor(contentTarget) {
this.contentTarget = contentTarget;
this.answerRaw = '';
this.answerInit = false;
this.cursorEl = document.createElement('span');
this.cursorEl.className = 'stream-cursor';
this.rafPending = false;
this.lastRender = 0;
this.RENDER_INTERVAL = 60;
}
_paintAnswer() {
if(!this.answerInit) { this.contentTarget.innerHTML = ''; this.answerInit = true; }
this.contentTarget.textContent = this.answerRaw;
this.contentTarget.appendChild(this.cursorEl);
this._scroll();
}
_scroll() {
if(this.rafPending) return;
this.rafPending = true;
requestAnimationFrame(() => { this.rafPending = false; scrollDown(); });
}
feed(chunk) {
this.answerRaw += chunk;
const now = Date.now();
if(now - this.lastRender > this.RENDER_INTERVAL) { this._paintAnswer(); this.lastRender = now; }
}
finalize() {
if(this.cursorEl && this.cursorEl.parentNode) this.cursorEl.parentNode.removeChild(this.cursorEl);
this.contentTarget.innerHTML = marked.parse(this.answerRaw || "");
enhanceCode(this.contentTarget);
renderMath(this.contentTarget);
scrollDown();
return { answerRaw: this.answerRaw };
}
}
async function send(mode){
const ta = mode==="home" ? $("#homeTA") : $("#chatTA");
const text = ta.value.trim();
const attachmentsForSend = pendingAttachments.filter(a=>!a.loading && a.data);
if((!text && attachmentsForSend.length===0) || state.busy) return;
if(pendingAttachments.some(a=>a.loading)){
await new Promise(res=>{
const iv = setInterval(()=>{ if(!pendingAttachments.some(a=>a.loading)){ clearInterval(iv); res(); } }, 100);
setTimeout(()=>{ clearInterval(iv); res(); }, 5000);
});
}
const finalAttachments = pendingAttachments.filter(a=>!a.loading && a.data)
.map(a=>({name:a.name, mime:a.mime, data:a.data}));
if(!state.currentId){
const id = Date.now() + "";
const now = new Date();
const dateStr = now.toLocaleDateString(state.settings.lang === 'ar' ? 'ar-SA' : state.settings.lang === 'fr' ? 'fr-FR' : 'en-US');
state.sessions.unshift({id, title:"", date:dateStr, messages:[]});
state.currentId = id;
}
const s = cur();
$("#shell").classList.add("chat");
if(mode==="home"){
$("#msgsInner").innerHTML="";
s.messages.forEach(m => m.role==="user" ? addUser(m.displayText||m.content) : addAI(m.content, false));
}
ta.value=""; ta.style.height="auto";
const attNames = finalAttachments.map(a=>a.name);
clearAttachments();
onTaInput(ta, mode==="home"?"homeSend":"chatSend");
const displayText = text + (attNames.length ? "\n" + attNames.map(n=>"📎 "+n).join("\n") : "");
addUser(displayText);
s.messages.push({role:"user", content:text, displayText, attachments: attNames});
buildRecents();
const model = state.settings.model;
const {body, contentTarget} = addAI(undefined, true);
const engine = new StreamEngine(contentTarget);
state.busy = true;
setSendButtons(true);
state.abort = new AbortController();
let toolCalls = [];
try {
const res = await fetch(API+"/api/chat/stream", {
method:"POST",
headers:{"Content-Type":"application/json"},
signal:state.abort.signal,
body: JSON.stringify({
messages: s.messages.slice(0,-1).map(m=>({role:m.role, content:m.content})),
message: text,
model,
attachments: finalAttachments
})
});
if(!res.ok){
const e = await res.json().catch(()=>({}));
throw new Error(e.detail || `HTTP ${res.status}`);
}
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
while(true) {
const {value, done} = await reader.read();
if(done) break;
buf += dec.decode(value, {stream:true});
let eventEnd;
while((eventEnd = buf.indexOf("\n\n")) !== -1) {
const event = buf.slice(0, eventEnd);
buf = buf.slice(eventEnd + 2);
const lines = event.split("\n");
let dataLine = "";
for(const line of lines) {
if(line.startsWith("data: ")) {
dataLine = line.slice(6);
}
}
if(!dataLine || dataLine === "[DONE]") continue;
let j;
try { j = JSON.parse(dataLine); } catch(e) { continue; }
if(j.type === "chunk") { engine.feed(j.data); }
else if(j.type === "tool_calls") { toolCalls = j.data || []; }
else if(j.type === "error") {
contentTarget.innerHTML = `<p style="color:#d33">${escapeHtml(j.data)}</p>`;
}
/* thinking_start / thinking / thinking_end events are ignored intentionally */
}
}
const result = engine.finalize();
if(toolCalls.length) {
for(const tc of toolCalls) await handleToolCall(tc, body);
}
s.messages.push({role:"assistant", content: result.answerRaw});
if(!s.title && text) s.title = text.slice(0,40) + (text.length>40 ? "..." : "");
else if(!s.title && attNames.length) s.title = attNames[0];
save(); buildRecents();
} catch(err) {
if(err.name === "AbortError") {
const result = engine.finalize();
if(result.answerRaw) {
s.messages.push({role:"assistant", content:result.answerRaw});
save(); buildRecents();
} else {
contentTarget.innerHTML = `<p style="color:var(--muted)">${t("stopped")}</p>`;
}
} else {
const msg = err.message || String(err);
contentTarget.innerHTML = (msg.includes("high load") || msg.includes("503"))
? `<p style="color:#d33">${t("highLoad")}</p>`
: `<p style="color:#d33">${t("errConnect")}: ${escapeHtml(msg)}</p>`;
}
} finally {
state.busy = false;
state.abort = null;
setSendButtons(false);
onTaInput($("#homeTA"),"homeSend");
onTaInput($("#chatTA"),"chatSend");
}
}
async function handleToolCall(tc, scope){
try {
const result = JSON.parse(tc.result || "{}");
if(result.special === "image") {
const n = document.createElement("div");
n.innerHTML = `<p style="color:var(--muted);font-size:13px;margin:8px 0;">${escapeHtml(result.prompt || "")}</p>`;
if(result.images) {
for(const img of result.images) {
if(img.url) {
const im = document.createElement("img"); im.className = "gen-img"; im.src = img.url; n.appendChild(im);
const ac = document.createElement("div"); ac.className = "img-actions";
const dl = document.createElement("button"); dl.className = "suggest"; dl.textContent = t("downloadImg");
dl.onclick = () => { const a = document.createElement("a"); a.href = img.url; a.download = "genisi.png"; a.target = "_blank"; a.click(); };
ac.appendChild(dl); n.appendChild(ac);
}
else if(img.b64) {
const im = document.createElement("img"); im.className = "gen-img"; im.src = "data:image/png;base64," + img.b64; n.appendChild(im);
const ac = document.createElement("div"); ac.className = "img-actions";
const dl = document.createElement("button"); dl.className = "suggest"; dl.textContent = t("downloadImg");
dl.onclick = () => { const a = document.createElement("a"); a.href = "data:image/png;base64," + img.b64; a.download = "genisi.png"; a.click(); };
ac.appendChild(dl); n.appendChild(ac);
}
}
}
scope.appendChild(n);
}
else if(result.special === "document") { renderDocPreview(result, scope); }
else if(tc.tool === "web_search") {
let r = result;
if(Array.isArray(r) && r.length) {
const d = document.createElement("div"); d.style.marginTop = "12px";
d.innerHTML = r.map(x =>
`<a href="${x.url}" target="_blank" style="display:block;margin:8px 0;color:var(--purple);text-decoration:none;">` +
`<b>${escapeHtml(x.title)}</b><br><small style="color:var(--muted)">${escapeHtml(x.snippet || "")}</small></a>`
).join("");
scope.appendChild(d);
}
}
} catch(e) { console.error("Tool error:", e); }
}
function asTextC(v) {
if(v == null) return "";
if(typeof v === "string") return v;
if(Array.isArray(v)) return v.map(asTextC).join("\n");
if(typeof v === "object") return v.text || v.body || v.content || "";
return String(v);
}
function docToHtml(cmd) {
const c = cmd.content || {}, esc = escapeHtml, b = [];
if(c.heading) b.push(`<h2>${esc(c.heading)}</h2>`);
(c.paragraphs || []).forEach(p => b.push(`<p>${esc(asTextC(p))}</p>`));
(c.sections || []).forEach(s => {
if(s.title) b.push(`<h3>${esc(s.title)}</h3>`);
const x = s.body || s.content;
if(x) b.push(`<p>${esc(asTextC(x))}</p>`);
if(s.bullets && s.bullets.length) {
b.push('<ul>');
s.bullets.forEach(y => b.push(`<li>${esc(asTextC(y))}</li>`));
b.push('</ul>');
}
});
(c.slides || []).forEach((s, i) => b.push(
`<div class="doc-slide"><span class="doc-slide-n">${i+1}</span><h3>${esc(s.title || "")}</h3>` +
`<ul>${(s.bullets || []).map(y => `<li>${esc(asTextC(y))}</li>`).join("")}</ul></div>`
));
if(c.headers) {
b.push(`<table><thead><tr>${c.headers.map(h => `<th>${esc(asTextC(h))}</th>`).join("")}</tr></thead>`);
b.push(`<tbody>${(c.rows || []).map(r => `<tr>${r.map(x => `<td>${esc(asTextC(x))}</td>`).join("")}</tr>`).join("")}</tbody></table>`);
}
return `<div class="doc-paper"><h1 class="doc-title">${esc(cmd.title || "Document")}</h1>${b.join("")}</div>`;
}
function renderDocPreview(cmd, scope) {
const wrap = document.createElement("div"); wrap.className = "doc-preview"; wrap.innerHTML = docToHtml(cmd); scope.appendChild(wrap);
const bar = document.createElement("div"); bar.className = "doc-dl"; bar.innerHTML = `<span class="doc-dl-label">${t("downloadDoc")}</span>`;
[["pdf","PDF"],["docx","Word"],["pptx","PowerPoint"],["xlsx","Excel"]].forEach(([fmt, label]) => {
const b = document.createElement("button"); b.className = "doc-dl-btn"; b.textContent = label;
b.onclick = async () => {
b.textContent = "..."; b.disabled = true;
try {
const r = await fetch(API+"/api/document", {
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({...cmd, format:fmt})
});
const blob = await r.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a"); a.href = url; a.download = (cmd.title || "genisi") + "." + fmt; a.click();
URL.revokeObjectURL(url);
} catch(e) {}
finally { b.textContent = label; b.disabled = false; }
};
bar.appendChild(b);
});
scope.appendChild(bar);
}
let recog = null;
function toggleMic(taId, btn) {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if(!SR) { alert(t("micUnsupported")); return; }
if(recog) { recog.stop(); recog = null; btn.classList.remove("recording"); return; }
recog = new SR();
recog.lang = state.settings.lang === "ar" ? "ar-SA" : state.settings.lang === "fr" ? "fr-FR" : "en-US";
recog.continuous = false; recog.interimResults = true; btn.classList.add("recording");
const base = $("#" + taId).value;
recog.onresult = e => { let tx = ""; for(const r of e.results) tx += r[0].transcript; $("#" + taId).value = (base ? base + " " : "") + tx; onTaInput($("#" + taId), taId === "homeTA" ? "homeSend" : "chatSend"); };
recog.onend = () => { btn.classList.remove("recording"); recog = null; };
recog.onerror = () => { btn.classList.remove("recording"); recog = null; };
recog.start();
}
function clearAll() {
if(confirm(t("confirmClear"))) { localStorage.clear(); location.reload(); }
}
const ttsEl = $("#ttsAuto");
if(ttsEl) ttsEl.addEventListener("change", e => { state.settings.ttsAuto = e.target.checked; save(); });
window.addEventListener('resize', () => {
if(window.innerWidth > 768) {
$("#sidebar").classList.remove("closed");
$("#topbar").classList.remove("show");
} else {
$("#sidebar").classList.add("closed");
$("#topbar").classList.add("show");
}
});
init();
</script>
</body>
</html>