PraisonAI / app /static /index.html
Sanyam400's picture
Update app/static/index.html
e4949fc verified
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>PraisonChat</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
<style>
:root{--bg1:#0c0c18;--bg2:#111122;--bg3:#181830;--bg4:#202040;--bg5:#282858;--border:#2a2a50;--border2:#3a3a6a;--t1:#eeeeff;--t2:#9898cc;--t3:#6060a0;--t4:#3a3a60;--acc:#7c6af7;--acc2:#9580ff;--acc3:rgba(124,106,247,.2);--acc4:rgba(124,106,247,.08);--green:#4fd1a0;--orange:#f0a055;--red:#f06060;--blue:#55b8f7;--yellow:#f0d055;--pink:#f06080;--sidebar:260px;--r:12px;--r2:8px;--r3:6px}
[data-theme=light]{--bg1:#f0f0fa;--bg2:#fff;--bg3:#ebebf8;--bg4:#e0e0f0;--bg5:#d5d5ee;--border:#d0d0e8;--border2:#c0c0dc;--t1:#1a1a35;--t2:#5a5a90;--t3:#9898c0;--t4:#c0c0d8}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;font-family:'Inter',system-ui,sans-serif;background:var(--bg1);color:var(--t1);overflow:hidden;font-size:14px;-webkit-font-smoothing:antialiased}
button,input,textarea,select{font-family:inherit}
a{color:var(--acc);text-decoration:none}
#app{display:flex;height:100vh}
#sb{width:var(--sidebar);min-width:var(--sidebar);background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;z-index:200;transition:transform .25s ease}
#sb-top{padding:14px 12px 10px;border-bottom:1px solid var(--border);flex-shrink:0}
.logo{display:flex;align-items:center;gap:9px;margin-bottom:12px}
.logo-icon{width:34px;height:34px;border-radius:9px;background:linear-gradient(135deg,var(--acc),#a855f7);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;box-shadow:0 2px 10px var(--acc3)}
.logo-text{font-size:17px;font-weight:800;letter-spacing:-.4px}.logo-text span{color:var(--acc)}
.stabs{display:flex;gap:2px;background:var(--bg3);border-radius:var(--r3);padding:3px}
.stab{flex:1;padding:5px 2px;background:none;border:none;cursor:pointer;color:var(--t3);font-size:11px;font-weight:700;border-radius:4px;transition:all .15s;text-align:center;white-space:nowrap}
.stab.active{background:var(--bg2);color:var(--t1);box-shadow:0 1px 4px rgba(0,0,0,.3)}
#new-btn{width:100%;padding:8px;margin-top:10px;background:var(--acc);color:#fff;border:none;border-radius:var(--r2);cursor:pointer;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:6px;transition:background .2s}
#new-btn:hover{background:var(--acc2)}
.tab-panel{display:none;flex:1;overflow-y:auto;padding:6px;scrollbar-width:thin;scrollbar-color:var(--border) transparent;flex-direction:column}
.tab-panel.active{display:flex}
.empty-hint{padding:18px;text-align:center;color:var(--t4);font-size:12px;line-height:1.6}
.conv-item{padding:8px 10px;border-radius:var(--r2);cursor:pointer;display:flex;align-items:center;gap:7px;color:var(--t2);transition:background .12s;position:relative}
.conv-item:hover{background:var(--bg4);color:var(--t1)}.conv-item.active{background:var(--acc3);color:var(--t1)}
.conv-title{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px}
.conv-del{opacity:0;background:none;border:none;cursor:pointer;color:var(--t3);padding:2px 5px;border-radius:3px;font-size:11px}
.conv-item:hover .conv-del{opacity:1}.conv-del:hover{color:var(--red)}
.mem-card,.skill-card{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r2);margin-bottom:5px;overflow:hidden}
.mem-header,.skill-header{display:flex;align-items:center;gap:6px;padding:7px 9px;cursor:pointer;transition:background .12s}
.mem-header:hover,.skill-header:hover{background:var(--bg4)}
.mem-key{font-size:12px;font-weight:700;flex:1;color:var(--acc);font-family:monospace}
.skill-name{font-size:12px;font-weight:700;flex:1;color:var(--green);font-family:monospace}
.mem-del,.skill-del{opacity:0;background:none;border:none;cursor:pointer;color:var(--t3);font-size:11px;padding:1px 4px;border-radius:3px}
.mem-header:hover .mem-del,.skill-header:hover .skill-del{opacity:1}
.mem-del:hover,.skill-del:hover{color:var(--red)}
.mem-preview,.skill-body{display:none;padding:0 9px 7px}
.mem-preview.open,.skill-body.open{display:block}
.skill-desc{font-size:11px;color:var(--t2);margin-bottom:4px}
.skill-code{background:var(--bg1);border-radius:4px;padding:6px;font-family:monospace;font-size:10.5px;color:var(--t1);overflow-x:auto;white-space:pre;max-height:120px;overflow-y:auto}
#sb-footer{padding:8px 6px;border-top:1px solid var(--border);flex-shrink:0}
.sfbtn{padding:8px 10px;border-radius:var(--r2);cursor:pointer;display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--t2);background:none;border:none;width:100%;text-align:left;transition:background .12s}
.sfbtn:hover{background:var(--bg4);color:var(--t1)}
#main{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}
#topbar{padding:10px 16px;border-bottom:1px solid var(--border);background:var(--bg2);display:flex;align-items:center;gap:10px;flex-shrink:0}
#menu-btn{background:none;border:none;color:var(--t2);cursor:pointer;font-size:18px;padding:5px;border-radius:var(--r2);transition:all .15s;line-height:1;display:none}
#menu-btn:hover{background:var(--bg4);color:var(--t1)}
.model-pill{display:flex;align-items:center;gap:6px;background:var(--bg3);border:1px solid var(--border);border-radius:20px;padding:5px 11px;cursor:pointer;font-size:12px;font-weight:700;transition:background .15s;position:relative;flex-shrink:0}
.model-pill:hover{background:var(--bg4)}
.led{width:7px;height:7px;border-radius:50%;background:var(--green);animation:ledP 2.5s infinite}
@keyframes ledP{0%,100%{opacity:1}50%{opacity:.25}}
#topbar-title{flex:1;font-size:14px;font-weight:600;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ibtn{background:none;border:none;cursor:pointer;color:var(--t2);padding:6px;border-radius:var(--r2);font-size:16px;transition:all .15s;line-height:1;position:relative}
.ibtn:hover{background:var(--bg4);color:var(--t1)}
.badge{position:absolute;top:-3px;right:-3px;width:15px;height:15px;background:var(--acc);border-radius:50%;font-size:9px;font-weight:800;color:#fff;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s}
.badge.on{opacity:1}
#model-dd{display:none;position:absolute;top:calc(100% + 6px);left:0;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);min-width:270px;box-shadow:0 8px 30px rgba(0,0,0,.6);z-index:500;overflow:hidden}
#model-dd.open{display:block}
.mdd-item{padding:10px 13px;cursor:pointer;transition:background .12s}
.mdd-item:hover{background:var(--bg4)}.mdd-item.active{background:var(--acc4);border-left:3px solid var(--acc)}
.mdd-name{font-size:13px;font-weight:700}.mdd-meta{font-size:11px;color:var(--t3);margin-top:2px}
#msgs-wrap{flex:1;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--border) transparent}
#msgs{max-width:800px;margin:0 auto;padding:20px 16px;display:flex;flex-direction:column;gap:4px}
#welcome{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:calc(100vh - 200px);padding:40px 20px;text-align:center}
#welcome h1{font-size:32px;font-weight:900;margin-bottom:8px;background:linear-gradient(135deg,var(--acc),#a855f7,var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
#welcome p{color:var(--t2);font-size:15px;max-width:480px;line-height:1.65;margin-bottom:22px}
.feat-chips{display:flex;flex-wrap:wrap;gap:7px;justify-content:center;margin-bottom:26px}
.chip{background:var(--bg3);border:1px solid var(--border);border-radius:20px;padding:5px 12px;font-size:12px;color:var(--t2)}
.sg{display:grid;grid-template-columns:1fr 1fr;gap:9px;max-width:570px;width:100%}
.sc{padding:13px;background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);cursor:pointer;text-align:left;transition:all .2s;font-size:13px;color:var(--t2);line-height:1.45}
.sc:hover{background:var(--bg4);border-color:var(--acc);color:var(--t1);transform:translateY(-1px)}
.sc strong{display:block;color:var(--t1);margin-bottom:3px;font-size:13.5px}
.mrow{display:flex;gap:10px;padding:8px 0;animation:fadeUp .18s ease}
@keyframes fadeUp{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
.mrow.user{flex-direction:row-reverse}
.av{width:34px;height:34px;min-width:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:800;flex-shrink:0}
.av.uav{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff}
.av.aav{background:linear-gradient(135deg,var(--acc),#a855f7);color:#fff}
.mcont{flex:1;min-width:0}.mrow.user .mcont{display:flex;flex-direction:column;align-items:flex-end}
.bubble{padding:11px 15px;border-radius:var(--r);font-size:14px;line-height:1.68}
.mrow.user .bubble{background:linear-gradient(135deg,var(--acc),#8b5cf6);color:#fff;border-radius:var(--r) var(--r) 3px var(--r);max-width:80%}
.mrow.assistant .bubble{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r) var(--r) var(--r) 3px;width:100%}
.bubble h1,.bubble h2,.bubble h3{margin:14px 0 6px;line-height:1.3}
.bubble h1{font-size:20px}.bubble h2{font-size:17px}.bubble h3{font-size:15px}
.bubble p{margin:0 0 10px}.bubble p:last-child{margin:0}
.bubble ul,.bubble ol{margin:7px 0 7px 18px}.bubble li{margin-bottom:3px}
.bubble strong{font-weight:700}.bubble em{font-style:italic}
.bubble a{color:var(--acc)}.bubble blockquote{border-left:3px solid var(--acc);padding-left:12px;color:var(--t2);margin:8px 0}
.bubble table{border-collapse:collapse;width:100%;margin:9px 0;font-size:13px}
.bubble th,.bubble td{border:1px solid var(--border);padding:7px 10px}.bubble th{background:var(--bg4);font-weight:700}
.bubble hr{border:none;border-top:1px solid var(--border);margin:12px 0}
.cbw{position:relative;margin:9px 0;border-radius:var(--r2);overflow:hidden;border:1px solid var(--border2)}
.chead{display:flex;justify-content:space-between;align-items:center;padding:5px 11px;background:#0d1117;font-size:11px;color:#8b949e}
.cpbtn{background:none;border:1px solid #30363d;color:#8b949e;padding:2px 8px;border-radius:3px;cursor:pointer;font-size:10.5px;transition:all .15s}
.cpbtn:hover{background:#21262d;color:#e6edf3}
.bubble pre{margin:0}.bubble pre code{display:block;padding:12px;overflow-x:auto;font-size:12.5px;line-height:1.55}
.bubble code:not(pre code){background:var(--bg4);padding:2px 5px;border-radius:3px;font-size:12.5px;font-family:'JetBrains Mono','Fira Code',monospace}
.status-pill{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:20px;font-size:11px;font-weight:600;margin-bottom:6px;background:var(--acc4);color:var(--acc);border:1px solid var(--acc3)}
.status-pill.done{display:none}
.s-dot{width:6px;height:6px;border-radius:50%;background:currentColor;animation:ledP .8s infinite}
.mmeta{font-size:11px;color:var(--t4);margin-top:5px;padding:0 3px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.mcbtn{background:none;border:none;cursor:pointer;color:var(--t4);font-size:11px;padding:2px 6px;border-radius:3px;transition:all .15s}
.mcbtn:hover{background:var(--bg4);color:var(--t1)}
.audio-player{margin:8px 0;display:flex;align-items:center;gap:9px;background:var(--bg4);border:1px solid var(--border);border-radius:var(--r2);padding:8px 12px}
.aplay-btn{width:34px;height:34px;border-radius:50%;background:var(--acc);border:none;cursor:pointer;color:#fff;font-size:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s}
.aplay-btn:hover{background:var(--acc2)}
.awave{display:flex;gap:3px;align-items:center;height:18px}
.awave span{width:3px;border-radius:2px;background:var(--acc)}
.awave.playing span{animation:wave .8s infinite}
.awave span:nth-child(1){height:30%}.awave span:nth-child(2){height:70%;animation-delay:.1s}
.awave span:nth-child(3){height:100%;animation-delay:.2s}.awave span:nth-child(4){height:60%;animation-delay:.3s}
.awave span:nth-child(5){height:40%;animation-delay:.4s}
@keyframes wave{0%,100%{transform:scaleY(.3)}50%{transform:scaleY(1)}}
.img-response{margin:8px 0;border:1px solid var(--border);border-radius:var(--r);overflow:hidden}
.img-response img{width:100%;display:block;max-height:500px;object-fit:contain;background:#000}
.img-caption{padding:6px 11px;font-size:11.5px;color:var(--t3);background:var(--bg4)}
.file-dl{display:flex;align-items:center;gap:10px;background:var(--bg4);border:1px solid var(--border);border-radius:var(--r2);padding:9px 13px;margin:6px 0}
.file-dl-icon{font-size:20px;flex-shrink:0}
.file-dl-info{flex:1;min-width:0}
.file-dl-name{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.file-dl-size{font-size:11px;color:var(--t3)}
.file-dl-btn{padding:5px 12px;background:var(--acc);color:#fff;border:none;border-radius:var(--r2);cursor:pointer;font-size:12px;font-weight:700;transition:background .15s}
.file-dl-btn:hover{background:var(--acc2)}
.typing-dots{display:flex;gap:5px;padding:12px 15px}
.typing-dots span{width:7px;height:7px;background:var(--t3);border-radius:50%;animation:bounce 1.1s infinite}
.typing-dots span:nth-child(2){animation-delay:.2s}.typing-dots span:nth-child(3){animation-delay:.4s}
@keyframes bounce{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-7px)}}
#ap{width:280px;min-width:280px;background:var(--bg2);border-left:1px solid var(--border);display:flex;flex-direction:column;transition:all .25s ease}
#ap.hidden{transform:translateX(100%);width:0;min-width:0;border:none}
#ap-hdr{padding:11px 13px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:7px;flex-shrink:0}
#ap-hdr h3{flex:1;font-size:12.5px;font-weight:700}
#ap-close{background:none;border:none;cursor:pointer;color:var(--t3);font-size:14px;padding:3px;border-radius:3px;transition:all .15s}
#ap-close:hover{background:var(--bg4);color:var(--t1)}
#ap-body{flex:1;overflow-y:auto;padding:7px;scrollbar-width:thin;scrollbar-color:var(--border) transparent}
#ap-footer{padding:7px 12px;border-top:1px solid var(--border);font-size:11.5px;color:var(--t2);display:flex;align-items:center;gap:6px;flex-shrink:0}
.live-dot{width:6px;height:6px;border-radius:50%;background:var(--green);flex-shrink:0}
.live-dot.busy{background:var(--acc);animation:ledP .7s infinite}
#live-txt{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.act{display:flex;gap:7px;padding:5px 6px;border-radius:5px;font-size:11.5px;margin-bottom:2px;border-left:2px solid transparent;animation:fadeUp .2s ease}
.act.thinking{border-color:var(--acc);background:var(--acc4);color:var(--t2)}
.act.exec{border-color:var(--orange);background:rgba(240,160,85,.07)}
.act.done{border-color:var(--green);background:rgba(79,209,160,.07)}
.act.error{border-color:var(--red);background:rgba(240,96,96,.07)}
.act.agent{border-color:var(--blue);background:rgba(85,184,247,.07)}
.act.memory{border-color:var(--yellow);background:rgba(240,208,85,.07)}
.act.skill{border-color:var(--pink);background:rgba(240,96,128,.07)}
.act-icon{font-size:12px;flex-shrink:0;margin-top:1px}
.act-body{flex:1;color:var(--t2);line-height:1.4;min-width:0}
.act-body strong{color:var(--t1)}.act-body code{font-family:monospace;font-size:10.5px;background:var(--bg4);padding:1px 4px;border-radius:2px;color:var(--acc)}
.act-out{margin-top:3px;background:var(--bg1);border-radius:3px;padding:4px 6px;font-family:monospace;font-size:10.5px;color:var(--t1);max-height:60px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;opacity:.8}
.act-time{font-size:10px;color:var(--t4);flex-shrink:0;align-self:flex-start;margin-top:2px}
#input-area{padding:12px 16px 16px;background:var(--bg2);border-top:1px solid var(--border);flex-shrink:0}
#input-wrap{max-width:800px;margin:0 auto}
#ibox{display:flex;align-items:flex-end;gap:8px;background:var(--bg3);border:1.5px solid var(--border);border-radius:var(--r);padding:9px 11px;transition:border-color .2s,box-shadow .2s}
#ibox:focus-within{border-color:var(--acc);box-shadow:0 0 0 3px var(--acc3)}
#msg-input{flex:1;background:none;border:none;color:var(--t1);font-size:14.5px;resize:none;outline:none;line-height:1.5;max-height:160px;min-height:22px;font-family:inherit}
#msg-input::placeholder{color:var(--t4)}
.iact{width:36px;height:36px;min-width:36px;border:none;border-radius:var(--r2);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:15px;transition:all .15s;flex-shrink:0}
#send-btn{background:var(--acc);color:#fff}
#send-btn:hover:not(:disabled){background:var(--acc2);transform:scale(1.05)}
#send-btn:disabled{opacity:.4;cursor:not-allowed}
#stop-btn{background:var(--red);color:#fff;display:none}
.ihint{font-size:11px;color:var(--t4);margin-top:6px;text-align:center}
.overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);backdrop-filter:blur(8px);z-index:999;align-items:center;justify-content:center}
.overlay.open{display:flex}
.modal{background:var(--bg2);border:1px solid var(--border);border-radius:16px;padding:26px;width:440px;max-width:95vw;box-shadow:0 8px 40px rgba(0,0,0,.7);animation:modalIn .2s ease;max-height:90vh;overflow-y:auto}
@keyframes modalIn{from{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}
.modal h2{font-size:17px;font-weight:800;margin-bottom:18px}
.fg{margin-bottom:14px}
.fg label{display:block;font-size:11.5px;font-weight:700;color:var(--t2);margin-bottom:5px;text-transform:uppercase;letter-spacing:.5px}
.fg input,.fg select,.fg textarea{width:100%;padding:9px 11px;background:var(--bg3);border:1.5px solid var(--border);border-radius:var(--r2);color:var(--t1);font-size:13.5px;outline:none;transition:border-color .2s}
.fg input:focus,.fg select:focus,.fg textarea:focus{border-color:var(--acc)}
.fg textarea{resize:vertical;min-height:60px}
.fg .hint{font-size:11px;color:var(--t3);margin-top:4px}
.mactions{display:flex;gap:8px;justify-content:flex-end;margin-top:20px}
.btn-p{padding:8px 18px;background:var(--acc);color:#fff;border:none;border-radius:var(--r2);cursor:pointer;font-size:13.5px;font-weight:700;transition:background .2s}
.btn-p:hover{background:var(--acc2)}
.btn-g{padding:8px 18px;background:none;color:var(--t2);border:1px solid var(--border);border-radius:var(--r2);cursor:pointer;font-size:13.5px;transition:background .2s}
.btn-g:hover{background:var(--bg4)}
.btn-danger{padding:8px 18px;background:none;color:var(--red);border:1px solid rgba(240,96,96,.3);border-radius:var(--r2);cursor:pointer;font-size:13.5px;transition:all .2s}
.btn-danger:hover{background:rgba(240,96,96,.1)}
.info-box{background:var(--bg3);border-radius:var(--r2);padding:12px;font-size:12.5px;color:var(--t2);line-height:1.6;margin-bottom:14px;border:1px solid var(--border)}
.info-box strong{color:var(--t1)}
.status-box{padding:10px 12px;border-radius:var(--r2);font-size:12.5px;border:1px solid var(--border);background:var(--bg3);margin-bottom:14px;line-height:1.5}
.status-box.ok{border-color:var(--green);background:rgba(79,209,160,.08);color:var(--green)}
.status-box.err{border-color:var(--red);background:rgba(240,96,96,.08);color:var(--red)}
#toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%) translateY(60px);background:var(--bg3);border:1px solid var(--border);border-radius:var(--r2);padding:9px 18px;font-size:12.5px;transition:transform .28s ease;z-index:9999;white-space:nowrap;box-shadow:0 4px 20px rgba(0,0,0,.5)}
#toast.show{transform:translateX(-50%) translateY(0)}
::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:10px}
@media(max-width:900px){#ap{display:none}#ap.mobile-on{display:flex;position:fixed;right:0;top:0;bottom:0;z-index:400;box-shadow:-4px 0 20px rgba(0,0,0,.6)}}
@media(max-width:680px){#sb{position:fixed;left:0;top:0;bottom:0;transform:translateX(-100%)}#sb.open{transform:translateX(0);box-shadow:4px 0 20px rgba(0,0,0,.6)}.sg{grid-template-columns:1fr}#menu-btn{display:flex!important}}
</style>
</head>
<body>
<div id="app">
<aside id="sb">
<div id="sb-top">
<div class="logo"><div class="logo-icon">🦞</div><div class="logo-text">Praison<span>Chat</span></div></div>
<div class="stabs">
<button class="stab active" onclick="switchTab('chats',this)">πŸ’¬ Chats</button>
<button class="stab" onclick="switchTab('memory',this)">🧠 Memory</button>
<button class="stab" onclick="switchTab('skills',this)">⚑ Skills</button>
</div>
<button id="new-btn" onclick="newChat()">οΌ‹ New Chat</button>
</div>
<div id="tab-chats" class="tab-panel active"><div class="empty-hint" id="no-chats">No conversations yet</div></div>
<div id="tab-memory" class="tab-panel"><div class="empty-hint" id="no-mem">No memories yet.<br>Agent saves info automatically.</div><div id="mem-list"></div></div>
<div id="tab-skills" class="tab-panel"><div class="empty-hint" id="no-skills">No skills yet.<br>Agent creates reusable skills as needed.</div><div id="skills-list"></div></div>
<div id="sb-footer">
<button class="sfbtn" onclick="openSettings()">βš™οΈ Settings</button>
<button class="sfbtn" onclick="openTelegram()">πŸ“± Telegram Bot</button>
<button class="sfbtn" onclick="openPkg()">πŸ“¦ Install Package</button>
<button class="sfbtn" onclick="toggleTheme()">πŸŒ“ Toggle Theme</button>
<button class="sfbtn" onclick="clearAllChats()">πŸ—‘οΈ Clear All Chats</button>
</div>
</aside>
<div id="main">
<div id="topbar">
<button id="menu-btn" class="ibtn" onclick="toggleSB()">☰</button>
<div class="model-pill" id="model-pill" onclick="toggleModelDd()">
<div class="led"></div><span id="model-name">LongCat Flash Lite</span><span style="font-size:9px;color:var(--t4)">β–Ό</span>
<div id="model-dd">
<div class="mdd-item active" onclick="selectModel('LongCat-Flash-Lite','LongCat Flash Lite',this)"><div class="mdd-name">LongCat Flash Lite</div><div class="mdd-meta">320K ctx · ⚑ Fastest · 50M free/day</div></div>
<div class="mdd-item" onclick="selectModel('LongCat-Flash-Chat','LongCat Flash Chat',this)"><div class="mdd-name">LongCat Flash Chat</div><div class="mdd-meta">256K ctx Β· πŸš€ Fast Β· 500K free/day</div></div>
<div class="mdd-item" onclick="selectModel('LongCat-Flash-Thinking-2601','LongCat Flash Thinking',this)"><div class="mdd-name">LongCat Flash Thinking</div><div class="mdd-meta">256K ctx · 🧠 Deep reasoning</div></div>
</div>
</div>
<div id="topbar-title">New Chat</div>
<button class="ibtn" onclick="toggleAP()" title="Activity Panel">πŸ“‘<span class="badge" id="ap-badge">0</span></button>
<button class="ibtn" onclick="clearCurrentChat()" title="Clear">πŸ—‘οΈ</button>
<button class="ibtn" onclick="openSettings()" title="Settings">βš™οΈ</button>
</div>
<div id="msgs-wrap"><div id="msgs"><div id="welcome">
<h1>PraisonChat 🦞</h1>
<p>Autonomous AI agent. Real code execution, persistent memory, auto-created skills β€” like OpenClaw, right in your browser.</p>
<div class="feat-chips"><div class="chip">πŸ” Real Search</div><div class="chip">🧠 Memory</div><div class="chip">⚑ Auto Skills</div><div class="chip">πŸ€– Sub-Agents</div><div class="chip">πŸ”Š Voice</div><div class="chip">πŸ“Š Charts</div></div>
<div class="sg">
<div class="sc" onclick="suggest(this)"><strong>πŸ” Web Research</strong>Search for the latest AI agent frameworks in 2025 and write a summary</div>
<div class="sc" onclick="suggest(this)"><strong>πŸ• Date & Time</strong>What is today's exact date, time, and day of the week?</div>
<div class="sc" onclick="suggest(this)"><strong>πŸ”Š Voice Answer</strong>Explain how neural networks work and give me the response as voice audio</div>
<div class="sc" onclick="suggest(this)"><strong>πŸ“Š Generate Chart</strong>Create a bar chart of the world top 10 programming languages by popularity</div>
</div>
</div></div></div>
<div id="input-area"><div id="input-wrap">
<div id="ibox">
<textarea id="msg-input" rows="1" placeholder="Message PraisonChat… Try: 'search for news' or 'make a QR code' or 'say hello in voice'" onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
<button id="send-btn" class="iact" onclick="sendMsg()">➀</button>
<button id="stop-btn" class="iact" onclick="stopGen()">⏹</button>
</div>
<div class="ihint">Enter ↡ to send Β· Shift+Enter for new line</div>
</div></div>
</div>
<div id="ap">
<div id="ap-hdr"><span>πŸ“‘</span><h3>Live Activity</h3><button id="ap-close" onclick="toggleAP()">βœ•</button></div>
<div id="ap-body"></div>
<div id="ap-footer"><div class="live-dot" id="ldot"></div><span id="live-txt">Ready</span></div>
</div>
</div>
<div class="overlay" id="settings-modal" onclick="closeOut(event,'settings-modal')">
<div class="modal"><h2>βš™οΈ Settings</h2>
<div class="fg"><label>LongCat API Key</label><input type="password" id="s-key" placeholder="Paste your key…"/><div class="hint">Free at <a href="https://longcat.chat/platform" target="_blank">longcat.chat/platform</a> β€” Flash Lite: 50M tokens/day</div></div>
<div class="fg"><label>Temperature β€” <span id="s-tv" style="color:var(--acc)">0.7</span></label><input type="range" id="s-temp" min="0" max="1" step="0.05" value="0.7" style="padding:0" oninput="document.getElementById('s-tv').textContent=this.value"/></div>
<div class="fg"><label>System Prompt (optional)</label><textarea id="s-sys" placeholder="You are a helpful AI…" rows="2"></textarea></div>
<div class="mactions"><button class="btn-g" onclick="closeModal('settings-modal')">Cancel</button><button class="btn-p" onclick="saveSettings()">Save</button></div>
</div>
</div>
<div class="overlay" id="tg-modal" onclick="closeOut(event,'tg-modal')">
<div class="modal"><h2>πŸ“± Telegram Bot</h2>
<div id="tg-status" class="status-box">Checking…</div>
<div class="fg"><label>Bot Token</label><input type="password" id="tg-token" placeholder="1234567890:ABCdef…"/><div class="hint">Get from <a href="https://t.me/BotFather" target="_blank">@BotFather</a> β†’ /newbot</div></div>
<div class="fg"><label>HuggingFace Space URL</label><input type="text" id="tg-url" placeholder="https://sanyam400-praisonai.hf.space"/><div class="hint">Must be HTTPS. Webhook registers here automatically.</div></div>
<div class="info-box">
<strong>Method 1 β€” Auto setup (if network allows):</strong><br>
1. Create bot via @BotFather β†’ /newbot<br>
2. Paste token above + your Space URL<br>
3. Click Connect Bot<br><br>
<strong>Method 2 β€” HF Space Secrets (recommended):</strong><br>
1. Go to HF Space β†’ Settings β†’ Variables and Secrets<br>
2. Add secret: <code>TELEGRAM_BOT_TOKEN</code> = your token<br>
3. Add secret: <code>LONGCAT_API_KEY</code> = your LongCat key<br>
4. Restart the Space<br>
5. Register webhook manually: open <code>https://api.telegram.org/bot&lt;TOKEN&gt;/setWebhook?url=https://sanyam400-praisonai.hf.space/telegram/webhook</code> in browser
</div>
<div class="mactions"><button class="btn-danger" onclick="disconnectTG()" id="tg-disc-btn" style="display:none">Disconnect</button><button class="btn-g" onclick="closeModal('tg-modal')">Close</button><button class="btn-p" onclick="connectTG()">Connect Bot</button></div>
</div>
</div>
<div class="overlay" id="pkg-modal" onclick="closeOut(event,'pkg-modal')">
<div class="modal"><h2>πŸ“¦ Install Python Package</h2>
<div class="fg"><label>Package Name(s)</label><input type="text" id="pkg-inp" placeholder="e.g. pandas yfinance opencv-python"/><div class="hint">Space-separated. Installed into agent's Python environment.</div></div>
<div id="pkg-result" style="display:none;padding:10px;border-radius:var(--r2);font-size:12.5px;font-family:monospace;margin-top:8px;background:var(--bg3);border:1px solid var(--border)"></div>
<div class="mactions"><button class="btn-g" onclick="closeModal('pkg-modal')">Close</button><button class="btn-p" onclick="installPkg()">Install</button></div>
</div>
</div>
<div id="toast"></div>
<script>
const S={convs:JSON.parse(localStorage.getItem('pc_convs')||'[]'),curId:null,msgs:[],settings:JSON.parse(localStorage.getItem('pc_cfg')||'{"apiKey":"","temperature":0.7,"model":"LongCat-Flash-Lite","systemPrompt":""}'),gen:false,abort:null,actN:0};
const uid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2);
function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
function escA(s){return String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,' ').slice(0,500)}
function fmtTime(ts){return ts?new Date(ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}):''}
function scrollBot(){const w=document.getElementById('msgs-wrap');w.scrollTop=w.scrollHeight}
function handleKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMsg()}}
function autoResize(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,160)+'px'}
function suggest(el){const t=el.querySelector('strong').nextSibling?.textContent?.trim()||el.textContent.trim();sendMsg(t)}
function showToast(msg){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2600)}
function saveConvs(){localStorage.setItem('pc_convs',JSON.stringify(S.convs))}
function saveCfg(){localStorage.setItem('pc_cfg',JSON.stringify(S.settings))}
async function init(){
applySettings();renderConvs();refreshMemory();refreshSkills();
const saved=localStorage.getItem('pc_theme');if(saved)document.documentElement.setAttribute('data-theme',saved);
if(S.convs.length)loadConv(S.convs[0].id);
if(!S.settings.apiKey)setTimeout(openSettings,800);
if(window.innerWidth<900)document.getElementById('ap').classList.add('hidden');
if(window.innerWidth<=680)document.getElementById('menu-btn').style.display='flex';
}
function newChat(){const id=uid();S.convs.unshift({id,title:'New Chat',msgs:[],ts:Date.now()});saveConvs();loadConv(id);closeSB()}
function loadConv(id){const c=S.convs.find(c=>c.id===id);if(!c)return;S.curId=id;S.msgs=[...c.msgs];renderMsgs();renderConvs();document.getElementById('topbar-title').textContent=c.title}
function syncConv(){const i=S.convs.findIndex(c=>c.id===S.curId);if(i<0)return;S.convs[i].msgs=[...S.msgs];const first=S.msgs[0]?.content||'New Chat';S.convs[i].title=first.slice(0,44)+(first.length>44?'…':'');document.getElementById('topbar-title').textContent=S.convs[i].title;saveConvs();renderConvs()}
function delConv(id,e){e.stopPropagation();S.convs=S.convs.filter(c=>c.id!==id);saveConvs();if(S.curId===id){S.curId=null;S.msgs=[];renderMsgs()}renderConvs()}
function clearCurrentChat(){S.msgs=[];renderMsgs();syncConv()}
function clearAllChats(){if(!confirm('Clear all chats?'))return;S.convs=[];S.curId=null;S.msgs=[];saveConvs();renderMsgs();renderConvs()}
function renderConvs(){
const el=document.getElementById('tab-chats');const em=document.getElementById('no-chats');
if(!S.convs.length){em.style.display='';el.innerHTML='';el.appendChild(em);return}
em.style.display='none';
el.innerHTML=S.convs.map(c=>`<div class="conv-item${c.id===S.curId?' active':''}" onclick="loadConv('${c.id}')"><span style="font-size:13px">πŸ’¬</span><span class="conv-title">${esc(c.title)}</span><button class="conv-del" onclick="delConv('${c.id}',event)">βœ•</button></div>`).join('');
}
async function refreshMemory(){
try{const r=await fetch('/api/memory');const d=await r.json();const el=document.getElementById('mem-list');const em=document.getElementById('no-mem');
if(!d.memories?.length){em.style.display='';el.innerHTML='';return}em.style.display='none';
el.innerHTML=d.memories.map(m=>`<div class="mem-card"><div class="mem-header" onclick="this.nextElementSibling.classList.toggle('open')"><span class="mem-key">${esc(m.key)}</span><button class="mem-del" onclick="delMem('${esc(m.key)}',event)">βœ•</button></div><div class="mem-preview">${esc(m.preview)}</div></div>`).join('');}catch(e){}}
async function delMem(key,e){e.stopPropagation();await fetch(`/api/memory/${encodeURIComponent(key)}`,{method:'DELETE'});refreshMemory();showToast('Memory deleted')}
async function refreshSkills(){
try{const r=await fetch('/api/skills');const d=await r.json();const el=document.getElementById('skills-list');const em=document.getElementById('no-skills');
if(!d.skills?.length){em.style.display='';el.innerHTML='';return}em.style.display='none';
el.innerHTML=d.skills.map(s=>`<div class="skill-card"><div class="skill-header" onclick="this.nextElementSibling.classList.toggle('open')"><span class="skill-name">${esc(s.name)}</span><button class="skill-del" onclick="delSkill('${esc(s.name)}',event)">βœ•</button></div><div class="skill-body"><div class="skill-desc">${esc(s.description||'')}</div><div class="skill-code">${esc((s.code||'').slice(0,300))}</div></div></div>`).join('');}catch(e){}}
async function delSkill(name,e){e.stopPropagation();await fetch(`/api/skills/${encodeURIComponent(name)}`,{method:'DELETE'});refreshSkills();showToast('Skill deleted')}
function switchTab(name,btn){document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));document.querySelectorAll('.stab').forEach(b=>b.classList.remove('active'));document.getElementById('tab-'+name).classList.add('active');btn.classList.add('active');if(name==='memory')refreshMemory();if(name==='skills')refreshSkills()}
function renderMsgs(){const el=document.getElementById('msgs');if(!S.msgs.length){el.innerHTML=welcomeHTML();return}el.innerHTML='';S.msgs.forEach(m=>appendMsg(m));scrollBot()}
function welcomeHTML(){return`<div id="welcome"><h1>PraisonChat 🦞</h1><p>Autonomous AI agent. Real code execution, persistent memory, auto-created skills β€” like OpenClaw, right in your browser.</p><div class="feat-chips"><div class="chip">πŸ” Real Search</div><div class="chip">🧠 Memory</div><div class="chip">⚑ Auto Skills</div><div class="chip">πŸ€– Sub-Agents</div><div class="chip">πŸ”Š Voice</div><div class="chip">πŸ“Š Charts</div></div><div class="sg"><div class="sc" onclick="suggest(this)"><strong>πŸ” Web Research</strong>Search for the latest AI agent frameworks in 2025 and write a summary</div><div class="sc" onclick="suggest(this)"><strong>πŸ• Date & Time</strong>What is today's exact date, time, and day of the week?</div><div class="sc" onclick="suggest(this)"><strong>πŸ”Š Voice Answer</strong>Explain how neural networks work and give me the response as voice audio</div><div class="sc" onclick="suggest(this)"><strong>πŸ“Š Generate Chart</strong>Create a bar chart of the world top 10 programming languages by popularity</div></div></div>`}
function appendMsg(msg){
const el=document.getElementById('msgs');const w=document.getElementById('welcome');if(w)w.style.display='none';
const row=document.createElement('div');row.className=`mrow ${msg.role}`;row.dataset.id=msg.id||'';
const isUser=msg.role==='user';const content=isUser?esc(msg.content).replace(/\n/g,'<br>'):renderMD(msg.content||'');
row.innerHTML=`<div class="av ${isUser?'uav':'aav'}">${isUser?'πŸ‘€':'🦞'}</div><div class="mcont">${!isUser?`<div class="status-pill" id="sp-${row.dataset.id}"><div class="s-dot"></div>Working…</div>`:''}<div class="bubble">${content}</div><div class="mmeta"><span>${fmtTime(msg.ts)}</span>${!isUser?`<button class="mcbtn" onclick="copyTxt('${escA(msg.content)}',this)">πŸ“‹ Copy</button><button class="mcbtn" onclick="speakTxt('${escA(msg.content)}')">πŸ”Š Speak</button>`:''}</div></div>`;
el.appendChild(row);addCodeCopyBtns(row);hljs.highlightAll();scrollBot();return row;
}
function markDone(row){if(!row)return;const sp=row.querySelector('.status-pill');if(sp)sp.className='status-pill done'}
function updateBubble(row,content){if(!row)return;const b=row.querySelector('.bubble');if(!b)return;b.innerHTML=renderMD(content);addCodeCopyBtns(row);hljs.highlightAll();scrollBot()}
function addMedia(row,type,data){
if(!row)return;const mc=row.querySelector('.mcont');if(!mc)return;
if(type==='audio'){const id='a'+Date.now();const div=document.createElement('div');div.className='audio-player';div.innerHTML=`<button class="aplay-btn" onclick="toggleAudio('${id}','${data.b64}')">β–Ά</button><span style="flex:1;font-size:12px;color:var(--t2)">πŸ”Š Voice Response</span><div class="awave" id="aw-${id}"><span></span><span></span><span></span><span></span><span></span></div>`;mc.appendChild(div);}
else if(type==='image'){const mime=`image/${data.ext||'png'}`;const div=document.createElement('div');div.className='img-response';div.innerHTML=`<img src="data:${mime};base64,${data.b64}" alt="${esc(data.name)}"/><div class="img-caption">πŸ–ΌοΈ ${esc(data.name)}</div>`;mc.appendChild(div);}
else if(type==='file'){const size=data.size>1048576?`${(data.size/1048576).toFixed(1)} MB`:data.size>1024?`${(data.size/1024).toFixed(0)} KB`:`${data.size} B`;const icon=data.name.endsWith('.pdf')?'πŸ“„':data.name.endsWith('.zip')?'πŸ—œοΈ':data.name.match(/\.(mp3|wav)$/)?'πŸ”Š':'πŸ“Ž';const div=document.createElement('div');div.className='file-dl';div.innerHTML=`<div class="file-dl-icon">${icon}</div><div class="file-dl-info"><div class="file-dl-name">${esc(data.name)}</div><div class="file-dl-size">${size}</div></div><button class="file-dl-btn" onclick="dlB64('${data.b64}','${esc(data.name)}')">⬇ Download</button>`;mc.appendChild(div);}
scrollBot();
}
const _aud={};
function toggleAudio(id,b64){let a=_aud[id];if(!a){const bytes=atob(b64);const arr=new Uint8Array(bytes.length);for(let i=0;i<bytes.length;i++)arr[i]=bytes.charCodeAt(i);a=new Audio(URL.createObjectURL(new Blob([arr],{type:'audio/mpeg'})));_aud[id]=a;a.onended=()=>{document.getElementById('aw-'+id)?.classList.remove('playing');document.querySelector(`[onclick="toggleAudio('${id}','${b64}')"]`).textContent='β–Ά'}}const wave=document.getElementById('aw-'+id);const btn=document.querySelector(`[onclick="toggleAudio('${id}','${b64}')"]`);if(a.paused){a.play();wave?.classList.add('playing');if(btn)btn.textContent='⏸'}else{a.pause();wave?.classList.remove('playing');if(btn)btn.textContent='β–Ά'}}
function speakTxt(text){if(!('speechSynthesis' in window)){showToast('TTS not supported');return}window.speechSynthesis.cancel();const u=new SpeechSynthesisUtterance(text.replace(/[#*`]/g,'').slice(0,600));u.rate=0.95;window.speechSynthesis.speak(u);showToast('πŸ”Š Speaking…')}
function dlB64(b64,name){const bytes=atob(b64);const arr=new Uint8Array(bytes.length);for(let i=0;i<bytes.length;i++)arr[i]=bytes.charCodeAt(i);const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([arr]));a.download=name;a.click()}
async function sendMsg(override){
const inp=document.getElementById('msg-input');const text=override||inp.value.trim();
if(!text||S.gen)return;
if(!S.settings.apiKey){openSettings();showToast('⚠️ Set your LongCat API key first');return}
if(!S.curId)newChat();
const w=document.getElementById('welcome');if(w)w.style.display='none';
const um={id:uid(),role:'user',content:text,ts:Date.now()};S.msgs.push(um);appendMsg(um);
inp.value='';autoResize(inp);syncConv();
const am={id:uid(),role:'assistant',content:'',ts:Date.now()};S.msgs.push(am);
const typing=addTyping();
S.gen=true;S.abort=new AbortController();
document.getElementById('send-btn').disabled=true;document.getElementById('stop-btn').style.display='flex';
setLive(true,'Thinking…');clearAct();S.actN=0;document.getElementById('ap-badge').textContent='0';document.getElementById('ap-badge').className='badge';
let msgRow=null,started=false;
try{
const resp=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({messages:[...S.msgs.slice(0,-1).map(m=>({role:m.role,content:m.content})),{role:'user',content:text}],api_key:S.settings.apiKey,model:S.settings.model||'LongCat-Flash-Lite',temperature:S.settings.temperature||0.7}),signal:S.abort.signal});
if(!resp.ok){const e=await resp.json().catch(()=>({detail:'Error '+resp.status}));throw new Error(e.detail||'Request failed')}
const reader=resp.body.getReader();const dec=new TextDecoder();let buf='';
while(true){const{value,done}=await reader.read();if(done)break;buf+=dec.decode(value,{stream:true});const lines=buf.split('\n');buf=lines.pop();
for(const line of lines){if(!line.startsWith('data: '))continue;let ev;try{ev=JSON.parse(line.slice(6))}catch{continue}handleEv(ev)}}
}catch(e){if(e.name!=='AbortError'){typing?.remove();am.content='❌ '+e.message;if(!started)msgRow=appendMsg(am);else updateBubble(msgRow,am.content);if(msgRow)markDone(msgRow)}}
finally{S.gen=false;S.abort=null;document.getElementById('send-btn').disabled=false;document.getElementById('stop-btn').style.display='none';setLive(false,'Ready');syncConv();scrollBot();setTimeout(()=>{refreshMemory();refreshSkills()},1000)}
function handleEv(ev){
switch(ev.type){
case 'thinking':setLive(true,ev.text||'');addAct({cls:'thinking',icon:'🧠',html:`<strong>Planning:</strong> ${esc(ev.text||'')}`});break;
case 'executing':setLive(true,'Executing code…');addAct({cls:'exec',icon:'🐍',html:`Running code block ${(ev.index||0)+1}…`});break;
case 'exec_done':addAct({cls:ev.ok?'done':'error',icon:ev.ok?'βœ…':'❌',html:`Code ${ev.ok?'done':'error'}${ev.files?.length?' Β· '+ev.files.join(', '):''}`,result:(ev.output||'').slice(0,200)});break;
case 'pkg_install':addAct({cls:'exec',icon:'πŸ“¦',html:`${ev.ok?'βœ… Installed':'❌ Failed'}: <code>${esc(ev.package||'')}</code>`});break;
case 'agent_created':addAct({cls:'agent',icon:'πŸ€–',html:`Spawned: <strong>${esc(ev.name||'')}</strong>`});break;
case 'agent_working':setLive(true,`${ev.name} working…`);addAct({cls:'agent',icon:'⚑',html:`<strong>${esc(ev.name||'')}</strong> executing`});break;
case 'agent_done':addAct({cls:'done',icon:'βœ…',html:`<strong>${esc(ev.name||'')}</strong> done`,result:(ev.preview||'').slice(0,200)});break;
case 'memory_saved':addAct({cls:'memory',icon:'🧠',html:`Memory: <code>${esc(ev.key||'')}</code>`});break;
case 'skill_saved':addAct({cls:'skill',icon:'⚑',html:`Skill created: <code>${esc(ev.name||'')}</code>`});break;
case 'response_start':typing?.remove();started=true;msgRow=appendMsg({...am,content:''});break;
case 'token':if(!started){typing?.remove();started=true;msgRow=appendMsg({...am,content:''})}am.content+=ev.content||'';updateBubble(msgRow,am.content);break;
case 'audio_response':if(!started){typing?.remove();started=true;msgRow=appendMsg({...am,content:am.content||'πŸ”Š Voice response:'})}if(msgRow)addMedia(msgRow,'audio',{b64:ev.audio_b64,name:ev.filename||'voice.mp3'});break;
case 'image_response':if(!started){typing?.remove();started=true;msgRow=appendMsg({...am,content:am.content||'πŸ–ΌοΈ Image:'})}if(msgRow)addMedia(msgRow,'image',{b64:ev.image_b64,name:ev.filename||'image.png',ext:ev.ext||'png'});break;
case 'file_response':if(!started){typing?.remove();started=true;msgRow=appendMsg({...am,content:am.content||'πŸ“„ File:'})}if(msgRow)addMedia(msgRow,'file',{b64:ev.file_b64,name:ev.filename||'file',size:ev.size||0});break;
case 'done':if(!started){typing?.remove();msgRow=appendMsg(am)}if(msgRow)markDone(msgRow);break;
case 'error':typing?.remove();am.content='❌ '+ev.message;if(!started)msgRow=appendMsg(am);else updateBubble(msgRow,am.content);if(msgRow)markDone(msgRow);addAct({cls:'error',icon:'❌',html:esc(ev.message||'')});break;
}
}
}
function stopGen(){if(S.abort)S.abort.abort()}
function addTyping(){const el=document.getElementById('msgs');const row=document.createElement('div');row.className='mrow assistant';row.id='typing-row';row.innerHTML='<div class="av aav">🦞</div><div class="mcont"><div class="bubble"><div class="typing-dots"><span></span><span></span><span></span></div></div></div>';el.appendChild(row);scrollBot();return row}
function addAct({cls,icon,html,result}){const el=document.getElementById('ap-body');const now=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});const res=result?`<div class="act-out">${esc(result)}</div>`:'';el.insertAdjacentHTML('beforeend',`<div class="act ${cls}"><div class="act-icon">${icon}</div><div class="act-body">${html}${res}</div><div class="act-time">${now}</div></div>`);el.scrollTop=el.scrollHeight;S.actN++;const badge=document.getElementById('ap-badge');badge.textContent=S.actN;badge.className='badge on'}
function clearAct(){document.getElementById('ap-body').innerHTML=''}
function setLive(busy,txt){document.getElementById('ldot').className='live-dot'+(busy?' busy':'');document.getElementById('live-txt').textContent=txt}
function toggleAP(){const ap=document.getElementById('ap');ap.classList.toggle('hidden');document.getElementById('ap-badge').className='badge';S.actN=0}
function toggleSB(){document.getElementById('sb').classList.toggle('open')}
function closeSB(){document.getElementById('sb').classList.remove('open')}
function toggleModelDd(){document.getElementById('model-dd').classList.toggle('open')}
function selectModel(id,name,el){S.settings.model=id;document.getElementById('model-name').textContent=name;document.querySelectorAll('.mdd-item').forEach(i=>i.classList.remove('active'));el.classList.add('active');document.getElementById('model-dd').classList.remove('open');saveCfg();showToast('Model: '+name)}
document.addEventListener('click',e=>{if(!e.target.closest('#model-pill'))document.getElementById('model-dd')?.classList.remove('open')});
function openSettings(){document.getElementById('s-key').value=S.settings.apiKey||'';document.getElementById('s-temp').value=S.settings.temperature||0.7;document.getElementById('s-tv').textContent=S.settings.temperature||0.7;document.getElementById('s-sys').value=S.settings.systemPrompt||'';document.getElementById('settings-modal').classList.add('open')}
function saveSettings(){S.settings.apiKey=document.getElementById('s-key').value.trim();S.settings.temperature=parseFloat(document.getElementById('s-temp').value);S.settings.systemPrompt=document.getElementById('s-sys').value.trim();saveCfg();closeModal('settings-modal');showToast('βœ… Settings saved')}
function applySettings(){const names={'LongCat-Flash-Lite':'LongCat Flash Lite','LongCat-Flash-Chat':'LongCat Flash Chat','LongCat-Flash-Thinking-2601':'LongCat Flash Thinking'};const el=document.getElementById('model-name');if(el&&S.settings.model)el.textContent=names[S.settings.model]||S.settings.model}
async function openTelegram(){document.getElementById('tg-modal').classList.add('open');const sb=document.getElementById('tg-status');sb.className='status-box';sb.textContent='Checking…';try{const r=await fetch('/api/telegram/status');const d=await r.json();if(d.connected&&d.bot?.username){sb.className='status-box ok';sb.innerHTML=`βœ… Connected as <strong>@${d.bot.username}</strong><br>Webhook: ${d.webhook?.url||'not set'}<br>API key: ${d.longcat_key_set?'βœ… saved':'⚠️ not saved β€” reconnect'}`;document.getElementById('tg-disc-btn').style.display='';}else{sb.className='status-box';sb.textContent='Not connected. Fill in below.';document.getElementById('tg-disc-btn').style.display='none'}}catch(e){sb.textContent='Check failed: '+e.message}}
async function connectTG(){const token=document.getElementById('tg-token').value.trim();const url=document.getElementById('tg-url').value.trim();const sb=document.getElementById('tg-status');if(!token){showToast('⚠️ Enter bot token');return}if(!url){showToast('⚠️ Enter Space URL');return}sb.className='status-box';sb.textContent='Connecting…';try{const r=await fetch('/api/telegram/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,base_url:url,api_key:S.settings.apiKey,model:S.settings.model})});const d=await r.json();if(d.ok){sb.className='status-box ok';sb.innerHTML=`βœ… Connected as <strong>@${d.bot?.username}</strong>!<br>Webhook registered. Open your bot in Telegram!`;document.getElementById('tg-disc-btn').style.display='';showToast('βœ… Telegram connected!')}else{sb.className='status-box err';sb.textContent='❌ '+(d.detail||JSON.stringify(d))}}catch(e){sb.className='status-box err';sb.textContent='Error: '+e.message}}
async function disconnectTG(){if(!confirm('Disconnect?'))return;await fetch('/api/telegram/disconnect',{method:'DELETE'});showToast('Disconnected');openTelegram()}
function openPkg(){document.getElementById('pkg-modal').classList.add('open')}
async function installPkg(){const inp=document.getElementById('pkg-inp').value.trim();if(!inp){showToast('Enter package name');return}const packages=inp.split(/\s+/);const res=document.getElementById('pkg-result');res.style.display='block';res.style.color='var(--t2)';res.style.borderColor='var(--border)';res.textContent='Installing…';try{const r=await fetch('/api/install-package',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({packages})});const d=await r.json();res.style.color=d.ok?'var(--green)':'var(--red)';res.style.borderColor=d.ok?'var(--green)':'var(--red)';res.textContent=(d.ok?'βœ… ':'❌ ')+d.message}catch(e){res.style.color='var(--red)';res.textContent='Error: '+e.message}}
function closeModal(id){document.getElementById(id).classList.remove('open')}
function closeOut(e,id){if(e.target===document.getElementById(id))closeModal(id)}
function toggleTheme(){const t=document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark';document.documentElement.setAttribute('data-theme',t);localStorage.setItem('pc_theme',t)}
function renderMD(txt){if(!txt)return'';marked.setOptions({breaks:true,gfm:true});let h=marked.parse(txt);h=h.replace(/<pre><code(.*?)>([\s\S]*?)<\/code><\/pre>/g,(_,a,c)=>{const lang=(a.match(/class="language-(\w+)"/))||[];return`<div class="cbw"><div class="chead"><span>${lang[1]||'code'}</span><button class="cpbtn" onclick="copyCode(this)">Copy</button></div><pre><code${a}>${c}</code></pre></div>`});return h}
function addCodeCopyBtns(c){c.querySelectorAll('pre code').forEach(b=>{if(!b.closest('.cbw')){const w=document.createElement('div');w.className='cbw';const h=document.createElement('div');h.className='chead';h.innerHTML='<span>code</span><button class="cpbtn" onclick="copyCode(this)">Copy</button>';const pre=b.parentElement;pre.parentNode.insertBefore(w,pre);w.appendChild(h);w.appendChild(pre)}})}
function copyCode(btn){navigator.clipboard.writeText(btn.closest('.cbw').querySelector('code').innerText).then(()=>{btn.textContent='Copied!';setTimeout(()=>btn.textContent='Copy',2000)})}
function copyTxt(txt,btn){navigator.clipboard.writeText(txt).then(()=>{btn.textContent='βœ…';setTimeout(()=>btn.textContent='πŸ“‹ Copy',2000)})}
// ══ CROSS-PLATFORM SYNC ════════════════════════════════════════
let _sseSource = null;
const _sessionId = 'web_' + (localStorage.getItem('pc_session') || (()=>{const id=uid();localStorage.setItem('pc_session',id);return id})());
function startSSESync(){
if(_sseSource) return;
_sseSource = new EventSource(`/api/sse/${_sessionId}`);
_sseSource.onmessage = (e) => {
try{
const ev = JSON.parse(e.data);
if(ev.type === 'tg_message'){
// Telegram message arrived β€” add to current chat
if(!S.curId) newChat();
const msg = {id:uid(), role:ev.role, content:ev.content, ts:Date.now(), source:'telegram'};
S.msgs.push(msg);
const row = appendMsg(msg);
if(row && ev.role==='assistant') markDone(row);
syncConv();
showToast('πŸ“± Message from Telegram');
}
}catch(e){}
};
_sseSource.onerror = () => {
_sseSource = null;
setTimeout(startSSESync, 5000); // reconnect after 5s
};
}
// Start SSE sync
setTimeout(startSSESync, 1000);
init();
</script>
</body>
</html>