| <!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<TOKEN>/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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')} |
| 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)})} |
| |
| 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'){ |
| |
| 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); |
| }; |
| } |
| |
| |
| setTimeout(startSSESync, 1000); |
| |
| init(); |
| </script> |
| </body> |
| </html> |