Spaces:
Sleeping
Sleeping
| <html lang="en" dir="ltr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Genisi AI</title> | |
| <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700;800&family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/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; | |
| --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; | |
| } | |
| 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);} | |
| .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;} | |
| .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:6px;height:6px;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;}} | |
| .worm-square-wrap{display:flex;align-items:center;padding:8px 4px;} | |
| .worm-square{position:relative;width:32px;height:22px;} | |
| .worm-square span{position:absolute;width:5.5px;height:5.5px;border-radius:50%;background:var(--purple);box-shadow:0 0 6px rgba(124,58,237,0.5);transition:background 0.3s,box-shadow 0.3s;} | |
| html[data-theme="dark"] .worm-square span{box-shadow:0 0 8px rgba(196,124,255,0.6);} | |
| .worm-square span:nth-child(1){animation:wormDot1 2.8s infinite cubic-bezier(0.4,0,0.2,1);} | |
| .worm-square span:nth-child(2){animation:wormDot2 2.8s infinite cubic-bezier(0.4,0,0.2,1);} | |
| .worm-square span:nth-child(3){animation:wormDot3 2.8s infinite cubic-bezier(0.4,0,0.2,1);} | |
| .worm-square span:nth-child(4){animation:wormDot4 2.8s infinite cubic-bezier(0.4,0,0.2,1);} | |
| @keyframes wormDot1{0%{left:25px;top:8px;transform:scale(1);}12%{left:24px;top:2px;transform:scale(1.15);}24%{left:22px;top:8px;transform:scale(1);}36%{left:20px;top:8px;transform:scale(1);}48%{left:18px;top:3px;transform:scale(1);}66%{left:18px;top:13px;transform:scale(1.25);background:#a855f7;box-shadow:0 0 12px #a855f7;}84%{left:22px;top:10px;transform:scale(1.05);}100%{left:25px;top:8px;transform:scale(1);}} | |
| @keyframes wormDot2{0%{left:17px;top:8px;transform:scale(1);}12%{left:16px;top:8px;transform:scale(1);}24%{left:15px;top:2px;transform:scale(1.15);}36%{left:13px;top:8px;transform:scale(1);}48%{left:8px;top:3px;transform:scale(1);}66%{left:18px;top:3px;transform:scale(1.25);background:#a855f7;box-shadow:0 0 12px #a855f7;}84%{left:18px;top:6px;transform:scale(1.05);}100%{left:17px;top:8px;transform:scale(1);}} | |
| @keyframes wormDot3{0%{left:9px;top:8px;transform:scale(1);}12%{left:9px;top:8px;transform:scale(1);}24%{left:9px;top:8px;transform:scale(1);}36%{left:9px;top:2px;transform:scale(1.15);}48%{left:8px;top:13px;transform:scale(1);}66%{left:8px;top:3px;transform:scale(1.25);background:#a855f7;box-shadow:0 0 12px #a855f7;}84%{left:8px;top:6px;transform:scale(1.05);}100%{left:9px;top:8px;transform:scale(1);}} | |
| @keyframes wormDot4{0%{left:1px;top:8px;transform:scale(1);}12%{left:2px;top:8px;transform:scale(1);}24%{left:3px;top:8px;transform:scale(1);}36%{left:4px;top:8px;transform:scale(1);}48%{left:18px;top:13px;transform:scale(1);}66%{left:8px;top:13px;transform:scale(1.25);background:#a855f7;box-shadow:0 0 12px #a855f7;}84%{left:4px;top:10px;transform:scale(1.05);}100%{left:1px;top:8px;transform:scale(1);}} | |
| .pro-thinking-wrap{display:flex;align-items:center;padding:6px 0;} | |
| .pro-thinking{display:inline-flex;align-items:center;gap:10px;padding:8px 18px;background:linear-gradient(135deg,rgba(124,58,237,0.1),rgba(168,85,247,0.05));border:1px solid rgba(124,58,237,0.3);border-radius:24px;animation:proBoxPulse 2.2s infinite ease-in-out;box-shadow:0 0 15px rgba(124,58,237,0.15);} | |
| html[data-theme="dark"] .pro-thinking{background:linear-gradient(135deg,rgba(168,85,247,0.15),rgba(124,58,237,0.08));border-color:rgba(196,124,255,0.35);} | |
| .pro-glow-icon{color:#a855f7;display:flex;align-items:center;justify-content:center;animation:proIconGlow 2.2s infinite ease-in-out;} | |
| .pro-glow-text{font-size:14.5px;font-weight:700;background:linear-gradient(90deg,#7c3aed,#c084fc,#7c3aed);background-size:200% auto;-webkit-background-clip:text;-webkit-text-fill-color:transparent;animation:proTextShimmer 2.2s infinite linear,proTextGlow 2.2s infinite ease-in-out;letter-spacing:0.4px;} | |
| @keyframes proBoxPulse{0%,100%{box-shadow:0 0 12px rgba(124,58,237,0.15),inset 0 0 8px rgba(124,58,237,0.05);transform:scale(0.99);border-color:rgba(124,58,237,0.25);}50%{box-shadow:0 0 25px rgba(168,85,247,0.45),0 0 45px rgba(124,58,237,0.25),inset 0 0 15px rgba(168,85,247,0.15);transform:scale(1.02);border-color:rgba(196,124,255,0.7);}} | |
| @keyframes proTextGlow{0%,100%{filter:drop-shadow(0 0 4px rgba(124,58,237,0.4));}50%{filter:drop-shadow(0 0 12px rgba(196,124,255,0.9)) drop-shadow(0 0 20px rgba(168,85,247,0.6));}} | |
| @keyframes proTextShimmer{to{background-position:200% center;}} | |
| @keyframes proIconGlow{0%,100%{transform:scale(0.95) rotate(0deg);filter:drop-shadow(0 0 4px rgba(168,85,247,0.4));}50%{transform:scale(1.15) rotate(180deg);filter:drop-shadow(0 0 14px rgba(196,124,255,0.9));}} | |
| .retry-banner{display:flex;align-items:center;gap:10px;padding:10px 16px;margin:8px 0;background:linear-gradient(135deg,rgba(245,158,11,0.1),rgba(245,158,11,0.05));border:1px solid rgba(245,158,11,0.3);border-radius:14px;font-size:13px;color:#b45309;animation:retryPulse 2s infinite ease-in-out;} | |
| html[data-theme="dark"] .retry-banner{color:#fbbf24;border-color:rgba(245,158,11,0.4);background:linear-gradient(135deg,rgba(245,158,11,0.15),rgba(245,158,11,0.05));} | |
| .retry-banner svg{flex-shrink:0;animation:spin 2s linear infinite;} | |
| @keyframes spin{to{transform:rotate(360deg);}} | |
| @keyframes retryPulse{0%,100%{opacity:0.85;}50%{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 video{width:100%;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" id="btnCloseSidebar"> | |
| <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" id="btnNewChatSB"> | |
| <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" id="btnOpenSearchSB"> | |
| <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" id="btnOpenSettings"> | |
| <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.4 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" id="btnOpenHowto"> | |
| <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" id="btnOpenSidebarTop"> | |
| <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" id="btnNewChatTop"> | |
| <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" id="btnOpenSearchTop"> | |
| <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" id="homeUpload"> | |
| <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"></textarea> | |
| <div class="input-right"> | |
| <div style="position:relative"> | |
| <button class="model-btn" id="modelBtnHome"> | |
| <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" data-model="flash"><b>Flash</b><small data-i18n="flashDesc">Fast & smart</small></div> | |
| <div class="mm-item" data-model="pro"><b>Pro</b><small data-i18n="proDesc">More accurate</small></div> | |
| </div> | |
| </div> | |
| <button class="mic-btn" id="homeMic"> | |
| <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" 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" id="chatUpload"> | |
| <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"></textarea> | |
| <div class="input-right"> | |
| <div style="position:relative"> | |
| <button class="model-btn" id="modelBtnChat"> | |
| <span class="model-label">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" data-model="flash"><b>Flash</b><small data-i18n="flashDesc">Fast & smart</small></div> | |
| <div class="mm-item" data-model="pro"><b>Pro</b><small data-i18n="proDesc">More accurate</small></div> | |
| </div> | |
| </div> | |
| <button class="mic-btn" id="chatMic"> | |
| <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" 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" autocomplete="off"> | |
| <button class="sp-x" id="btnCloseSearch"> | |
| <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" id="btnCloseSettings"> | |
| <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"><span class="option-text" data-i18n="light">Light</span></label> | |
| <label class="settings-option"><input type="radio" name="theme" value="dark"><span class="option-text" data-i18n="dark">Dark</span></label> | |
| <label class="settings-option"><input type="radio" name="theme" value="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"><span class="option-text">English</span></label> | |
| <label class="settings-option"><input type="radio" name="lang" value="ar"><span class="option-text">العربية</span></label> | |
| <label class="settings-option"><input type="radio" name="lang" value="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"><span class="option-text">Flash</span></label> | |
| <label class="settings-option"><input type="radio" name="dmodel" value="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" id="btnHowtoFromSettings" data-i18n="howtoBtn">How to use Genisi?</button></div> | |
| <div class="settings-group"><button class="btn-purple" style="background:#fde8e8;color:#d33" id="btnClearAll" data-i18n="clearBtn">Clear all chats and data</button></div> | |
| </div> | |
| </div> | |
| <div class="overlay" id="overlay"></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" data-i18n="male">Male</div> | |
| <div class="gender-opt" data-g="Female" data-i18n="female">Female</div> | |
| <div class="gender-opt" data-g="" data-i18n="noSay">Prefer not to say</div> | |
| </div> | |
| </div> | |
| <button class="modal-btn" id="btnConfirmOnboard" 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" id="btnCloseHowto" data-i18n="getStarted">Get started</button> | |
| </div> | |
| </div> | |
| <script> | |
| // ===================== Safe JSON parse helper ===================== | |
| function safeJSON(response) { | |
| return response.text().then(function(txt) { | |
| if (!txt || !txt.trim()) return {}; | |
| try { return JSON.parse(txt); } catch(e) { return {}; } | |
| }); | |
| } | |
| // ===================== localStorage helper (safe for tracking prevention) ===================== | |
| var LS = { | |
| get: function(k, def) { | |
| try { var v = localStorage.getItem(k); return v !== null ? JSON.parse(v) : def; } | |
| catch(e) { return def; } | |
| }, | |
| set: function(k, v) { | |
| try { localStorage.setItem(k, JSON.stringify(v)); } catch(e) {} | |
| }, | |
| clear: function() { | |
| try { localStorage.clear(); } catch(e) {} | |
| } | |
| }; | |
| // ===================== i18n ===================== | |
| var 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:"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:"Download MP4", | |
| buildingVideo:"Generating video (this can take a while)...", | |
| recording:"Recording...",videoReady:"Video ready", | |
| retrying:"Retrying in",retryAttempt:"Attempt",retrySeconds:"seconds", | |
| 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</h3><ul><li>Ask for a short AI-generated video, then download it as 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:"الفيديو جاهز", | |
| retrying:"إعادة المحاولة خلال",retryAttempt:"المحاولة",retrySeconds:"ثانية", | |
| 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:"restant(s)", | |
| 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:"Télécharger MP4", | |
| buildingVideo:"Génération de la vidéo (peut prendre du temps)...", | |
| recording:"Enregistrement...",videoReady:"Vidéo prête", | |
| retrying:"Nouvelle tentative dans",retryAttempt:"Tentative",retrySeconds:"secondes", | |
| 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>Vidéo</h3><ul><li>Demandez une courte vidéo générée par IA et téléchargez-la en MP4.</li></ul><h3>Fichiers & Voix</h3><ul><li>Téléversez des fichiers, dictée vocale, lecture des réponses.</li></ul>' | |
| } | |
| }; | |
| function t(k) { return (I18N[state.settings.lang] || I18N.en)[k] || I18N.en[k] || k; } | |
| var 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) { | |
| var c = CAPS[model] || CAPS.flash; | |
| return [].concat(c.vision ? c.imageMimes : [], c.docExts.map(function(e){return "."+e;})).join(","); | |
| } | |
| // ===================== State ===================== | |
| var state = { | |
| user: LS.get("genisi_user", null), | |
| settings: LS.get("genisi_settings", {theme:"light", lang:"en", model:"flash", ttsAuto:false}), | |
| sessions: LS.get("genisi_sessions", []), | |
| interests: LS.get("genisi_interests", []), | |
| currentId: null, | |
| busy: false, | |
| abort: null | |
| }; | |
| var clientId = LS.get("genisi_client", null) || (Date.now() + "-" + Math.random().toString(36).slice(2)); | |
| LS.set("genisi_client", clientId); | |
| var pendingAttachments = []; | |
| function save() { | |
| LS.set("genisi_user", state.user); | |
| LS.set("genisi_settings", state.settings); | |
| LS.set("genisi_sessions", state.sessions); | |
| LS.set("genisi_interests", state.interests); | |
| } | |
| function $(s) { return document.querySelector(s); } | |
| function cur() { return state.sessions.find(function(s){return s.id === state.currentId;}); } | |
| // ===================== Init ===================== | |
| function init() { | |
| applyTheme(); | |
| applyLang(); | |
| document.querySelectorAll('input[name="theme"]').forEach(function(r){r.checked = r.value === state.settings.theme;}); | |
| document.querySelectorAll('input[name="lang"]').forEach(function(r){r.checked = r.value === state.settings.lang;}); | |
| document.querySelectorAll('input[name="dmodel"]').forEach(function(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"); } | |
| if (window.marked) marked.setOptions({breaks:true, gfm:true}); | |
| bindEvents(); | |
| } | |
| // ===================== Event binding ===================== | |
| function bindEvents() { | |
| $("#btnCloseSidebar").onclick = closeSidebar; | |
| $("#btnOpenSidebarTop").onclick = openSidebar; | |
| $("#btnNewChatSB").onclick = newChat; | |
| $("#btnNewChatTop").onclick = newChat; | |
| $("#btnOpenSearchSB").onclick = openSearch; | |
| $("#btnOpenSearchTop").onclick = openSearch; | |
| $("#btnOpenSettings").onclick = openSettings; | |
| $("#btnOpenHowto").onclick = openHowto; | |
| $("#btnCloseSettings").onclick = closeSettings; | |
| $("#overlay").onclick = closeSettings; | |
| $("#btnHowtoFromSettings").onclick = openHowto; | |
| $("#btnClearAll").onclick = clearAll; | |
| $("#btnCloseHowto").onclick = closeHowto; | |
| $("#btnCloseSearch").onclick = closeSearch; | |
| $("#btnConfirmOnboard").onclick = confirmOnboard; | |
| $("#ttsAuto").onchange = function(e) { state.settings.ttsAuto = e.target.checked; save(); }; | |
| document.querySelectorAll('input[name="theme"]').forEach(function(r){r.onchange = function(){setTheme(r.value);};}); | |
| document.querySelectorAll('input[name="lang"]').forEach(function(r){r.onchange = function(){setLang(r.value);};}); | |
| document.querySelectorAll('input[name="dmodel"]').forEach(function(r){r.onchange = function(){setModel(r.value);};}); | |
| $("#modelBtnHome").onclick = function(e){toggleModelMenu(e,"modelMenuHome");}; | |
| $("#modelBtnChat").onclick = function(e){toggleModelMenu(e,"modelMenuChat");}; | |
| document.querySelectorAll(".mm-item").forEach(function(el){el.onclick = function(){setModel(el.dataset.model);};}); | |
| document.addEventListener("click", function(){document.querySelectorAll(".model-menu").forEach(function(m){m.classList.remove("open");});}); | |
| $("#homeTA").oninput = function(){onTaInput($("#homeTA"),"homeSend");}; | |
| $("#homeTA").onkeydown = function(e){onTaKey(e,"home");}; | |
| $("#chatTA").oninput = function(){onTaInput($("#chatTA"),"chatSend");}; | |
| $("#chatTA").onkeydown = function(e){onTaKey(e,"chat");}; | |
| $("#homeSend").onclick = function(){send("home");}; | |
| $("#chatSend").onclick = function(){send("chat");}; | |
| $("#homeUpload").onclick = function(){uploadFiles();}; | |
| $("#chatUpload").onclick = function(){uploadFiles();}; | |
| $("#homeMic").onclick = function(){toggleMic("homeTA",$("#homeMic"));}; | |
| $("#chatMic").onclick = function(){toggleMic("chatTA",$("#chatMic"));}; | |
| $("#spInput").oninput = function(){filterSP($("#spInput").value);}; | |
| document.querySelectorAll(".gender-opt").forEach(function(el){el.onclick = function(){pickGender(el);};}); | |
| $("#recentWrap").addEventListener("click", function(e) { | |
| var item = e.target.closest(".recent-item"); | |
| if (!item) return; | |
| var delBtn = e.target.closest(".ri-more"); | |
| if (delBtn) { e.stopPropagation(); deleteSession(item.dataset.id); return; } | |
| loadSession(item.dataset.id); | |
| }); | |
| $("#spResults").addEventListener("click", function(e) { | |
| var row = e.target.closest(".sp-row"); | |
| if (row) { loadSession(row.dataset.id); closeSearch(); } | |
| }); | |
| } | |
| // ===================== Theme / Lang ===================== | |
| function applyLang() { | |
| var l = state.settings.lang, rtl = l === "ar"; | |
| document.documentElement.lang = l; | |
| document.documentElement.dir = rtl ? "rtl" : "ltr"; | |
| document.querySelectorAll("[data-i18n]").forEach(function(el) { | |
| var k = el.getAttribute("data-i18n"), v = t(k); | |
| (k === "intro" || k === "howto") ? (el.innerHTML = v) : (el.textContent = v); | |
| }); | |
| document.querySelectorAll("[data-ph]").forEach(function(el){el.placeholder = t(el.getAttribute("data-ph"));}); | |
| if ($("#howtoIntro")) $("#howtoIntro").innerHTML = t("intro"); | |
| if ($("#howtoBody")) $("#howtoBody").innerHTML = t("howto"); | |
| if ($("#greeting")) $("#greeting").textContent = state.user ? greetText() : t("askGenisi"); | |
| } | |
| function setLang(l) { state.settings.lang = l; save(); applyLang(); } | |
| function setTheme(th) { state.settings.theme = th; save(); applyTheme(); } | |
| function applyTheme() { | |
| var th = state.settings.theme; | |
| if (th === "auto") th = matchMedia("(prefers-color-scheme:dark)").matches ? "dark" : "light"; | |
| document.documentElement.setAttribute("data-theme", th); | |
| } | |
| // ===================== Onboarding ===================== | |
| var pickedGender = null; | |
| function pickGender(el) { | |
| document.querySelectorAll(".gender-opt").forEach(function(o){o.classList.remove("sel");}); | |
| el.classList.add("sel"); | |
| pickedGender = el.dataset.g; | |
| } | |
| function confirmOnboard() { | |
| var name = $("#obName").value.trim(); | |
| if (!name) { $("#obName").focus(); return; } | |
| state.user = {name:name, dob:$("#obDob").value || "", gender:pickedGender || ""}; | |
| save(); refreshUserUI(); | |
| $("#welcomeModal").classList.remove("open"); | |
| $("#howtoModal").classList.add("open"); | |
| } | |
| function refreshUserUI() { | |
| if (!state.user) return; | |
| $("#userName").textContent = state.user.name; | |
| $("#userAvatar").textContent = (state.user.name[0] || "G").toUpperCase(); | |
| if ($("#greeting")) $("#greeting").textContent = greetText(); | |
| } | |
| function greetText() { | |
| var n = (state.user && state.user.name) || "", 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 : "") + "?"; | |
| } | |
| // ===================== Sidebar / Settings / Search ===================== | |
| 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 openHowto() { $("#howtoModal").classList.add("open"); } | |
| function closeHowto() { $("#howtoModal").classList.remove("open"); } | |
| function openSearch() { $("#searchPane").classList.add("open"); buildSP(); setTimeout(function(){$("#spInput").focus();},50); } | |
| function closeSearch() { $("#searchPane").classList.remove("open"); $("#spInput").value = ""; } | |
| // ===================== Model ===================== | |
| function toggleModelMenu(e, id) { | |
| e.stopPropagation(); | |
| document.querySelectorAll(".model-menu").forEach(function(m){if(m.id !== id) m.classList.remove("open");}); | |
| $("#" + id).classList.toggle("open"); | |
| } | |
| function setModel(m, silent) { | |
| state.settings.model = m; save(); | |
| var label = m === "flash" ? "Flash" : "Pro"; | |
| document.querySelectorAll(".modelLabel, .model-label").forEach(function(e){e.textContent = label;}); | |
| document.querySelectorAll('input[name="dmodel"]').forEach(function(r){r.checked = r.value === m;}); | |
| var caps = CAPS[m] || CAPS.flash; | |
| pendingAttachments = pendingAttachments.filter(function(a){return a.kind === "image" ? caps.vision : true;}); | |
| renderAttachments(); | |
| if (!silent) document.querySelectorAll(".model-menu").forEach(function(mm){mm.classList.remove("open");}); | |
| } | |
| // ===================== Sessions ===================== | |
| function newChat() { | |
| state.currentId = null; | |
| $("#shell").classList.remove("chat"); | |
| $("#msgsInner").innerHTML = ""; | |
| $("#homeTA").value = ""; $("#homeSend").disabled = true; | |
| clearAttachments(); | |
| if ($("#greeting")) $("#greeting").textContent = greetText(); | |
| buildRecents(); closeSearch(); | |
| } | |
| function buildRecents() { | |
| $("#recentWrap").innerHTML = state.sessions.map(function(s) { | |
| var active = s.id === state.currentId ? " active" : ""; | |
| var titleTxt = escapeHtml(s.title || t("newConvo")); | |
| return '<div class="recent-item' + active + '" data-id="' + escapeHtml(s.id) + '">' + | |
| '<span class="ri-text">' + titleTxt + '</span>' + | |
| '<span class="ri-more" title="Delete">' + | |
| '<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(function(s) { | |
| var key = escapeHtml((s.title || "").toLowerCase()); | |
| var titleTxt = escapeHtml(s.title || t("newConvo")); | |
| return '<div class="sp-row" data-id="' + escapeHtml(s.id) + '" data-t="' + key + '">' + | |
| '<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">' + titleTxt + '</span>' + | |
| '<span class="sr-date">' + escapeHtml(s.date || "") + '</span></div>'; | |
| }).join(""); | |
| } | |
| function filterSP(q) { | |
| q = q.toLowerCase(); | |
| document.querySelectorAll("#spResults .sp-row").forEach(function(r){ | |
| r.classList.toggle("hide", q && r.dataset.t.indexOf(q) === -1); | |
| }); | |
| } | |
| function deleteSession(id) { | |
| state.sessions = state.sessions.filter(function(s){return s.id !== id;}); | |
| if (state.currentId === id) newChat(); | |
| save(); buildRecents(); | |
| } | |
| function loadSession(id) { | |
| state.currentId = id; | |
| var s = cur(); if (!s) return; | |
| $("#shell").classList.add("chat"); | |
| $("#msgsInner").innerHTML = ""; | |
| s.messages.forEach(function(m){ | |
| if (m.role === "user") addUser(m.content, m.attachments); | |
| else addAI(m.content, false); | |
| }); | |
| buildRecents(); renderMathAll(); scrollDown(); | |
| } | |
| // ===================== Textarea helpers ===================== | |
| 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); } | |
| } | |
| // ===================== File upload ===================== | |
| function uploadFiles() { | |
| var model = state.settings.model; | |
| var input = document.createElement("input"); | |
| input.type = "file"; input.multiple = true; input.accept = acceptFor(model); | |
| input.onchange = function(e) { | |
| Array.from(e.target.files).forEach(function(f){doUpload(f, model);}); | |
| }; | |
| input.click(); | |
| } | |
| function doUpload(file, model) { | |
| var fd = new FormData(); | |
| fd.append("file", file); | |
| fd.append("model", model); | |
| fetch("/api/upload", {method:"POST", body:fd}) | |
| .then(function(r){return safeJSON(r);}) | |
| .then(function(j) { | |
| if (j.message && !j.kind) { alert(j.message); 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}); | |
| renderAttachments(); | |
| }) | |
| .catch(function(){alert(t("unsupportedFile"));}); | |
| } | |
| function attChipHtml(a, idx) { | |
| var 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>'; | |
| var xBtn = idx !== undefined ? '<button class="att-x" data-idx="' + idx + '">×</button>' : ""; | |
| return '<div class="att-chip">' + icon + '<span class="att-name">' + escapeHtml(a.name) + '</span>' + xBtn + '</div>'; | |
| } | |
| function renderAttachments() { | |
| [$("#homeAtts"), $("#chatAtts")].forEach(function(w) { | |
| if (!w) return; | |
| w.innerHTML = pendingAttachments.map(function(a,i){return attChipHtml(a,i);}).join(""); | |
| w.querySelectorAll(".att-x").forEach(function(btn) { | |
| btn.onclick = function(){pendingAttachments.splice(+btn.dataset.idx, 1); renderAttachments();}; | |
| }); | |
| }); | |
| } | |
| function clearAttachments() { pendingAttachments = []; renderAttachments(); } | |
| // ===================== Utilities ===================== | |
| function escapeHtml(s) { | |
| return (s || "").replace(/[&<>"']/g, function(c) { | |
| return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c]; | |
| }); | |
| } | |
| function addUser(text, atts) { | |
| var el = document.createElement("div"); | |
| el.className = "msg-user"; | |
| if (atts && atts.length) { | |
| var box = document.createElement("div"); | |
| box.className = "attachments"; box.style.padding = "0 0 8px"; | |
| box.innerHTML = atts.map(function(a){return attChipHtml(a);}).join(""); | |
| el.appendChild(box); | |
| } | |
| var tx = document.createElement("div"); | |
| tx.textContent = text; | |
| el.appendChild(tx); | |
| $("#msgsInner").appendChild(el); | |
| scrollDown(); | |
| return el; | |
| } | |
| function addAI(md) { | |
| var wrap = document.createElement("div"); | |
| wrap.className = "msg-ai"; | |
| var body = document.createElement("div"); | |
| body.className = "msg-ai-body"; | |
| wrap.appendChild(body); | |
| $("#msgsInner").appendChild(wrap); | |
| if (md !== undefined) renderInto(body, md); | |
| scrollDown(); | |
| return {wrap:wrap, body:body}; | |
| } | |
| function renderFormatted(text, autoClose) { | |
| var cleaned = cleanForRender(text || ""); | |
| if (autoClose) { | |
| var codeMatches = cleaned.match(/```/g) || []; | |
| if (codeMatches.length % 2 !== 0) { if (!cleaned.endsWith("\n")) cleaned += "\n"; cleaned += "```"; } | |
| var mathMatches = cleaned.match(/\$\$/g) || []; | |
| if (mathMatches.length % 2 !== 0) cleaned += " $$"; | |
| var openBracket = (cleaned.match(/\\\[/g) || []).length; | |
| var closeBracket = (cleaned.match(/\\\]/g) || []).length; | |
| if (openBracket > closeBracket) cleaned += " \\]"; | |
| } | |
| return window.marked ? marked.parse(cleaned) : escapeHtml(cleaned); | |
| } | |
| function renderInto(body, md) { | |
| body.innerHTML = renderFormatted(md, true); | |
| enhanceCode(body); | |
| renderMath(body); | |
| } | |
| function enhanceCode(scope) { | |
| scope.querySelectorAll("pre code").forEach(function(code) { | |
| if (code.dataset.done) return; | |
| code.dataset.done = "1"; | |
| var pre = code.parentElement; | |
| var langMatch = code.className.match(/language-(\w+)/); | |
| var lang = langMatch ? langMatch[1] : "code"; | |
| var wrap = document.createElement("div"); | |
| wrap.className = "code-wrap"; | |
| var bar = document.createElement("div"); | |
| bar.className = "code-bar"; | |
| bar.innerHTML = "<span>" + lang + "</span>"; | |
| var btn = document.createElement("button"); | |
| btn.className = "copy-btn"; btn.textContent = "Copy"; | |
| btn.onclick = function() { | |
| navigator.clipboard.writeText(code.innerText); | |
| btn.textContent = "Copied"; | |
| setTimeout(function(){btn.textContent = "Copy";}, 1500); | |
| }; | |
| bar.appendChild(btn); | |
| pre.parentNode.insertBefore(wrap, pre); | |
| wrap.appendChild(bar); | |
| wrap.appendChild(pre); | |
| try { if (window.hljs) 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} | |
| ], | |
| throwOnError: false, errorColor: "var(--purple)" | |
| }); | |
| } catch(e) {} | |
| } | |
| } | |
| function renderMathAll() { document.querySelectorAll(".msg-ai-body").forEach(renderMath); } | |
| function scrollDown() { var m = $("#msgs"); m.scrollTop = m.scrollHeight; } | |
| // ===================== Send buttons state ===================== | |
| function setSendButtons(stop) { | |
| [["#homeSend","home"],["#chatSend","chat"]].forEach(function(pair) { | |
| var btn = $(pair[0]); if (!btn) return; | |
| if (stop) { | |
| btn.classList.add("stop"); btn.disabled = false; | |
| btn.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>'; | |
| btn.onclick = stopResponse; | |
| } else { | |
| btn.classList.remove("stop"); | |
| btn.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>'; | |
| btn.onclick = function(){send(pair[1]);}; | |
| } | |
| }); | |
| } | |
| function stopResponse() { if (state.abort) state.abort.abort(); } | |
| // ===================== Main send ===================== | |
| function send(mode) { | |
| var ta = mode === "home" ? $("#homeTA") : $("#chatTA"); | |
| var text = ta.value.trim(); | |
| if ((!text && !pendingAttachments.length) || state.busy) return; | |
| if (!state.currentId) { | |
| var id = Date.now() + ""; | |
| state.sessions.unshift({id:id, title:"", date:"now", messages:[]}); | |
| state.currentId = id; | |
| } | |
| var s = cur(); | |
| $("#shell").classList.add("chat"); | |
| if (mode === "home") { | |
| $("#msgsInner").innerHTML = ""; | |
| s.messages.forEach(function(m) { | |
| if (m.role === "user") addUser(m.content, m.attachments); | |
| else addAI(m.content, false); | |
| }); | |
| } | |
| ta.value = ""; ta.style.height = "auto"; | |
| var atts = pendingAttachments.slice(); | |
| addUser(text, atts); | |
| s.messages.push({role:"user", content:text, attachments:atts}); | |
| clearAttachments(); buildRecents(); | |
| var aiEl = addAI(undefined, true); | |
| var body = aiEl.body; | |
| var answer = document.createElement("div"); | |
| answer.className = "streaming"; | |
| body.appendChild(answer); | |
| // Loading animation | |
| if (state.settings.model === "pro") { | |
| answer.innerHTML = '<div class="pro-thinking-wrap"><div class="pro-thinking"><span class="pro-glow-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg></span><span class="pro-glow-text">' + escapeHtml(t("thinking")) + '</span></div></div>'; | |
| } else { | |
| answer.innerHTML = '<div class="worm-square-wrap"><div class="worm-square"><span></span><span></span><span></span><span></span></div></div>'; | |
| } | |
| state.busy = true; | |
| setSendButtons(true); | |
| state.abort = new AbortController(); | |
| var full = "", rafPending = false, retryBanner = null, retryCountdown = null; | |
| function paint() { | |
| rafPending = false; | |
| answer.classList.add("streaming"); | |
| answer.innerHTML = renderFormatted(full, true); | |
| enhanceCode(answer); | |
| renderMath(answer); | |
| scrollDown(); | |
| } | |
| function showRetryBanner(attempt, nextRetryIn, error) { | |
| if (retryBanner) retryBanner.remove(); | |
| retryBanner = document.createElement("div"); | |
| retryBanner.className = "retry-banner"; | |
| var secs = nextRetryIn || 300; | |
| retryBanner.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.22-8.56"/><polyline points="21 3 21 9 15 9"/></svg>' + | |
| '<span>' + t("retryAttempt") + ' #' + attempt + ' — ' + t("retrying") + ' <b id="retryCount">' + secs + '</b> ' + t("retrySeconds") + '</span>'; | |
| body.insertBefore(retryBanner, answer); | |
| var remaining = secs; | |
| var countEl = retryBanner.querySelector("#retryCount"); | |
| if (retryCountdown) clearInterval(retryCountdown); | |
| retryCountdown = setInterval(function() { | |
| remaining--; | |
| if (countEl) countEl.textContent = remaining; | |
| if (remaining <= 0) clearInterval(retryCountdown); | |
| }, 1000); | |
| scrollDown(); | |
| } | |
| function removeRetryBanner() { | |
| if (retryCountdown) { clearInterval(retryCountdown); retryCountdown = null; } | |
| if (retryBanner) { retryBanner.remove(); retryBanner = null; } | |
| } | |
| fetch("/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" | |
| }) | |
| }).then(function(res) { | |
| var reader = res.body.getReader(), dec = new TextDecoder(), buf = ""; | |
| function processChunk(result) { | |
| if (result.done) return finishStream(); | |
| buf += dec.decode(result.value, {stream:true}); | |
| var parts = buf.split("\n\n"); | |
| buf = parts.pop(); | |
| for (var i = 0; i < parts.length; i++) { | |
| var line = parts[i].replace(/^data: /, "").trim(); | |
| if (!line) continue; | |
| var j; | |
| try { j = JSON.parse(line); } catch(e) { continue; } | |
| // ===== Retry event from backend ===== | |
| if (j.retrying) { | |
| removeRetryBanner(); | |
| showRetryBanner(j.attempt, j.nextRetryIn, j.error); | |
| continue; | |
| } | |
| if (j.error) { | |
| removeRetryBanner(); | |
| answer.innerHTML = '<p style="color:#d33">' + escapeHtml(j.error) + '</p>'; | |
| continue; | |
| } | |
| if (j.done) continue; | |
| // Got real content — remove retry banner if any | |
| if (full.length === 0) removeRetryBanner(); | |
| full += j.delta || ""; | |
| if (!rafPending) { rafPending = true; requestAnimationFrame(paint); } | |
| } | |
| return reader.read().then(processChunk); | |
| } | |
| return reader.read().then(processChunk); | |
| }).then(function() { | |
| // handled in finishStream via the chain | |
| }).catch(function(err) { | |
| removeRetryBanner(); | |
| if (err.name === "AbortError") { | |
| var partial = full; | |
| answer.classList.remove("streaming"); | |
| answer.innerHTML = partial.trim() | |
| ? renderFormatted(partial, true) | |
| : '<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(function() { | |
| removeRetryBanner(); | |
| state.busy = false; | |
| state.abort = null; | |
| setSendButtons(false); | |
| $("#homeSend").disabled = true; | |
| $("#chatSend").disabled = true; | |
| }); | |
| function finishStream() { | |
| removeRetryBanner(); | |
| var finalAns = full; | |
| answer.classList.remove("streaming"); | |
| answer.innerHTML = renderFormatted(finalAns, true); | |
| enhanceCode(answer); | |
| renderMath(answer); | |
| handleTools(finalAns, answer); | |
| var 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(); | |
| } | |
| } | |
| // ===================== Tool extraction ===================== | |
| function extractToolJSON(text) { | |
| var key = text.indexOf('"tool"'); | |
| if (key === -1) return null; | |
| var start = text.lastIndexOf("{", key); | |
| while (start !== -1) { | |
| var depth = 0, inStr = false, esc = false; | |
| for (var i = start; i < text.length; i++) { | |
| var 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) { | |
| var out = text || ""; | |
| var 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) { | |
| var 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) { | |
| var box = document.createElement("div"); | |
| box.className = "suggests"; | |
| list.forEach(function(txt) { | |
| var b = document.createElement("button"); | |
| b.className = "suggest"; b.textContent = txt; | |
| b.onclick = function(){$("#chatTA").value = txt; send("chat");}; | |
| box.appendChild(b); | |
| }); | |
| scope.appendChild(box); | |
| } | |
| function downloadUrl(url, filename) { | |
| var a = document.createElement("a"); | |
| a.href = url; a.download = filename; | |
| document.body.appendChild(a); a.click(); a.remove(); | |
| } | |
| // ===================== Tool handlers ===================== | |
| function handleTools(text, scope) { | |
| var raw = extractToolJSON(text); | |
| if (!raw) return; | |
| var cmd; | |
| try { cmd = JSON.parse(raw); } catch(e) { | |
| var 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") { | |
| var n = document.createElement("p"); | |
| n.textContent = t("generatingImage"); | |
| scope.appendChild(n); | |
| fetch("/api/image", {method:"POST", headers:{"Content-Type":"application/json"}, | |
| body:JSON.stringify({prompt:cmd.prompt, clientId:clientId})}) | |
| .then(function(r){return safeJSON(r);}) | |
| .then(function(j) { | |
| if (j.image) { | |
| var fname = "genisi-" + Date.now() + ".png"; | |
| var div = document.createElement("div"); | |
| div.innerHTML = '<img class="gen-img" src="' + escapeHtml(j.image) + '" alt="">' + | |
| '<div class="img-actions"><button class="suggest" id="dlImg">' + t("downloadImg") + '</button></div>' + | |
| '<p style="color:var(--muted);font-size:13px">' + (j.remaining||0) + ' ' + t("imagesLeft") + '</p>'; | |
| div.querySelector("#dlImg").onclick = function(){downloadUrl(j.image, fname);}; | |
| n.replaceWith(div); | |
| } else { | |
| n.textContent = j.message || ""; | |
| } | |
| }) | |
| .catch(function(){n.textContent = "error";}); | |
| } else if (cmd.tool === "search") { | |
| var n = document.createElement("p"); | |
| n.textContent = t("searching"); | |
| scope.appendChild(n); | |
| fetch("/api/search", {method:"POST", headers:{"Content-Type":"application/json"}, | |
| body:JSON.stringify({query:cmd.query})}) | |
| .then(function(r){return safeJSON(r);}) | |
| .then(function(j) { | |
| n.innerHTML = (j.results || []).map(function(x) { | |
| return '<a href="' + escapeHtml(x.url) + '" target="_blank">' + escapeHtml(x.title) + '</a>' + | |
| '<br><small>' + escapeHtml(x.snippet) + '</small>'; | |
| }).join("<br><br>") || "No results."; | |
| }) | |
| .catch(function(){n.textContent = "error";}); | |
| } else if (cmd.tool === "image_search") { | |
| var n = document.createElement("div"); | |
| scope.appendChild(n); | |
| fetchImages(cmd.query).then(function(imgs) { | |
| n.innerHTML = imgs.slice(0,6).map(function(u) { | |
| return '<img class="gen-img" style="display:inline-block;width:31%;margin:1%" src="' + escapeHtml(proxify(u)) + '">'; | |
| }).join(""); | |
| }).catch(function(){n.textContent = "error";}); | |
| } else if (cmd.tool === "document") { | |
| renderDocPreview(cmd, scope); | |
| } else if (cmd.tool === "video") { | |
| renderVideo(cmd, scope); | |
| } | |
| } | |
| // ===================== Document helpers ===================== | |
| 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) { | |
| var c = cmd.content || {}, esc = escapeHtml, blocks = []; | |
| if (c.heading) blocks.push("<h2>" + esc(c.heading) + "</h2>"); | |
| (c.paragraphs || []).forEach(function(p){blocks.push("<p>" + esc(asTextC(p)) + "</p>");}); | |
| (c.sections || []).forEach(function(s) { | |
| if (s.title) blocks.push("<h3>" + esc(s.title) + "</h3>"); | |
| var b = s.body || s.content || s.text; | |
| if (b) blocks.push("<p>" + esc(asTextC(b)) + "</p>"); | |
| (s.bullets || s.points || []).forEach(function(x){blocks.push("<li>" + esc(asTextC(x)) + "</li>");}); | |
| }); | |
| (c.slides || []).forEach(function(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(function(x){return "<li>"+esc(asTextC(x))+"</li>";}).join("") + | |
| "</ul></div>"); | |
| }); | |
| if (c.sheets || c.headers) { | |
| var sh = (c.sheets && c.sheets[0]) || c; | |
| if (sh.headers) { | |
| blocks.push("<table><thead><tr>" + | |
| sh.headers.map(function(h){return "<th>"+esc(asTextC(h))+"</th>";}).join("") + | |
| "</tr></thead><tbody>" + | |
| (sh.rows || []).map(function(r){return "<tr>"+r.map(function(x){return "<td>"+esc(asTextC(x))+"</td>";}).join("")+"</tr>";}).join("") + | |
| "</tbody></table>"); | |
| } | |
| } | |
| (c.bullets || c.points || []).forEach(function(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) { | |
| var wrap = document.createElement("div"); | |
| wrap.className = "doc-preview"; | |
| wrap.innerHTML = docToHtml(cmd); | |
| scope.appendChild(wrap); | |
| var 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(function(pair) { | |
| var b = document.createElement("button"); | |
| b.className = "doc-dl-btn"; b.textContent = pair[1]; | |
| b.onclick = function(){downloadDoc(Object.assign({}, cmd, {format:pair[0]}), b);}; | |
| bar.appendChild(b); | |
| }); | |
| scope.appendChild(bar); | |
| } | |
| function downloadDoc(cmd, btn) { | |
| var old = btn.textContent; | |
| btn.textContent = "..."; btn.disabled = true; | |
| fetch("/api/document", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(cmd)}) | |
| .then(function(r) { | |
| if (!r.ok) throw 0; | |
| return r.blob(); | |
| }) | |
| .then(function(blob) { | |
| var url = URL.createObjectURL(blob); | |
| var 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(function(){alert("error");}) | |
| .finally(function(){btn.textContent = old; btn.disabled = false;}); | |
| } | |
| // ===================== Media helpers ===================== | |
| function proxify(u) { | |
| if (!u) return u; | |
| if (u.indexOf("data:") === 0) return u; | |
| return "/api/img?u=" + encodeURIComponent(u); | |
| } | |
| function fetchImages(query) { | |
| return fetch("/api/image-search", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({query:query})}) | |
| .then(function(r){return safeJSON(r);}) | |
| .then(function(j){return (j.images || []).map(function(x){return x.image || x.thumbnail;}).filter(Boolean);}); | |
| } | |
| function renderVideo(cmd, scope) { | |
| var status = document.createElement("p"); | |
| status.textContent = t("buildingVideo"); | |
| scope.appendChild(status); | |
| return fetch("/api/video", {method:"POST", headers:{"Content-Type":"application/json"}, | |
| body:JSON.stringify({prompt:cmd.prompt||cmd.query||cmd.title||"", clientId:clientId})}) | |
| .then(function(r){return safeJSON(r);}) | |
| .then(function(j) { | |
| if (!j.video) { status.textContent = j.message || "error"; return; } | |
| status.remove(); | |
| var box = document.createElement("div"); | |
| box.className = "video-box"; | |
| var stage = document.createElement("div"); | |
| stage.className = "video-stage"; | |
| var video = document.createElement("video"); | |
| video.src = j.video; video.controls = true; | |
| stage.appendChild(video); | |
| box.appendChild(stage); | |
| var ctrl = document.createElement("div"); | |
| ctrl.className = "video-ctrl"; | |
| var dlBtn = document.createElement("button"); | |
| dlBtn.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>'; | |
| dlBtn.onclick = function(){downloadUrl(j.video, (cmd.title||"genisi-video")+".mp4");}; | |
| var vstat = document.createElement("span"); | |
| vstat.className = "vstatus"; | |
| vstat.textContent = j.remaining !== undefined ? j.remaining + " " + t("imagesLeft") : ""; | |
| ctrl.appendChild(dlBtn); ctrl.appendChild(vstat); | |
| box.appendChild(ctrl); | |
| scope.appendChild(box); | |
| }) | |
| .catch(function(){status.textContent = t("errConnect");}); | |
| } | |
| // ===================== Misc ===================== | |
| function maybeTitle(s) { | |
| var u = s.messages.filter(function(m){return m.role === "user";}).length; | |
| if (u !== 1 && u !== 3) return; | |
| fetch("/api/title", {method:"POST", headers:{"Content-Type":"application/json"}, | |
| body:JSON.stringify({messages:s.messages})}) | |
| .then(function(r){return safeJSON(r);}) | |
| .then(function(j) { | |
| if (j.title) { s.title = j.title; save(); buildRecents(); } | |
| }) | |
| .catch(function(){}); | |
| } | |
| function learnInterests(text) { | |
| var words = (text || "").toLowerCase().match(/[\u0600-\u06FF\w]{4,}/g) || []; | |
| var stop = new Set(["this","that","with","please","about","comment","pourquoi","هذا","هذه","التي","الذي","كيف"]); | |
| words.filter(function(w){return !stop.has(w);}).slice(0,3).forEach(function(w) { | |
| if (state.interests.indexOf(w) === -1) 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:.*$/,""); | |
| } | |
| function speak(text) { | |
| if (!text.trim()) return; | |
| fetch("/api/tts", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({text:text})}) | |
| .then(function(r) { | |
| if (!r.ok) throw 0; | |
| return r.blob(); | |
| }) | |
| .then(function(blob){new Audio(URL.createObjectURL(blob)).play();}) | |
| .catch(function() { | |
| if (window.speechSynthesis) speechSynthesis.speak(new SpeechSynthesisUtterance(text)); | |
| }); | |
| } | |
| var recog = null; | |
| function toggleMic(taId, btn) { | |
| var 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"); | |
| var ta = $("#" + taId), base = ta.value; | |
| recog.onresult = function(e) { | |
| var tx = ""; | |
| for (var i = 0; i < e.results.length; i++) tx += e.results[i][0].transcript; | |
| ta.value = (base ? base + " " : "") + tx; | |
| onTaInput(ta, taId === "homeTA" ? "homeSend" : "chatSend"); | |
| }; | |
| recog.onend = function() { btn.classList.remove("recording"); recog = null; }; | |
| recog.onerror = function() { btn.classList.remove("recording"); recog = null; }; | |
| recog.start(); | |
| } | |
| function clearAll() { | |
| if (confirm(t("confirmClear"))) { LS.clear(); location.reload(); } | |
| } | |
| // Start | |
| init(); | |
| </script> | |
| <script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a1759fac19f0335d',t:'MTc4MzQxNDQ1OA=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body> | |
| </html> | |