Genisi / static /index.html
AnesKAM's picture
Update static/index.html
d704101 verified
Raw
History Blame Contribute Delete
81.9 kB
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Genisi - AI Assistant</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700&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;}
:root{
--sidebar-bg:#dde3ea; --sidebar-hover:rgba(0,0,0,0.08); --sidebar-active:rgba(0,0,0,0.10);
--bg:#ffffff; --text:#1f1f1f; --text2:#444746; --muted:#6b6f6e; --divider:#e3e6ea;
--white:#fff; --purple:#7c3aed; --upgrade-bg:#f3e8ff;
--card:#fff; --code-bg:#0d1117; --think-bg:#f5f3fb;
--input-shadow:0 1px 3px rgba(0,0,0,0.10),0 1px 2px rgba(0,0,0,0.06);
--menu-shadow:0 2px 6px 2px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.3);
--sidebar-w:268px; --transition:cubic-bezier(0.2,0,0,1);
--success:#22c55e; --warning:#f59e0b; --danger:#ef4444;
}
html[data-theme="dark"]{
--sidebar-bg:#1b1c1d; --sidebar-hover:rgba(255,255,255,0.08); --sidebar-active:rgba(255,255,255,0.12);
--bg:#131314; --text:#e3e3e3; --text2:#c4c7c5; --muted:#9aa0a6; --divider:#3c4043;
--white:#1e1f20; --upgrade-bg:#2d2535; --card:#1e1f20; --think-bg:#202124;
}
html,body{height:100%;font-family:'Inter','Cairo',sans-serif;background:var(--bg);color:var(--text);overflow:hidden;}
html[dir="rtl"] body{font-family:'Cairo','Inter',sans-serif;}
.shell{display:flex;height:100vh;position:relative;}
.sidebar{width:var(--sidebar-w);min-width:var(--sidebar-w);background:var(--sidebar-bg);display:flex;flex-direction:column;overflow:hidden;transition:width .25s var(--transition),min-width .25s var(--transition);position:relative;z-index:20;}
.sidebar.closed{width:0;min-width:0;}
.sb-head{display:flex;align-items:center;padding:10px 8px 4px 16px;gap:4px;height:52px;flex-shrink:0;}
.sb-logo{display:flex;align-items:center;gap:9px;flex:1;min-width:0;text-decoration:none;}
.sb-logo img{border-radius:50%;object-fit:cover;flex-shrink:0;}
.sb-logo-text{font-size:18px;font-weight:700;color:var(--text);white-space:nowrap;}
.sb-ico-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;flex-shrink:0;}
.sb-ico-btn:hover{background:var(--sidebar-hover);}
.sb-scroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:4px 0 8px;}
.sb-scroll::-webkit-scrollbar{width:4px;}
.sb-scroll::-webkit-scrollbar-thumb{background:rgba(128,128,128,0.4);border-radius:2px;}
.sb-nav{padding:4px 8px 0;}
.sb-item{display:flex;align-items:center;gap:12px;padding:10px 14px;border-radius:28px;cursor:pointer;font-size:14px;font-weight:500;color:var(--text);white-space:nowrap;transition:background .15s;user-select:none;}
.sb-item:hover{background:var(--sidebar-hover);}
.sb-item svg{flex-shrink:0;color:var(--text2);}
.sb-item .skill-badge{font-size:10px;background:var(--purple);color:#fff;padding:2px 6px;border-radius:10px;margin-inline-start:auto;}
.sb-section{font-size:11px;font-weight:600;color:var(--muted);padding:14px 16px 4px;letter-spacing:.6px;text-transform:uppercase;white-space:nowrap;}
.recent-wrap{padding:0 8px;}
.recent-item{display:flex;align-items:center;padding:8px 14px;border-radius:28px;cursor:pointer;font-size:13.5px;color:var(--text);white-space:nowrap;gap:6px;transition:background .15s;}
.recent-item:hover{background:var(--sidebar-hover);}
.recent-item.active{background:var(--sidebar-active);}
.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(128,128,128,0.25);}
.sb-bottom{flex-shrink:0;padding:6px 8px 10px;border-top:1px solid var(--divider);}
.sb-settings{display:flex;align-items:center;gap:12px;padding:10px 14px;border-radius:28px;cursor:pointer;font-size:14px;font-weight:500;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:8px 14px;border-radius:28px;cursor:pointer;transition:background .15s;}
.sb-user:hover{background:var(--sidebar-hover);}
.avatar{width:30px;height:30px;border-radius:50%;background:var(--purple);color:#fff;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;flex-shrink:0;}
.sb-user-name{font-size:14px;font-weight:500;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{position:absolute;top:0;inset-inline:0;height:52px;z-index:15;display:flex;align-items:center;padding:0 8px;pointer-events:none;opacity:0;transition:opacity .2s;}
.topbar.show{opacity:1;pointer-events:all;}
.tb-left{display:flex;align-items:center;gap:0;}
.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);}
.home{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 20px 80px;position:relative;overflow:hidden;}
.home::before{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-55%);width:900px;height:700px;background:radial-gradient(ellipse 50% 45% at 50% 50%,rgba(168,85,247,.30) 0%,rgba(168,85,247,.15) 30%,transparent 60%),radial-gradient(ellipse 40% 50% at 55% 45%,rgba(147,112,219,.18) 0%,transparent 65%),radial-gradient(ellipse 45% 40% at 45% 60%,rgba(196,124,255,.16) 0%,transparent 70%);filter:blur(90px);pointer-events:none;z-index:0;}
.greeting{font-size:38px;font-weight:600;color:var(--text);margin-bottom:28px;text-align:center;position:relative;z-index:1;animation:greet .45s ease both;}
@keyframes greet{from{opacity:0;transform:translateY(14px);}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;}
.att-chip img{width:32px;height:32px;border-radius:6px;object-fit:cover;flex-shrink:0;}
.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;}
.input-box{background:var(--card);border-radius:28px;box-shadow:var(--input-shadow);display:flex;align-items:flex-end;padding:10px 16px;gap:4px;border:1.5px solid var(--divider);transition:box-shadow .2s,border-color .2s;}
.input-box:focus-within{border-color:transparent;box-shadow:0 0 28px rgba(124,58,237,.25),0 0 60px rgba(168,85,247,.12);}
.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;}
.input-plus:hover,.mic-btn:hover{background:var(--sidebar-hover);}
.mic-btn.recording{background:#e8453c;color:#fff;}
.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;}
.model-btn{display:flex;align-items:center;gap:3px;padding:7px 10px;border-radius:20px;border:none;background:transparent;font-family:inherit;font-size:14px;color:var(--text2);font-weight:600;cursor:pointer;white-space:nowrap;transition:background .15s;}
.model-btn:hover{background:var(--sidebar-hover);}
.send-btn{width:36px;height:36px;border:none;background:var(--purple);border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;transition:background .15s,transform .1s;}
.send-btn:hover{background:#6d28d9;}
.send-btn:active{transform:scale(.93);}
.send-btn:disabled{background:#c4c7c5;cursor:default;}
.send-btn.stop{background:#1f1f1f;}
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:600;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 10px 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:700;}
.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:10px;font-weight:700;cursor:pointer;font-family:inherit;}
.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{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:70px 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:20px;}
.msg-user{align-self:flex-end;background:var(--upgrade-bg);color:var(--text);padding:12px 18px;border-radius:20px 20px 5px 20px;max-width:78%;font-size:15px;line-height:1.6;word-break:break-word;white-space:pre-wrap;}
html[dir="rtl"] .msg-user{border-radius:20px 20px 20px 5px;align-self:flex-start;}
.msg-ai{align-self:flex-start;max-width:92%;width:100%;}
html[dir="rtl"] .msg-ai{align-self:flex-end;}
.msg-ai-body{font-size:15.5px;line-height:1.75;color:var(--text);}
.msg-ai-body p{margin:0 0 12px;}
.msg-ai-body ul,.msg-ai-body ol{margin:0 0 12px;padding-inline-start:24px;}
.msg-ai-body h1,.msg-ai-body h2,.msg-ai-body h3{margin:14px 0 8px;}
.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 12px;}
.msg-ai-body table{border-collapse:collapse;width:100%;margin:12px 0;font-size:14px;border-radius:8px;overflow:hidden;}
.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);}
.streaming{white-space:pre-wrap;}
.gen-img{max-width:100%;border-radius:14px;margin:8px 0;display:block;}
.img-actions{display:flex;gap:8px;margin:4px 0 8px;}
.think{background:var(--think-bg);border:1px solid var(--divider);border-radius:14px;margin:0 0 12px;overflow:hidden;}
.think-head{display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:pointer;font-size:13.5px;font-weight:600;color:var(--muted);user-select:none;}
.think-head .chev{transition:transform .2s;margin-inline-start:auto;}
.think.open .chev{transform:rotate(180deg);}
.think-body{display:none;padding:0 14px 12px;font-size:13.5px;line-height:1.7;color:var(--muted);white-space:pre-wrap;}
.think.open .think-body{display:block;}
.think.live .think-head{color:var(--purple);}
.spin{width:14px;height:14px;border:2px solid var(--divider);border-top-color:var(--purple);border-radius:50%;animation:sp .7s linear infinite;}
@keyframes sp{to{transform:rotate(360deg);}}
.code-wrap{position:relative;margin:12px 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:6px 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;}
.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;}
.dots span{display:inline-block;width:8px;height:8px;border-radius:50%;margin:0 2px;background:var(--text);animation:dot 1.2s infinite;}
.dots span:nth-child(2){animation-delay:.18s;}
.dots span:nth-child(3){animation-delay:.36s;}
@keyframes dot{0%,80%,100%{transform:scale(.6);opacity:.4;}40%{transform:scale(1);opacity:1;}}
.suggests{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px;}
.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;}
.model-menu{position:absolute;bottom:calc(100% + 6px);inset-inline-end:0;background:var(--card);border-radius:14px;box-shadow:var(--menu-shadow);padding:6px;min-width:200px;z-index:200;display:none;}
.model-menu.open{display:block;}
.mm-item{display:flex;flex-direction:column;padding:10px 14px;border-radius:10px;cursor:pointer;}
.mm-item:hover{background:var(--sidebar-hover);}
.mm-item b{font-size:14px;}
.mm-item small{font-size:12px;color:var(--muted);}
.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;}
.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:700;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;}
.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);color:#fff;border:none;border-radius:14px;font-size:16px;font-weight:700;cursor:pointer;font-family:inherit;margin-top:8px;}
.modal-btn:hover{background:#6d28d9;}
.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;}
.skills-panel{position:fixed;top:0;inset-inline-start:0;bottom:0;width:320px;max-width:85vw;background:var(--card);box-shadow:2px 0 10px 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"] .skills-panel{transform:translateX(100%);inset-inline-start:auto;inset-inline-end:0;box-shadow:-2px 0 10px rgba(0,0,0,0.15);}
.skills-panel.open{transform:translateX(0)!important;}
.skills-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--divider);}
.skills-title{font-size:16px;font-weight:700;}
.skills-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;}
.skills-close:hover{background:var(--sidebar-hover);}
.skills-content{flex:1;overflow-y:auto;padding:20px;}
.skill-card{background:var(--bg);border:1px solid var(--divider);border-radius:14px;padding:14px;margin-bottom:12px;cursor:pointer;transition:all .15s;}
.skill-card:hover{border-color:var(--purple);background:var(--upgrade-bg);}
.skill-card h4{font-size:14px;font-weight:700;color:var(--text);margin-bottom:4px;}
.skill-card p{font-size:12px;color:var(--muted);line-height:1.5;}
.skill-card .skill-tag{font-size:11px;background:var(--purple);color:#fff;padding:2px 8px;border-radius:10px;display:inline-block;margin-top:6px;}
.doc-preview{border:1px solid var(--divider);border-radius:14px;background:#fff;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:'Inter','Cairo',sans-serif;color:#1f1f1f;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 #e3e6ea;padding:8px 12px;}
.doc-slide{border:1px solid #eadcff;border-radius:12px;padding:16px 18px;margin:12px 0;position:relative;background:linear-gradient(135deg,#faf5ff,#fff);animation:slideIn .4s ease both;}
@keyframes slideIn{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:translateY(0);}}
.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:background .15s;}
.doc-dl-btn:hover{background:#e9d5ff;}
.doc-dl-btn:disabled{opacity:.6;cursor:default;}
.video-box{border:1px solid var(--divider);border-radius:16px;overflow:hidden;margin:12px 0;background:#000;}
.video-stage{position:relative;width:100%;background:#000;display:flex;align-items:center;justify-content:center;}
.video-stage canvas{width:100%;height:auto;display:block;}
.video-ctrl{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--card);flex-wrap:wrap;}
.video-ctrl button{display:flex;align-items:center;gap:6px;background:var(--upgrade-bg);color:var(--purple);border:none;border-radius:12px;padding:8px 16px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:background .15s;}
.video-ctrl button:hover{background:#e9d5ff;}
.video-ctrl button:disabled{opacity:.55;cursor:default;}
.video-ctrl .vstatus{font-size:13px;color:var(--muted);margin-inline-start:auto;}
.search-results{margin:12px 0;padding:12px;background:var(--think-bg);border-radius:12px;border:1px solid var(--divider);}
.search-results a{color:var(--purple);font-weight:600;font-size:14px;text-decoration:none;}
.search-results a:hover{text-decoration:underline;}
.search-results small{color:var(--muted);font-size:12px;display:block;margin-top:2px;margin-bottom:8px;}
.toast{position:fixed;bottom:20px;inset-inline-end:20px;background:var(--card);color:var(--text);padding:12px 18px;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.15);z-index:400;font-size:14px;display:flex;align-items:center;gap:10px;transform:translateY(100px);opacity:0;transition:all .3s;}
.toast.show{transform:translateY(0);opacity:1;}
.toast.success{border-inline-start:4px solid var(--success);}
.toast.error{border-inline-start:4px solid var(--danger);}
svg{user-select:none;}
button svg{pointer-events:none;}
</style>
<base target="_blank">
</head>
<body>
<div class="shell" id="shell">
<div class="sidebar" id="sidebar">
<div class="sb-head">
<a class="sb-logo" href="#" onclick="return false;">
<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>
</a>
<button class="sb-ico-btn" onclick="closeSidebar()">
<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">New chat</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">Search chats</span>
</div>
<div class="sb-item" onclick="openSkills()">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
<span data-i18n="skills">Skills</span>
<span class="skill-badge">8</span>
</div>
</div>
<div class="sb-section" data-i18n="recent">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">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()">
<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()">
<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()">
<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>
<div class="home" id="homeView">
<h1 class="greeting" id="greeting">How can I help you?</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('home')">
<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="Ask Genisi" rows="1" oninput="onTaInput(this,'homeSend')" onkeydown="onTaKey(event,'home')"></textarea>
<div class="input-right">
<div style="position:relative">
<button class="model-btn" onclick="toggleModelMenu(event,'modelMenuHome')">
<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" id="modelMenuHome">
<div class="mm-item" onclick="setModel('flash')"><b>Flash</b><small data-i18n="flashDesc">Fast & smart</small></div>
<div class="mm-item" onclick="setModel('pro')"><b>Pro</b><small data-i18n="proDesc">More accurate</small></div>
</div>
</div>
<button class="mic-btn" id="homeMic" onclick="toggleMic('homeTA',this)">
<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>
<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('chat')">
<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="Ask Genisi" rows="1" oninput="onTaInput(this,'chatSend')" onkeydown="onTaKey(event,'chat')"></textarea>
<div class="input-right">
<div style="position:relative">
<button class="model-btn" onclick="toggleModelMenu(event,'modelMenuChat')">
<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" id="modelMenuChat">
<div class="mm-item" onclick="setModel('flash')"><b>Flash</b><small data-i18n="flashDesc">Fast & smart</small></div>
<div class="mm-item" onclick="setModel('pro')"><b>Pro</b><small data-i18n="proDesc">More accurate</small></div>
</div>
</div>
<button class="mic-btn" id="chatMic" onclick="toggleMic('chatTA',this)">
<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>
<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="Search chats" oninput="filterSP(this.value)" autocomplete="off">
<button class="sp-x" onclick="closeSearch()">
<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">Recent</div>
<div class="sp-results" id="spResults"></div>
</div>
</div>
<div class="skills-panel" id="skillsPanel">
<div class="skills-header">
<h2 class="skills-title" data-i18n="skills">Skills</h2>
<button class="skills-close" onclick="closeSkills()">
<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="skills-content" id="skillsContent">
<div class="skill-card" onclick="useSkill('search')"><h4>🔍 Internet Search</h4><p>Search the web for real-time information.</p><span class="skill-tag">Active</span></div>
<div class="skill-card" onclick="useSkill('image')"><h4>🎨 Image Generation</h4><p>Generate images using NVIDIA AI.</p><span class="skill-tag">Active</span></div>
<div class="skill-card" onclick="useSkill('document')"><h4>📄 Document Creator</h4><p>Create Word, PDF, PowerPoint, Excel.</p><span class="skill-tag">Active</span></div>
<div class="skill-card" onclick="useSkill('video')"><h4>🎬 Video Studio</h4><p>Create animated videos.</p><span class="skill-tag">Active</span></div>
<div class="skill-card" onclick="useSkill('code')"><h4>💻 Code Assistant</h4><p>Write, debug code in any language.</p><span class="skill-tag">Active</span></div>
<div class="skill-card" onclick="useSkill('math')"><h4>📐 Math & LaTeX</h4><p>Solve equations, render formulas.</p><span class="skill-tag">Active</span></div>
<div class="skill-card" onclick="useSkill('tts')"><h4>🔊 Text to Speech</h4><p>Read responses aloud.</p><span class="skill-tag">Active</span></div>
<div class="skill-card" onclick="useSkill('translate')"><h4>🌐 Translation</h4><p>Translate between languages.</p><span class="skill-tag">Active</span></div>
</div>
</div>
<div class="settings-panel" id="settingsPanel">
<div class="settings-header">
<h2 class="settings-title" data-i18n="settings">Settings</h2>
<button class="settings-close" onclick="closeSettings()">
<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">Theme</label>
<label class="settings-option"><input type="radio" name="theme" value="light" onchange="setTheme('light')"><span class="option-text" data-i18n="light">Light</span></label>
<label class="settings-option"><input type="radio" name="theme" value="dark" onchange="setTheme('dark')"><span class="option-text" data-i18n="dark">Dark</span></label>
<label class="settings-option"><input type="radio" name="theme" value="auto" onchange="setTheme('auto')"><span class="option-text" data-i18n="auto">Auto</span></label>
</div>
<div class="settings-divider"></div>
<div class="settings-group">
<label class="settings-label" data-i18n="language">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">Default model</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">Voice</label>
<label class="settings-option"><input type="checkbox" id="ttsAuto"><span class="option-text" data-i18n="autoTts">Auto-play audio</span></label>
</div>
<div class="settings-divider"></div>
<div class="settings-group">
<button class="btn-purple" onclick="openHowto()" data-i18n="howtoBtn">How to use Genisi?</button>
</div>
<div class="settings-group">
<button class="btn-purple" style="background:#fde8e8;color:#d33" onclick="clearAll()" data-i18n="clearBtn">Clear all chats and data</button>
</div>
</div>
</div>
<div class="overlay" id="overlay" onclick="closeSettings();closeSkills();"></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">Welcome to Genisi</h2>
<p class="sub" data-i18n="welcomeSub">Let's personalize your experience. Your data is stored only in your browser for privacy.</p>
<div class="field"><label data-i18n="name">Name</label><input id="obName" data-ph="yourName" placeholder="Your name"></div>
<div class="field"><label data-i18n="dob">Date of birth</label><input id="obDob" type="date"></div>
<div class="field"><label data-i18n="gender">Gender</label>
<div class="gender-row">
<div class="gender-opt" data-g="Male" onclick="pickGender(this)" data-i18n="male">Male</div>
<div class="gender-opt" data-g="Female" onclick="pickGender(this)" data-i18n="female">Female</div>
<div class="gender-opt" data-g="" onclick="pickGender(this)" data-i18n="noSay">Prefer not to say</div>
</div>
</div>
<button class="modal-btn" onclick="confirmOnboard()" data-i18n="confirm">Confirm & continue</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">How to use Genisi</h2>
<p class="sub" id="howtoIntro"></p>
<div id="howtoBody"></div>
<button class="modal-btn" onclick="closeHowto()" data-i18n="getStarted">Get started</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const API = "";
const I18N = {
en:{ newChat:"New chat", searchChats:"Search chats", recent:"Recent", settings:"Settings", askGenisi:"Ask Genisi", skills:"Skills",
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:"More accurate", welcome:"Welcome to Genisi",
welcomeSub:"Let's personalize your experience. Your data is stored only in your browser for privacy.",
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",
thinking:"Thinking...", showThinking:"Show thinking", generatingImage:"Generating image...", searching:"Searching...",
searchingImages:"Finding images...", creatingDoc:"Creating document...", downloadDoc:"Download as", downloadImg:"Download image",
imagesLeft:"images left", errConnect:"Connection failed", stopped:"Response stopped", newConvo:"New conversation",
micUnsupported:"Your browser doesn't support voice input. Use Chrome.", confirmClear:"Clear all chats and data? This cannot be undone.",
cutoff:"Response was too long and got cut off. Retry or ask for shorter content.",
unsupportedFile:"This file type isn't supported by the current model.",
playVideo:"Play", exportVideo:"Export MP4", buildingVideo:"Preparing video...", recording:"Recording...", videoReady:"Video ready",
intro:"Genisi is an Algerian AI developed by the <b>AnesNT</b> project, currently owned by <b>Anes Kameche</b>.",
howto:"<h3>Chat</h3><ul><li>Type your question and switch between <b>Flash</b> (fast) and <b>Pro</b> (accurate) anytime.</li><li>Code blocks with copy, tables, and math.</li></ul><h3>Images</h3><ul><li>Ask for an image, generated with high realism. Download with one click.</li><li>Limit: 3 images every 3 hours.</li></ul><h3>Documents</h3><ul><li>Preview Word, PowerPoint, Excel and PDF, then download in any format.</li></ul><h3>Video Studio</h3><ul><li>Create animated videos from your images or web images, and export to MP4.</li></ul><h3>Files & Voice</h3><ul><li>Upload files, voice dictation, and read responses aloud.</li></ul>" },
ar:{ newChat:"محادثة جديدة", searchChats:"البحث في المحادثات", recent:"الأخيرة", settings:"الإعدادات", askGenisi:"اسأل Genisi", skills:"المهارات",
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:"ابدأ الآن",
thinking:"يفكّر...", showThinking:"عرض التفكير", generatingImage:"جاري توليد الصورة...", searching:"جاري البحث...",
searchingImages:"جاري إيجاد الصور...", creatingDoc:"جاري إنشاء المستند...", downloadDoc:"تحميل بصيغة", downloadImg:"تحميل الصورة",
imagesLeft:"صور متبقية", errConnect:"تعذّر الاتصال", stopped:"تم إيقاف الرد", newConvo:"محادثة جديدة",
micUnsupported:"متصفحك لا يدعم الإملاء الصوتي. استخدم Chrome.", confirmClear:"مسح كل المحادثات والبيانات؟ لا يمكن التراجع.",
cutoff:"الرد طويل وانقطع. أعد المحاولة أو اطلب محتوى أقصر.",
unsupportedFile:"هذا النوع من الملفات غير مدعوم في النموذج الحالي.",
playVideo:"تشغيل", exportVideo:"تصدير MP4", buildingVideo:"تجهيز الفيديو...", recording:"جاري التسجيل...", videoReady:"الفيديو جاهز",
intro:"Genisi ذكاء اصطناعي جزائري طُوّر من طرف مشروع <b>AnesNT</b>، ويملكه حالياً <b>Anes Kameche (أنس كامش)</b>.",
howto:"<h3>المحادثة</h3><ul><li>اكتب سؤالك وبدّل بين <b>Flash</b> (سريع) و <b>Pro</b> (دقيق) في أي وقت.</li><li>أكواد مع نسخ، جداول، ومعادلات.</li></ul><h3>الصور</h3><ul><li>اطلب صورة بدقة واقعية، ونزّلها بضغطة.</li><li>الحد: 3 صور كل 3 ساعات.</li></ul><h3>المستندات</h3><ul><li>عاين Word, PowerPoint, Excel و PDF ثم حمّلها بأي صيغة.</li></ul><h3>استوديو الفيديو</h3><ul><li>أنشئ فيديوهات متحركة من صورك أو من الويب، وصدّرها MP4.</li></ul><h3>الملفات والصوت</h3><ul><li>ارفع ملفات، إملاء صوتي، وقراءة الردود.</li></ul>" },
fr:{ newChat:"Nouvelle discussion", searchChats:"Rechercher", recent:"Récents", settings:"Paramètres", askGenisi:"Demandez à Genisi", skills:"Compétences",
theme:"Thème", light:"Clair", dark:"Sombre", auto:"Auto", language:"Langue", defaultModel:"Modèle par défaut",
voice:"Voix", autoTts:"Lecture audio auto", howtoBtn:"Comment utiliser Genisi ?", clearBtn:"Effacer toutes les données",
flashDesc:"Rapide et intelligent", proDesc:"Plus précis", welcome:"Bienvenue sur Genisi",
welcomeSub:"Personnalisons votre expérience. Vos données restent dans votre navigateur.",
name:"Nom", yourName:"Votre nom", dob:"Date de naissance", gender:"Genre", male:"Homme", female:"Femme", noSay:"Préfère ne pas dire",
confirm:"Confirmer et continuer", howtoTitle:"Comment utiliser Genisi", getStarted:"Commencer",
thinking:"Réflexion...", showThinking:"Voir la réflexion", generatingImage:"Génération de l'image...", searching:"Recherche...",
searchingImages:"Recherche d'images...", creatingDoc:"Création du document...", downloadDoc:"Télécharger en", downloadImg:"Télécharger l'image",
imagesLeft:"images restantes", errConnect:"Échec de connexion", stopped:"Réponse arrêtée", newConvo:"Nouvelle discussion",
micUnsupported:"Votre navigateur ne supporte pas la dictée. Utilisez Chrome.", confirmClear:"Effacer toutes les discussions et données ? Irréversible.",
cutoff:"Réponse trop longue et interrompue. Réessayez ou demandez plus court.",
unsupportedFile:"Ce type de fichier n'est pas pris en charge par le modèle actuel.",
playVideo:"Lire", exportVideo:"Exporter MP4", buildingVideo:"Préparation...", recording:"Enregistrement...", videoReady:"Vidéo prête",
intro:"Genisi est une IA algérienne développée par le projet <b>AnesNT</b>, actuellement détenue par <b>Anes Kameche</b>.",
howto:"<h3>Discussion</h3><ul><li>Posez votre question et basculez entre <b>Flash</b> et <b>Pro</b>.</li><li>Code avec copie, tableaux et équations.</li></ul><h3>Images</h3><ul><li>Demandez une image réaliste, téléchargeable en un clic.</li><li>Limite : 3 images / 3 heures.</li></ul><h3>Documents</h3><ul><li>Aperçu Word, PowerPoint, Excel et PDF, puis téléchargement.</li></ul><h3>Studio Vidéo</h3><ul><li>Créez des vidéos animées et exportez en MP4.</li></ul><h3>Fichiers & Voix</h3><ul><li>Téléversez des fichiers, dictée vocale, lecture des réponses.</li></ul>" }
};
const t = k => (I18N[state.settings.lang] || I18N.en)[k] || I18N.en[k] || k;
const CAPS = {
flash:{ vision:true, imageMimes:["image/jpeg","image/png","image/webp","image/gif"], docExts:["pdf","docx","txt","md","csv","json","js","ts","py","html","css","java","c","cpp"] },
pro:{ vision:false, imageMimes:[], docExts:["pdf","docx","txt","md","csv","json","js","ts","py","html","css","java","c","cpp"] },
};
function acceptFor(model){ const c=CAPS[model]||CAPS.flash; return [...(c.vision?c.imageMimes:[]), ...c.docExts.map(e=>"."+e)].join(","); }
let state = {
user: JSON.parse(localStorage.getItem("genisi_user") || "null"),
settings: JSON.parse(localStorage.getItem("genisi_settings") || '{"theme":"light","lang":"en","model":"flash","ttsAuto":false}'),
sessions: JSON.parse(localStorage.getItem("genisi_sessions") || "[]"),
interests: JSON.parse(localStorage.getItem("genisi_interests") || "[]"),
currentId: null, busy: false, abort: null,
};
let clientId = localStorage.getItem("genisi_client") || (Date.now()+"-"+Math.random().toString(36).slice(2));
localStorage.setItem("genisi_client", clientId);
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)); localStorage.setItem("genisi_interests",JSON.stringify(state.interests)); };
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);
$("#ttsAuto").checked = !!state.settings.ttsAuto;
setModel(state.settings.model, true);
buildRecents();
if(state.user){ refreshUserUI(); } else { $("#welcomeModal").classList.add("open"); }
marked.setOptions({ breaks:true, gfm:true });
}
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"); if(!$("#skillsPanel").classList.contains("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 openSkills(){ $("#skillsPanel").classList.add("open"); $("#overlay").classList.add("active"); }
function closeSkills(){ $("#skillsPanel").classList.remove("open"); if(!$("#settingsPanel").classList.contains("open")) $("#overlay").classList.remove("active"); }
function useSkill(skill){
closeSkills();
const prompts = { search:"Search the web for: ", image:"Generate an image of: ", document:"Create a document about: ", video:"Create a video about: ", code:"Write code for: ", math:"Solve this equation: ", tts:"Read this aloud: ", translate:"Translate: " };
const ta = state.currentId ? $("#chatTA") : $("#homeTA");
ta.value = prompts[skill] || "";
ta.focus();
onTaInput(ta, state.currentId ? "chatSend" : "homeSend");
}
function toggleModelMenu(e,id){ e.stopPropagation(); document.querySelectorAll(".model-menu").forEach(m=>{ if(m.id!==id) m.classList.remove("open"); }); $("#"+id).classList.toggle("open"); }
document.addEventListener("click", ()=>document.querySelectorAll(".model-menu").forEach(m=>m.classList.remove("open")));
function setModel(m, silent){
state.settings.model=m; save();
const label=m==="flash"?"Flash":"Pro";
document.querySelectorAll(".modelLabel").forEach(e=>e.textContent=label);
document.querySelectorAll('input[name="dmodel"]').forEach(r=>r.checked=r.value===m);
if(!silent) document.querySelectorAll(".model-menu").forEach(mm=>mm.classList.remove("open"));
}
function newChat(){ state.currentId=null; $("#shell").classList.remove("chat"); $("#msgsInner").innerHTML=""; $("#homeTA").value=""; $("#homeSend").disabled=true; 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=>r.classList.toggle("hide", q && !r.dataset.t.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=""; s.messages.forEach(m=> m.role==="user"?addUser(m.content,m.attachments):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(); }
function onTaKey(e,mode){ if(e.key==="Enter"&&!e.shiftKey){ e.preventDefault(); send(mode); } }
function uploadFiles(mode){
const model=state.settings.model;
const input=document.createElement("input"); input.type="file"; input.multiple=true; input.accept=acceptFor(model);
input.onchange=async e=>{ for(const f of e.target.files){ await doUpload(f, model); } renderAttachments(); };
input.click();
}
async function doUpload(file, model){
const fd=new FormData(); fd.append("file",file); fd.append("model",model);
try{ const r=await fetch(API+"/api/upload",{method:"POST",body:fd}); const j=await r.json();
if(!r.ok){ showToast(j.message||t("unsupportedFile"), "error"); return; }
if(j.kind==="image") pendingAttachments.push({kind:"image",name:j.name,dataUrl:j.dataUrl});
else pendingAttachments.push({kind:"text",name:j.name,text:j.text});
}catch(err){ showToast(t("unsupportedFile"), "error"); }
}
function attChipHtml(a, idx){
const icon = a.kind==="image" ? `<img src="${a.dataUrl}" alt="">` : `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`;
const x = idx===undefined ? "" : `<button class="att-x" onclick="removeAttachment(${idx})">×</button>`;
return `<div class="att-chip">${icon}<span class="att-name">${escapeHtml(a.name)}</span>${x}</div>`;
}
function renderAttachments(){ [$("#homeAtts"),$("#chatAtts")].forEach(w=>{ if(w) w.innerHTML = pendingAttachments.map((a,i)=>attChipHtml(a,i)).join(""); }); }
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, atts){
const el=document.createElement("div"); el.className="msg-user";
if(atts&&atts.length){ const box=document.createElement("div"); box.className="attachments"; box.style.padding="0 0 8px"; box.innerHTML=atts.map(a=>attChipHtml(a)).join(""); el.appendChild(box); }
const tx=document.createElement("div"); tx.textContent=text; el.appendChild(tx);
$("#msgsInner").appendChild(el); scrollDown(); return el;
}
function addAI(md, animate){ const wrap=document.createElement("div"); wrap.className="msg-ai"; const body=document.createElement("div"); body.className="msg-ai-body"; wrap.appendChild(body); $("#msgsInner").appendChild(wrap); if(md!==undefined) renderInto(body,md); scrollDown(); return {wrap,body}; }
function renderInto(body,md){ body.innerHTML=marked.parse(cleanForRender(md||"")); enhanceCode(body); renderMath(body); }
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"); 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(); }
async function send(mode){
const ta = mode==="home" ? $("#homeTA") : $("#chatTA");
const text = ta.value.trim();
if((!text && !pendingAttachments.length) || state.busy) return;
if(!state.currentId){ const id=Date.now()+""; state.sessions.unshift({id,title:"",date:"now",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.content,m.attachments):addAI(m.content,false)); }
ta.value=""; ta.style.height="auto";
const atts = pendingAttachments.slice();
addUser(text, atts);
s.messages.push({ role:"user", content:text, attachments:atts });
clearAttachments(); buildRecents();
const {body}=addAI(undefined,true);
const think=document.createElement("div"); think.className="think live";
think.innerHTML=`<div class="think-head" onclick="this.parentElement.classList.toggle('open')"><div class="spin"></div><span class="tlabel">${t("thinking")}</span><svg class="chev" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></div><div class="think-body"></div>`;
body.appendChild(think);
const thinkBody=think.querySelector(".think-body");
const answer=document.createElement("div"); answer.className="streaming"; body.appendChild(answer);
answer.innerHTML=`<div class="dots"><span></span><span></span><span></span></div>`;
state.busy=true; setSendButtons(true); state.abort=new AbortController();
let full="", thinking="", rafPending=false, hasThink=false;
const paint=()=>{ rafPending=false;
const th=parseThinking(full);
if(th){ hasThink=true; thinkBody.textContent=th; }
const ans=answerPart(full);
answer.classList.add("streaming");
answer.textContent = cleanForRender(ans);
scrollDown();
};
try{
const res=await fetch(API+"/api/chat",{ method:"POST", headers:{"Content-Type":"application/json"}, signal:state.abort.signal,
body:JSON.stringify({messages:s.messages,model:state.settings.model,user:state.user||{},interests:state.interests,think:state.settings.model==="pro"}) });
const reader=res.body.getReader(), dec=new TextDecoder(); let buf="";
while(true){
const {value,done}=await reader.read(); if(done) break;
buf+=dec.decode(value,{stream:true});
const parts=buf.split("\n\n"); buf=parts.pop();
for(const p of parts){
const line=p.replace(/^data: /,"").trim(); if(!line) continue;
let j; try{ j=JSON.parse(line); }catch(e){ continue; }
if(j.error){ answer.innerHTML=`<p style="color:#d33">${escapeHtml(j.error)}</p>`; continue; }
if(j.done) continue;
full += j.delta||"";
thinking = parseThinking(full);
if(!rafPending){ rafPending=true; requestAnimationFrame(paint); }
}
}
finalizeThink(think, thinking, hasThink);
const finalAns=answerPart(full);
answer.classList.remove("streaming");
answer.innerHTML=marked.parse(cleanForRender(finalAns)); enhanceCode(answer); renderMath(answer);
await handleTools(finalAns, answer);
const sugg=extractSuggestions(full); if(sugg.length) renderSuggestions(answer,sugg);
s.messages.push({role:"assistant",content:finalAns});
if(state.settings.ttsAuto) speak(stripForTTS(finalAns));
maybeTitle(s); learnInterests(text); save(); buildRecents();
}catch(err){
if(err.name==="AbortError"){
finalizeThink(think, thinking, hasThink);
const partial=answerPart(full);
answer.classList.remove("streaming");
answer.innerHTML = partial.trim()? marked.parse(cleanForRender(partial)) : `<p style="color:var(--muted)">${t("stopped")}</p>`;
enhanceCode(answer); renderMath(answer);
if(partial.trim()) s.messages.push({role:"assistant",content:partial});
maybeTitle(s); save(); buildRecents();
}else{ answer.innerHTML=`<p style="color:#d33">${t("errConnect")}: ${escapeHtml(String(err.message||err))}</p>`; }
}finally{ state.busy=false; state.abort=null; setSendButtons(false); $("#homeSend").disabled=true; $("#chatSend").disabled=true; }
}
function finalizeThink(think, thinking, hasThink){ if(hasThink && thinking){ think.classList.remove("live"); think.querySelector(".tlabel").textContent=t("showThinking"); const sp=think.querySelector(".spin"); if(sp) sp.remove(); } else think.remove(); }
function parseThinking(full){ const i=full.indexOf("Thinking..."); if(i===-1) return ""; const a=full.slice(i+11); const sep=a.search(/\n\s*\n(?=\S)/); return sep===-1?a.trim():a.slice(0,sep).trim(); }
function answerPart(full){ const i=full.indexOf("Thinking..."); if(i===-1) return full; const a=full.slice(i+11); const sep=a.search(/\n\s*\n(?=\S)/); return sep===-1?"":a.slice(sep).trim(); }
function extractToolJSON(text){
const key = text.indexOf('"tool"');
if(key === -1) return null;
let start = text.lastIndexOf("{", key);
while(start !== -1){
let depth=0, inStr=false, esc=false;
for(let i=start; i<text.length; i++){
const ch=text[i];
if(esc){ esc=false; continue; }
if(ch==="\\"){ esc=true; continue; }
if(ch==='"'){ inStr=!inStr; continue; }
if(inStr) continue;
if(ch==="{") depth++;
else if(ch==="}"){ depth--; if(depth===0) return text.slice(start, i+1); }
}
start = text.lastIndexOf("{", start-1);
}
return null;
}
function cleanForRender(text){
let out = text||"";
const raw = extractToolJSON(out);
if(raw) out = out.replace(raw, "");
out = out.replace(/```[a-zA-Z]*\s*\{[\s\S]*?"tool"[\s\S]*?```/g, "");
return out.replace(/SUGGESTIONS:\s*\[[\s\S]*?\]\s*$/,"").trim();
}
function extractSuggestions(text){ const m=text.match(/SUGGESTIONS:\s*(\[[\s\S]*?\])/); if(!m) return []; try{ return JSON.parse(m[1]).slice(0,4); }catch(e){ return []; } }
function renderSuggestions(scope,list){ const box=document.createElement("div"); box.className="suggests"; list.forEach(txt=>{ const b=document.createElement("button"); b.className="suggest"; b.textContent=txt; b.onclick=()=>{ $("#chatTA").value=txt; send("chat"); }; box.appendChild(b); }); scope.appendChild(box); }
function downloadDataUrl(dataUrl, filename){ const a=document.createElement("a"); a.href=dataUrl; a.download=filename; document.body.appendChild(a); a.click(); a.remove(); }
async function handleTools(text,scope){
const raw = extractToolJSON(text);
if(!raw) return;
let cmd;
try{ cmd=JSON.parse(raw); }
catch(e){ const n=document.createElement("p"); n.style.color="#d33"; n.textContent=t("cutoff"); scope.appendChild(n); return; }
if(!cmd || !cmd.tool) return;
if(cmd.tool==="image"){
const n=document.createElement("p"); n.textContent=t("generatingImage"); scope.appendChild(n);
try{
const r=await fetch(API+"/api/image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt:cmd.prompt,clientId})});
const j=await r.json();
if(j.image){
const fname="genisi-"+Date.now()+".png";
n.outerHTML=`<img class="gen-img" src="${j.image}" alt=""><div class="img-actions"><button class="suggest" onclick="downloadDataUrl('${j.image}','${fname}')">${t("downloadImg")}</button></div><p style="color:var(--muted);font-size:13px">${j.remaining} ${t("imagesLeft")}</p>`;
showToast("Image generated!", "success");
} else { n.textContent=(j.message||""); showToast(j.message||"Error", "error"); }
}catch(e){ n.textContent="error"; showToast("Image generation failed", "error"); }
} else if(cmd.tool==="search"){
const n=document.createElement("div"); n.className="search-results"; n.innerHTML=`<p style="color:var(--muted)">${t("searching")}</p>`; scope.appendChild(n);
try{ const r=await fetch(API+"/api/search",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:cmd.query})}); const j=await r.json();
n.innerHTML=(j.results||[]).map(x=>`<a href="${x.url}" target="_blank">${escapeHtml(x.title)}</a><small>${escapeHtml(x.snippet)}</small>`).join("");
showToast("Search completed!", "success");
}catch(e){ n.innerHTML=`<p style="color:#d33">Search error</p>`; showToast("Search failed", "error"); }
} else if(cmd.tool==="image_search"){
const n=document.createElement("div"); scope.appendChild(n);
try{ const imgs=await fetchImages(cmd.query); n.innerHTML=imgs.slice(0,6).map(u=>`<img class="gen-img" style="display:inline-block;width:31%;margin:1%" src="${proxify(u)}">`).join(""); }catch(e){ n.textContent="error"; }
} else if(cmd.tool==="document"){
renderDocPreview(cmd, scope);
showToast("Document ready for download!", "success");
} else if(cmd.tool==="video"){
await renderVideo(cmd, scope);
showToast("Video studio ready!", "success");
}
}
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, blocks=[];
if(c.heading) blocks.push(`<h2>${esc(c.heading)}</h2>`);
(c.paragraphs||[]).forEach(p=> blocks.push(`<p>${esc(asTextC(p))}</p>`));
(c.sections||[]).forEach(s=>{ if(s.title) blocks.push(`<h3>${esc(s.title)}</h3>`); const b=s.body||s.content||s.text; if(b) blocks.push(`<p>${esc(asTextC(b))}</p>`); (s.bullets||s.points||[]).forEach(x=> blocks.push(`<li>${esc(asTextC(x))}</li>`)); });
(c.slides||[]).forEach((s,i)=> blocks.push(`<div class="doc-slide"><span class="doc-slide-n">${i+1}</span><h3>${esc(s.title||"")}</h3><ul>${(s.bullets||s.points||[]).map(x=>`<li>${esc(asTextC(x))}</li>`).join("")}</ul></div>`));
if(c.sheets||c.headers){ const sh=(c.sheets&&c.sheets[0])||c; if(sh.headers){ blocks.push(`<table><thead><tr>${sh.headers.map(h=>`<th>${esc(asTextC(h))}</th>`).join("")}</tr></thead><tbody>${(sh.rows||[]).map(r=>`<tr>${r.map(x=>`<td>${esc(asTextC(x))}</td>`).join("")}</tr>`).join("")}</tbody></table>`); } }
(c.bullets||c.points||[]).forEach(x=> blocks.push(`<li>${esc(asTextC(x))}</li>`));
return `<div class="doc-paper"><h1 class="doc-title">${esc(cmd.title||"Document")}</h1>${blocks.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=()=>downloadDoc({...cmd, format:fmt}, b); bar.appendChild(b);
});
scope.appendChild(bar);
}
async function downloadDoc(cmd, btn){
const old=btn.textContent; btn.textContent="..."; btn.disabled=true;
try{
const r=await fetch(API+"/api/document",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(cmd)});
if(!r.ok) throw 0;
const blob=await r.blob(); const url=URL.createObjectURL(blob);
const a=document.createElement("a"); a.href=url; a.download=(cmd.title||"genisi")+"."+cmd.format; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
showToast(`${cmd.format.toUpperCase()} downloaded!`, "success");
}catch(e){ showToast("Download error", "error"); } finally{ btn.textContent=old; btn.disabled=false; }
}
function proxify(u){ if(!u) return u; if(u.startsWith("data:")) return u; return API+"/api/img?u="+encodeURIComponent(u); }
async function fetchImages(query){
const r=await fetch(API+"/api/image-search",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query})});
const j=await r.json(); return (j.images||[]).map(x=>x.image||x.thumbnail).filter(Boolean);
}
function loadImg(src){ return new Promise(res=>{ const im=new Image(); im.crossOrigin="anonymous"; im.onload=()=>res(im); im.onerror=()=>res(null); im.src=src; }); }
async function renderVideo(cmd, scope){
const status=document.createElement("p"); status.textContent=t("buildingVideo"); scope.appendChild(status);
const aspect = cmd.aspect||"16:9";
let cw=1280, ch=720;
if(aspect==="9:16"){ cw=720; ch=1280; } else if(aspect==="1:1"){ cw=1080; ch=1080; }
const scenes = Array.isArray(cmd.scenes) ? cmd.scenes : [];
for(const sc of scenes){
let src = sc.image||"";
if(src && !/^https?:|^data:/.test(src)){ const imgs=await fetchImages(src); src=imgs[0]||""; }
sc._img = src ? await loadImg(proxify(src)) : null;
sc.duration = Math.max(1.5, Math.min(8, sc.duration||3.5));
sc.animation = sc.animation||"kenburns";
}
status.remove();
if(!scenes.length){ const p=document.createElement("p"); p.textContent="No scenes."; scope.appendChild(p); return; }
const box=document.createElement("div"); box.className="video-box";
const stage=document.createElement("div"); stage.className="video-stage";
const canvas=document.createElement("canvas"); canvas.width=cw; canvas.height=ch;
stage.appendChild(canvas); box.appendChild(stage);
const ctrl=document.createElement("div"); ctrl.className="video-ctrl";
const playBtn=document.createElement("button");
playBtn.innerHTML=`<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg><span>${t("playVideo")}</span>`;
const expBtn=document.createElement("button");
expBtn.innerHTML=`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg><span>${t("exportVideo")}</span>`;
const vstat=document.createElement("span"); vstat.className="vstatus";
ctrl.appendChild(playBtn); ctrl.appendChild(expBtn); ctrl.appendChild(vstat);
box.appendChild(ctrl); scope.appendChild(box);
const ctx=canvas.getContext("2d");
const total = scenes.reduce((a,s)=>a+s.duration,0);
function easeInOut(x){ return x<0.5 ? 2*x*x : 1-Math.pow(-2*x+2,2)/2; }
function drawCover(img, zoom, panX, panY){
const ir=img.width/img.height, cr=cw/ch;
let dw,dh;
if(ir>cr){ dh=ch*zoom; dw=dh*ir; } else { dw=cw*zoom; dh=dw/ir; }
const dx=(cw-dw)/2 + panX, dy=(ch-dh)/2 + panY;
ctx.drawImage(img, dx, dy, dw, dh);
}
function wrapText(text, x, y, maxW, lh, font, color, align){
ctx.font=font; ctx.fillStyle=color; ctx.textAlign=align||"center"; ctx.textBaseline="alphabetic";
const words=(text||"").split(/\s+/); let line="", lines=[];
for(const w of words){ const test=line?line+" "+w:w; if(ctx.measureText(test).width>maxW && line){ lines.push(line); line=w; } else line=test; }
if(line) lines.push(line);
const startY = y - (lines.length-1)*lh/2;
lines.forEach((ln,i)=> ctx.fillText(ln, x, startY+i*lh));
return lines.length;
}
function drawFrame(time){
let acc=0, sc=scenes[0], local=0, idx=0;
for(let i=0;i<scenes.length;i++){ const s=scenes[i]; if(time < acc+s.duration){ sc=s; local=time-acc; idx=i; break; } acc+=s.duration; }
const p = sc ? Math.min(1, local/sc.duration) : 0;
ctx.fillStyle="#0b0b0f"; ctx.fillRect(0,0,cw,ch);
if(sc && sc._img){
let zoom=1.06, panX=0, panY=0;
if(sc.animation==="kenburns"){ zoom=1.06 + 0.10*p; panX=-(cw*0.04)*p; }
else if(sc.animation==="slide"){ zoom=1.08; panX=(cw*0.06)*(0.5-p); }
ctx.save(); ctx.globalAlpha=1; drawCover(sc._img, zoom, panX, panY); ctx.restore();
} else {
const g=ctx.createLinearGradient(0,0,cw,ch); g.addColorStop(0,"#7c3aed"); g.addColorStop(1,"#4c1d95");
ctx.fillStyle=g; ctx.fillRect(0,0,cw,ch);
}
const grad=ctx.createLinearGradient(0,ch*0.45,0,ch); grad.addColorStop(0,"rgba(0,0,0,0)"); grad.addColorStop(1,"rgba(0,0,0,0.72)");
ctx.fillStyle=grad; ctx.fillRect(0,ch*0.45,cw,ch*0.55);
const fadeIn=Math.min(1, p/0.18), fadeOut=Math.min(1,(1-p)/0.18);
const a=easeInOut(Math.min(fadeIn,fadeOut));
const slideUp=(1-easeInOut(fadeIn))*40;
if(sc){
ctx.globalAlpha=a;
if(sc.heading){ const fs=Math.round(cw*0.055); wrapText(sc.heading, cw/2, ch*0.80 - slideUp, cw*0.86, fs*1.2, `700 ${fs}px Inter, Cairo, sans-serif`, "#ffffff"); }
if(sc.subtitle){ const fs2=Math.round(cw*0.032); wrapText(sc.subtitle, cw/2, ch*0.90 - slideUp, cw*0.82, fs2*1.3, `500 ${fs2}px Inter, Cairo, sans-serif`, "#e9d5ff"); }
ctx.globalAlpha=1;
}
ctx.fillStyle="rgba(255,255,255,0.18)"; ctx.fillRect(0,ch-6,cw,6);
ctx.fillStyle="#a855f7"; ctx.fillRect(0,ch-6,cw*(time/total),6);
ctx.globalAlpha=0.85; ctx.font=`600 ${Math.round(cw*0.022)}px Inter, sans-serif`; ctx.fillStyle="#ffffff"; ctx.textAlign="left";
ctx.fillText("Genisi", cw*0.03, ch*0.07); ctx.globalAlpha=1;
}
let previewRAF=null;
function preview(){
const start=performance.now();
cancelAnimationFrame(previewRAF);
(function loop(now){
const tsec=(now-start)/1000;
if(tsec>=total){ drawFrame(total-0.001); return; }
drawFrame(tsec); previewRAF=requestAnimationFrame(loop);
})(start);
}
drawFrame(0);
playBtn.onclick=preview;
expBtn.onclick=async ()=>{
expBtn.disabled=true; playBtn.disabled=true; vstat.textContent=t("recording");
const fps=30;
const stream=canvas.captureStream(fps);
const mime = MediaRecorder.isTypeSupported("video/mp4") ? "video/mp4" : MediaRecorder.isTypeSupported("video/webm;codecs=vp9") ? "video/webm;codecs=vp9" : "video/webm";
const chunks=[]; const rec=new MediaRecorder(stream,{ mimeType:mime, videoBitsPerSecond:6_000_000 });
rec.ondataavailable=e=>{ if(e.data.size) chunks.push(e.data); };
const done=new Promise(r=> rec.onstop=r);
rec.start();
const start=performance.now();
await new Promise(resolve=>{
(function loop(now){
const tsec=(now-start)/1000;
if(tsec>=total){ drawFrame(total-0.001); resolve(); return; }
drawFrame(tsec); requestAnimationFrame(loop);
})(start);
});
await new Promise(r=>setTimeout(r,120));
rec.stop(); await done;
const ext = mime.startsWith("video/mp4")?"mp4":"webm";
const blob=new Blob(chunks,{type:mime}); const url=URL.createObjectURL(blob);
const a=document.createElement("a"); a.href=url; a.download=(cmd.title||"genisi-video")+"."+ext;
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
vstat.textContent=t("videoReady"); expBtn.disabled=false; playBtn.disabled=false;
showToast("Video exported!", "success");
};
}
function maybeTitle(s){ const u=s.messages.filter(m=>m.role==="user").length; if(u!==1&&u!==3) return; fetch(API+"/api/title",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({messages:s.messages})}).then(r=>r.json()).then(j=>{ if(j.title){ s.title=j.title; save(); buildRecents(); } }).catch(()=>{}); }
function learnInterests(text){ const words=(text||"").toLowerCase().match(/[\u0600-\u06FF\w]{4,}/g)||[]; const stop=new Set(["this","that","with","please","about","comment","pourquoi","هذا","هذه","التي","الذي","كيف"]); words.filter(w=>!stop.has(w)).slice(0,3).forEach(w=>{ if(!state.interests.includes(w)) state.interests.push(w); }); state.interests=state.interests.slice(-30); save(); }
function stripForTTS(x){ return x.replace(/```[\s\S]*?```/g,"").replace(/\{[\s\S]*?\}/g,"").replace(/SUGGESTIONS:.*$/,""); }
async function speak(text){ if(!text.trim()) return; try{ const r=await fetch(API+"/api/tts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text})}); if(!r.ok) throw 0; const blob=await r.blob(); new Audio(URL.createObjectURL(blob)).play(); }catch(e){ if(window.speechSynthesis) speechSynthesis.speak(new SpeechSynthesisUtterance(text)); } }
let recog=null;
function toggleMic(taId,btn){
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;
if(!SR){ showToast(t("micUnsupported"), "error"); 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(); } }
$("#ttsAuto")?.addEventListener("change", e=>{ state.settings.ttsAuto=e.target.checked; save(); });
function showToast(msg, type="success"){
const toast = $("#toast");
toast.textContent = msg;
toast.className = `toast ${type} show`;
setTimeout(()=> toast.classList.remove("show"), 3000);
}
init();
</script>
</body>
</html>