| <!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</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); |
| } |
| 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-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;} |
| |
| .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;} |
| svg{user-select:none;} |
| button svg{pointer-events:none;} |
| </style> |
| </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> |
| <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="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()"></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> |
| <script> |
| const API = ""; |
| 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:"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", |
| 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", |
| 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"); $("#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").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); |
| const caps=CAPS[m]||CAPS.flash; |
| pendingAttachments = pendingAttachments.filter(a => a.kind==="image" ? caps.vision : true); |
| renderAttachments(); |
| 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){ alert(j.message||t("unsupportedFile")); 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){ alert(t("unsupportedFile")); } |
| } |
| 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=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[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>`; |
| } else n.textContent=(j.message||""); |
| }catch(e){ n.textContent="error"; } |
| } else if(cmd.tool==="search"){ |
| const n=document.createElement("p"); n.textContent=t("searching"); 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><br><small>${escapeHtml(x.snippet)}</small>`).join("<br><br>"); }catch(e){ n.textContent="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); |
| } else if(cmd.tool==="video"){ |
| await renderVideo(cmd, scope); |
| } |
| } |
| |
| |
| 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); |
| }catch(e){ alert("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; |
| }; |
| } |
| 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){ 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(); } } |
| $("#ttsAuto")?.addEventListener("change", e=>{ state.settings.ttsAuto=e.target.checked; save(); }); |
| |
| init(); |
| </script> |
| </body> |
| </html> |
|
|