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