| <!DOCTYPE html> |
| <html lang="en" data-theme="dark"> |
| <head> |
| <meta charset="UTF-8"/> |
| <meta name="viewport" content="width=device-width,initial-scale=1.0"/> |
| <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:#0d0d18;--bg2:#13132a;--bg3:#1a1a35;--bg4:#22223f;--bg5:#2a2a4e; |
| --border:#2e2e55;--border2:#3d3d6e; |
| --txt1:#eeeeff;--txt2:#9898cc;--txt3:#6060a0;--txt4:#404070; |
| --acc:#7c6af7;--acc2:#9580ff;--acc3:rgba(124,106,247,.18);--acc4:rgba(124,106,247,.08); |
| --green:#4fd1a0;--orange:#f0a055;--red:#f05858;--blue:#55b8f7;--yellow:#f0d055; |
| --sidebar:260px;--activity:300px;--r:10px;--r2:6px; |
| } |
| [data-theme=light]{ |
| --bg1:#f0f0f8;--bg2:#fff;--bg3:#ebebf5;--bg4:#e0e0f0;--bg5:#d5d5ee; |
| --border:#d0d0e8;--border2:#c0c0dc; |
| --txt1:#1a1a30;--txt2:#555580;--txt3:#9898b8;--txt4:#c0c0d8; |
| --acc3:rgba(124,106,247,.12);--acc4:rgba(124,106,247,.05); |
| } |
| *{box-sizing:border-box;margin:0;padding:0} |
| html,body{height:100%;font-family:'Inter',system-ui,sans-serif;background:var(--bg1);color:var(--txt1);overflow:hidden;font-size:14px} |
| button{font-family:inherit} |
|
|
| /* βββ Layout βββββββββββββββββββββββββββββββββββββββββββββ */ |
| #app{display:flex;height:100vh} |
|
|
| /* βββ Left Sidebar βββββββββββββββββββββββββββββββββββββββ */ |
| #sidebar{ |
| 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; |
| } |
| #sidebar-header{padding:14px 12px 10px;border-bottom:1px solid var(--border)} |
| .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:17px;flex-shrink:0} |
| .logo-text{font-size:17px;font-weight:800;letter-spacing:-.4px}.logo-text span{color:var(--acc)} |
|
|
| /* Tabs */ |
| .stabs{display:flex;gap:2px;background:var(--bg3);border-radius:var(--r2);padding:3px} |
| .stab{flex:1;padding:6px 4px;background:none;border:none;cursor:pointer;color:var(--txt3);font-size:12px;font-weight:600;border-radius:4px;transition:all .15s;text-align:center} |
| .stab.active{background:var(--bg2);color:var(--txt1);box-shadow:0 1px 4px rgba(0,0,0,.3)} |
|
|
| #new-chat-btn{ |
| width:100%;padding:8px 12px;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;gap:6px; |
| transition:background .2s,transform .1s;justify-content:center; |
| } |
| #new-chat-btn:hover{background:var(--acc2)} |
| #new-chat-btn:active{transform:scale(.98)} |
|
|
| .tab-panel{display:none;flex:1;overflow-y:auto;padding:6px;scrollbar-width:thin;scrollbar-color:var(--border) transparent} |
| .tab-panel.active{display:flex;flex-direction:column} |
|
|
| /* Conv items */ |
| .group-label{font-size:10px;font-weight:700;color:var(--txt4);text-transform:uppercase;letter-spacing:.8px;padding:8px 6px 3px} |
| .conv-item{padding:8px 10px;border-radius:var(--r2);cursor:pointer;display:flex;align-items:center;gap:7px;color:var(--txt2);transition:background .12s;position:relative} |
| .conv-item:hover{background:var(--bg4);color:var(--txt1)} |
| .conv-item.active{background:var(--acc3);color:var(--txt1)} |
| .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(--txt3);padding:2px 5px;border-radius:4px;font-size:11px;transition:all .15s} |
| .conv-item:hover .conv-del{opacity:1} |
| .conv-del:hover{color:var(--red);background:rgba(240,88,88,.12)} |
|
|
| /* Agent cards in sidebar */ |
| .agent-sidebar-card{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);margin-bottom:6px;overflow:hidden;transition:border-color .2s} |
| .agent-sidebar-card.working{border-color:var(--acc);animation:borderPulse 1.5s infinite} |
| .agent-sidebar-card.done{border-color:var(--green)} |
| .agent-sidebar-card.error{border-color:var(--red)} |
| @keyframes borderPulse{0%,100%{border-color:var(--acc);box-shadow:0 0 0 0 var(--acc3)}50%{border-color:var(--acc2);box-shadow:0 0 0 4px transparent}} |
| .asc-header{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer} |
| .asc-dot{width:8px;height:8px;border-radius:50%;background:var(--txt4);flex-shrink:0} |
| .agent-sidebar-card.working .asc-dot{background:var(--acc);animation:spin .8s linear infinite} |
| .agent-sidebar-card.done .asc-dot{background:var(--green)} |
| .agent-sidebar-card.error .asc-dot{background:var(--red)} |
| @keyframes spin{from{box-shadow:0 0 0 0 var(--acc3)}to{box-shadow:0 0 0 6px transparent}} |
| .asc-name{font-size:12.5px;font-weight:700;flex:1} |
| .asc-del{opacity:0;background:none;border:none;cursor:pointer;color:var(--txt3);font-size:11px;padding:2px 5px;border-radius:3px} |
| .asc-header:hover .asc-del{opacity:1} |
| .asc-del:hover{color:var(--red);background:rgba(240,88,88,.12)} |
| .asc-role{font-size:11px;color:var(--txt3);padding:0 10px 5px} |
| .asc-tools{display:flex;flex-wrap:wrap;gap:3px;padding:0 10px 8px} |
| .tool-chip{background:var(--bg4);border:1px solid var(--border);border-radius:20px;padding:2px 8px;font-size:10.5px;color:var(--txt3);font-family:monospace;cursor:pointer;transition:all .15s} |
| .tool-chip:hover{border-color:var(--acc);color:var(--acc)} |
| .asc-task{font-size:11px;color:var(--txt2);padding:0 10px 8px;font-style:italic} |
| .asc-status{font-size:10.5px;padding:4px 10px 7px;color:var(--txt3)} |
| .asc-status.working{color:var(--acc)} |
| .asc-status.done{color:var(--green)} |
| .asc-status.error{color:var(--red)} |
|
|
| /* Tool cards in sidebar */ |
| .tool-sidebar-card{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);margin-bottom:6px;overflow:hidden} |
| .tsc-header{display:flex;align-items:center;gap:7px;padding:8px 10px;cursor:pointer} |
| .tsc-icon{font-size:14px} |
| .tsc-name{font-size:12.5px;font-weight:700;flex:1;font-family:monospace;color:var(--acc)} |
| .tsc-body{display:none;padding:0 10px 8px;font-size:11.5px;color:var(--txt2)} |
| .tsc-body.open{display:block} |
| .tsc-desc{margin-bottom:5px;color:var(--txt3)} |
| .tsc-code{background:var(--bg1);border-radius:var(--r2);padding:7px;font-family:monospace;font-size:11px;color:var(--txt1);overflow-x:auto;white-space:pre;line-height:1.5} |
| .empty-state{padding:20px;text-align:center;color:var(--txt4);font-size:12.5px} |
|
|
| /* Sidebar footer */ |
| #sidebar-footer{padding:8px 6px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:2px} |
| .sfbtn{padding:8px 10px;border-radius:var(--r2);cursor:pointer;display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--txt2);background:none;border:none;width:100%;text-align:left;transition:background .12s} |
| .sfbtn:hover{background:var(--bg4);color:var(--txt1)} |
|
|
| /* βββ Main area βββββββββββββββββββββββββββββββββββββββββββ */ |
| #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-toggle{background:none;border:none;color:var(--txt2);cursor:pointer;font-size:18px;padding:5px;border-radius:var(--r2);transition:all .15s;line-height:1} |
| #menu-toggle:hover{background:var(--bg4);color:var(--txt1)} |
| .topbar-model{display:flex;align-items:center;gap:7px;background:var(--bg3);border:1px solid var(--border);border-radius:20px;padding:5px 12px;cursor:pointer;font-size:12.5px;font-weight:600;transition:background .15s;position:relative;flex-shrink:0} |
| .topbar-model:hover{background:var(--bg4)} |
| .model-led{width:7px;height:7px;border-radius:50%;background:var(--green);animation:ledPulse 2.5s infinite} |
| @keyframes ledPulse{0%,100%{opacity:1}50%{opacity:.3}} |
| #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(--txt2);padding:6px;border-radius:var(--r2);font-size:16px;transition:all .15s;line-height:1} |
| .ibtn:hover{background:var(--bg4);color:var(--txt1)} |
|
|
| /* Model dropdown */ |
| #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,.5);z-index:500;overflow:hidden} |
| #model-dd.open{display:block} |
| .mdd-item{padding:10px 14px;cursor:pointer;transition:background .12s;display:flex;flex-direction:column;gap:2px} |
| .mdd-item:hover{background:var(--bg4)} |
| .mdd-item.active{background:var(--acc4)} |
| .mdd-name{font-size:13px;font-weight:700} |
| .mdd-meta{font-size:11px;color:var(--txt3)} |
|
|
| /* βββ Messages ββββββββββββββββββββββββββββββββββββββββββββ */ |
| #msgs-wrap{flex:1;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--border) transparent} |
| #msgs{max-width:780px;margin:0 auto;padding:20px 16px;display:flex;flex-direction:column;gap:2px} |
|
|
| /* Welcome */ |
| #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:30px;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(--txt2);font-size:15px;max-width:460px;line-height:1.65;margin-bottom:28px} |
| .feature-badges{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-bottom:28px} |
| .fbadge{background:var(--bg3);border:1px solid var(--border);border-radius:20px;padding:5px 12px;font-size:12px;color:var(--txt2);display:flex;align-items:center;gap:5px} |
| .suggestion-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px;max-width:560px;width:100%} |
| .sc{padding:13px 14px;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(--txt2);line-height:1.45} |
| .sc:hover{background:var(--bg4);border-color:var(--acc);color:var(--txt1);transform:translateY(-1px);box-shadow:0 4px 16px var(--acc3)} |
| .sc strong{display:block;color:var(--txt1);margin-bottom:3px;font-size:13.5px} |
|
|
| /* Message rows */ |
| .mrow{display:flex;gap:11px;padding:10px 0;animation:fadeUp .2s ease} |
| @keyframes fadeUp{from{opacity:0;transform:translateY(5px)}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} |
| .mcontent{flex:1;min-width:0} |
| .mrow.user .mcontent{display:flex;flex-direction:column;align-items:flex-end} |
| .bubble{padding:11px 15px;border-radius:var(--r);font-size:14px;line-height:1.65;max-width:88%} |
| .mrow.user .bubble{background:var(--acc);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%;max-width:100%} |
|
|
| /* Markdown */ |
| .bubble h1,.bubble h2,.bubble h3{margin:13px 0 5px;line-height:1.3} |
| .bubble h1{font-size:19px}.bubble h2{font-size:16px}.bubble h3{font-size:14.5px} |
| .bubble p{margin:0 0 9px}.bubble p:last-child{margin-bottom:0} |
| .bubble ul,.bubble ol{margin:6px 0 6px 18px}.bubble li{margin-bottom:3px} |
| .bubble strong{font-weight:700}.bubble em{font-style:italic} |
| .bubble a{color:var(--acc);text-decoration:underline} |
| .bubble hr{border:none;border-top:1px solid var(--border);margin:12px 0} |
| .bubble blockquote{border-left:3px solid var(--acc);padding-left:11px;margin:7px 0;color:var(--txt2)} |
| .bubble table{border-collapse:collapse;width:100%;margin:9px 0;font-size:12.5px} |
| .bubble th,.bubble td{border:1px solid var(--border);padding:6px 10px} |
| .bubble th{background:var(--bg4);font-weight:700} |
|
|
| /* Code */ |
| .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;border-bottom:1px solid #30363d;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:13px;overflow-x:auto;font-size:12.5px;line-height:1.55} |
| .bubble code:not(pre code){background:var(--bg4);padding:2px 6px;border-radius:3px;font-size:12.5px;font-family:'JetBrains Mono','Fira Code',monospace} |
|
|
| /* Meta row */ |
| .mmeta{font-size:11px;color:var(--txt4);margin-top:5px;padding:0 3px;display:flex;gap:8px;align-items:center;flex-wrap:wrap} |
| .mcpbtn{background:none;border:none;cursor:pointer;color:var(--txt4);font-size:11px;padding:2px 6px;border-radius:3px;transition:all .15s} |
| .mcpbtn:hover{background:var(--bg4);color:var(--txt1)} |
| .speak-btn{background:none;border:none;cursor:pointer;color:var(--txt4);font-size:11px;padding:2px 6px;border-radius:3px;transition:all .15s;display:flex;align-items:center;gap:4px} |
| .speak-btn:hover{background:var(--bg4);color:var(--blue)} |
|
|
| /* Audio player */ |
| .audio-player{margin-top:8px;display:flex;align-items:center;gap:9px;background:var(--bg4);border:1px solid var(--border);border-radius:var(--r2);padding:8px 12px} |
| .audio-play-btn{width:32px;height:32px;border-radius:50%;background:var(--acc);border:none;cursor:pointer;color:#fff;font-size:13px;display:flex;align-items:center;justify-content:center;transition:background .15s;flex-shrink:0} |
| .audio-play-btn:hover{background:var(--acc2)} |
| .audio-label{font-size:12px;color:var(--txt2);flex:1} |
| .audio-wave{display:flex;gap:3px;align-items:center;height:20px} |
| .audio-wave span{width:3px;background:var(--acc);border-radius:2px;animation:wave 1s infinite;animation-play-state:paused} |
| .audio-wave span:nth-child(2){animation-delay:.1s;height:60%} |
| .audio-wave span:nth-child(3){animation-delay:.2s;height:90%} |
| .audio-wave span:nth-child(4){animation-delay:.3s;height:50%} |
| .audio-wave span:nth-child(5){animation-delay:.4s;height:80%} |
| @keyframes wave{0%,100%{transform:scaleY(.4)}50%{transform:scaleY(1)}} |
| .audio-wave.playing span{animation-play-state:running} |
|
|
| /* Typing */ |
| .typing-dots{display:flex;gap:5px;padding:12px 15px} |
| .typing-dots span{width:7px;height:7px;background:var(--txt3);border-radius:50%;animation:bounce 1.1s infinite} |
| .typing-dots span:nth-child(2){animation-delay:.18s} |
| .typing-dots span:nth-child(3){animation-delay:.36s} |
| @keyframes bounce{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-7px)}} |
|
|
| /* βββ Right Activity Panel ββββββββββββββββββββββββββββββββ */ |
| #activity-panel{ |
| width:var(--activity);min-width:var(--activity); |
| background:var(--bg2);border-left:1px solid var(--border); |
| display:flex;flex-direction:column; |
| transition:transform .25s ease, width .25s ease; |
| } |
| #activity-panel.hidden{transform:translateX(100%);width:0;min-width:0;border-left:none} |
| #ap-header{padding:12px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;flex-shrink:0} |
| #ap-header h3{flex:1;font-size:13px;font-weight:700} |
| #ap-close{background:none;border:none;cursor:pointer;color:var(--txt3);font-size:15px;padding:4px;border-radius:4px;transition:all .15s} |
| #ap-close:hover{background:var(--bg4);color:var(--txt1)} |
| #ap-body{flex:1;overflow-y:auto;padding:8px;scrollbar-width:thin;scrollbar-color:var(--border) transparent} |
|
|
| /* Activity items */ |
| .act-item{display:flex;gap:8px;padding:6px 7px;border-radius:var(--r2);font-size:12px;margin-bottom:3px;animation:fadeUp .2s ease;border-left:2px solid transparent} |
| .act-item.thinking{border-left-color:var(--acc);background:var(--acc4)} |
| .act-item.tool_call{border-left-color:var(--orange);background:rgba(240,160,85,.07)} |
| .act-item.tool_result{border-left-color:var(--green);background:rgba(79,209,160,.07)} |
| .act-item.agent_working{border-left-color:var(--blue);background:rgba(85,184,247,.07)} |
| .act-item.agent_done{border-left-color:var(--green);background:rgba(79,209,160,.07)} |
| .act-item.agent_error{border-left-color:var(--red);background:rgba(240,88,88,.07)} |
| .act-item.step{border-left-color:var(--txt4);background:var(--bg3)} |
| .act-icon{font-size:13px;flex-shrink:0;margin-top:1px} |
| .act-content{flex:1;min-width:0;color:var(--txt2);line-height:1.4} |
| .act-content strong{color:var(--txt1);font-weight:600} |
| .act-content code{font-family:monospace;background:var(--bg4);padding:1px 5px;border-radius:3px;font-size:11px;color:var(--acc)} |
| .act-time{font-size:10px;color:var(--txt4);flex-shrink:0} |
| .act-result{margin-top:4px;background:var(--bg1);border-radius:4px;padding:5px 7px;font-family:monospace;font-size:11px;color:var(--txt1);max-height:80px;overflow-y:auto;white-space:pre-wrap;word-break:break-all} |
|
|
| /* Live status bar */ |
| #live-bar{padding:8px 14px;border-top:1px solid var(--border);background:var(--bg3);font-size:11.5px;color:var(--txt2);display:flex;align-items:center;gap:7px;flex-shrink:0} |
| .live-dot{width:7px;height:7px;border-radius:50%;background:var(--green);flex-shrink:0} |
| .live-dot.working{background:var(--acc);animation:ledPulse 1s infinite} |
| #live-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
|
|
| /* βββ Input area ββββββββββββββββββββββββββββββββββββββββββ */ |
| #input-area{padding:12px 16px 16px;background:var(--bg2);border-top:1px solid var(--border);flex-shrink:0} |
| #input-wrap{max-width:780px;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(--txt1);font-size:14px;resize:none;outline:none;line-height:1.5;max-height:160px;min-height:22px;font-family:inherit} |
| #msg-input::placeholder{color:var(--txt4)} |
| .iaction{width:34px;height:34px;min-width:34px;border:none;border-radius:8px;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)} |
| #send-btn:disabled{opacity:.4;cursor:not-allowed} |
| #stop-btn{background:var(--red);color:#fff;display:none} |
| #stop-btn:hover{opacity:.85} |
| .ihint{font-size:11px;color:var(--txt4);margin-top:6px;text-align:center} |
|
|
| /* βββ Settings Modal ββββββββββββββββββββββββββββββββββββββ */ |
| .overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(6px);z-index:999;align-items:center;justify-content:center} |
| .overlay.open{display:flex} |
| .modal{background:var(--bg2);border:1px solid var(--border);border-radius:14px;padding:26px;width:430px;max-width:95vw;box-shadow:0 8px 40px rgba(0,0,0,.6);animation:modalIn .2s ease} |
| @keyframes modalIn{from{opacity:0;transform:scale(.95)}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:12px;font-weight:700;color:var(--txt2);margin-bottom:5px;text-transform:uppercase;letter-spacing:.5px} |
| .fg input,.fg select,.fg textarea{width:100%;padding:8px 11px;background:var(--bg3);border:1.5px solid var(--border);border-radius:var(--r2);color:var(--txt1);font-size:13.5px;outline:none;transition:border-color .2s;font-family:inherit} |
| .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(--txt3);margin-top:4px} |
| .modal-actions{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(--txt2);border:1px solid var(--border);border-radius:var(--r2);cursor:pointer;font-size:13.5px;transition:background .2s} |
| .btn-g:hover{background:var(--bg4)} |
|
|
| /* Tool detail modal */ |
| #tool-modal .modal{width:600px} |
| #tool-code-view{background:var(--bg1);border-radius:var(--r2);padding:12px;font-family:monospace;font-size:12.5px;color:var(--txt1);overflow-x:auto;white-space:pre;line-height:1.6;max-height:300px;overflow-y:auto;border:1px solid var(--border)} |
|
|
| /* Toast */ |
| #toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%) translateY(60px);background:var(--bg3);border:1px solid var(--border);border-radius:8px;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)} |
|
|
| /* Activity panel toggle button */ |
| #ap-toggle{position:relative} |
| #ap-toggle .badge{position:absolute;top:-4px;right:-4px;width:16px;height:16px;background:var(--acc);border-radius:50%;font-size:9px;font-weight:700;color:#fff;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s} |
| #ap-toggle .badge.show{opacity:1} |
|
|
| /* Scrollbar */ |
| ::-webkit-scrollbar{width:4px;height:4px} |
| ::-webkit-scrollbar-track{background:transparent} |
| ::-webkit-scrollbar-thumb{background:var(--border);border-radius:10px} |
|
|
| /* Mobile */ |
| @media(max-width:900px){#activity-panel{display:none}#activity-panel.mobile-open{display:flex;position:fixed;right:0;top:0;bottom:0;z-index:300;box-shadow:-4px 0 20px rgba(0,0,0,.5)}} |
| @media(max-width:680px){ |
| #sidebar{position:fixed;left:0;top:0;bottom:0;transform:translateX(-100%)} |
| #sidebar.open{transform:translateX(0);box-shadow:4px 0 20px rgba(0,0,0,.5)} |
| .suggestion-grid{grid-template-columns:1fr} |
| } |
| </style> |
| </head> |
| <body> |
| <div id="app"> |
|
|
| |
| <aside id="sidebar"> |
| <div id="sidebar-header"> |
| <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('agents',this)">π€ Agents</button> |
| <button class="stab" onclick="switchTab('tools',this)">π§ Tools</button> |
| </div> |
| <button id="new-chat-btn" onclick="newChat()">οΌ New Chat</button> |
| </div> |
|
|
| <div id="tab-chats" class="tab-panel active"></div> |
| <div id="tab-agents" class="tab-panel"> |
| <div class="empty-state" id="agents-empty">No agents created yet.<br>Send a complex task to spawn agents.</div> |
| <div id="agents-list"></div> |
| </div> |
| <div id="tab-tools" class="tab-panel"> |
| <div class="empty-state" id="tools-empty">No custom tools yet.<br>Agents create tools when needed.</div> |
| <div id="tools-list"></div> |
| <div style="padding:8px 6px"> |
| <div class="group-label">Built-in Tools</div> |
| <div id="builtin-tools-list"></div> |
| </div> |
| </div> |
|
|
| <div id="sidebar-footer"> |
| <button class="sfbtn" onclick="openSettings()">βοΈ Settings</button> |
| <button class="sfbtn" onclick="toggleTheme()">π Toggle Theme</button> |
| <button class="sfbtn" onclick="clearAllAgents()">ποΈ Clear Agents</button> |
| <button class="sfbtn" onclick="clearAllChats()">ποΈ Clear All Chats</button> |
| </div> |
| </aside> |
|
|
| |
| <div id="main"> |
| <div id="topbar"> |
| <button id="menu-toggle" class="ibtn" onclick="toggleSidebar()" title="Toggle sidebar">β°</button> |
| <div class="topbar-model" id="model-badge" onclick="toggleModelDd()"> |
| <div class="model-led"></div> |
| <span id="cur-model-name">LongCat Flash Lite</span> |
| <span style="font-size:9px;color:var(--txt4)">βΌ</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 context Β· β‘ Fastest Β· 50M tokens/day free</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 context Β· π Fast Β· 500K tokens/day free</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 context Β· π§ Deep reasoning Β· 500K tokens/day free</div> |
| </div> |
| </div> |
| </div> |
| <div id="topbar-title">New Conversation</div> |
| <button class="ibtn" id="ap-toggle" onclick="toggleActivity()" title="Toggle activity panel">π‘ <span class="badge" id="ap-badge">0</span></button> |
| <button class="ibtn" onclick="clearCurrentChat()" title="Clear chat">ποΈ</button> |
| <button class="ibtn" onclick="openSettings()" title="Settings">βοΈ</button> |
| </div> |
|
|
| <div id="msgs-wrap"> |
| <div id="msgs"> |
| <div id="welcome"> |
| <h1>PraisonChat</h1> |
| <p>Multi-agent AI with dynamic sub-agent spawning, real-time tool creation, voice responses, and 320K context via LongCat Flash Lite.</p> |
| <div class="feature-badges"> |
| <div class="fbadge">π€ Dynamic Agents</div> |
| <div class="fbadge">π§ Auto Tool Creation</div> |
| <div class="fbadge">π Voice Output</div> |
| <div class="fbadge">π Real-time Tools</div> |
| <div class="fbadge">π Code Execution</div> |
| </div> |
| <div class="suggestion-grid"> |
| <div class="sc" onclick="useSuggestion(this)"><strong>π Voice Response</strong>Explain quantum entanglement and give me the answer as voice audio</div> |
| <div class="sc" onclick="useSuggestion(this)"><strong>π Date & Time</strong>What is today's date and time? Also tell me what day of the week it is</div> |
| <div class="sc" onclick="useSuggestion(this)"><strong>π¬ Deep Research</strong>Research the top 5 AI breakthroughs of 2025 and write a detailed report</div> |
| <div class="sc" onclick="useSuggestion(this)"><strong>π» Code & Run</strong>Write and execute a Python script that generates a Fibonacci sequence up to 1000</div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div id="input-area"> |
| <div id="input-wrap"> |
| <div id="ibox"> |
| <textarea id="msg-input" placeholder="Message PraisonChat⦠(try: 'what time is it' or 'respond in voice')" rows="1" |
| onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea> |
| <button id="send-btn" class="iaction" onclick="sendMessage()" title="Send (Enter)">β€</button> |
| <button id="stop-btn" class="iaction" onclick="stopGen()" title="Stop generation">βΉ</button> |
| </div> |
| <div class="ihint">Enter to send Β· Shift+Enter for new line Β· Try "respond in voice" for audio output</div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="activity-panel"> |
| <div id="ap-header"> |
| <span>π‘</span> |
| <h3>Live Activity</h3> |
| <button id="ap-close" onclick="toggleActivity()" title="Close panel">β</button> |
| </div> |
| <div id="ap-body"></div> |
| <div id="live-bar"> |
| <div class="live-dot" id="live-dot"></div> |
| <span id="live-text">Ready</span> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| |
| <div class="overlay" id="settings-modal" onclick="closeModalOutside(event,'settings-modal')"> |
| <div class="modal"> |
| <h2>βοΈ Settings</h2> |
| <div class="fg"> |
| <label>LongCat API Key</label> |
| <input type="password" id="s-apikey" placeholder="Paste your LongCat API keyβ¦"/> |
| <div class="hint">Free at <a href="https://longcat.chat/platform" target="_blank" style="color:var(--acc)">longcat.chat/platform</a> Β· Flash Lite: 50M tokens/day free</div> |
| </div> |
| <div class="fg"> |
| <label>Temperature <span id="s-temp-val" 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-temp-val').textContent=this.value"/> |
| </div> |
| <div class="fg"> |
| <label>System Prompt (optional)</label> |
| <textarea id="s-sysprompt" placeholder="You are a helpful AI assistantβ¦" rows="2"></textarea> |
| </div> |
| <div class="modal-actions"> |
| <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="tool-modal" onclick="closeModalOutside(event,'tool-modal')"> |
| <div class="modal"> |
| <h2 id="tool-modal-title">π§ Tool Details</h2> |
| <div class="fg"><label>Description</label><div id="tool-modal-desc" style="color:var(--txt2);font-size:13px"></div></div> |
| <div class="fg"><label>Implementation</label><div id="tool-code-view"></div></div> |
| <div class="modal-actions"> |
| <button class="btn-g" onclick="closeModal('tool-modal')">Close</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_settings')||'{"apiKey":"","temperature":0.7,"model":"LongCat-Flash-Lite","systemPrompt":""}'), |
| generating: false, |
| abort: null, |
| agents: {}, |
| tools: {}, |
| actCount: 0, |
| }; |
| |
| |
| |
| |
| function init(){ |
| applySettings(); |
| renderConvs(); |
| loadBuiltinTools(); |
| if(S.convs.length) loadConv(S.convs[0].id); |
| if(!S.settings.apiKey) setTimeout(openSettings,700); |
| } |
| |
| async function loadBuiltinTools(){ |
| try{ |
| const r = await fetch('/api/builtin-tools'); |
| const d = await r.json(); |
| const el = document.getElementById('builtin-tools-list'); |
| el.innerHTML = d.tools.map(t=>` |
| <div class="tool-sidebar-card"> |
| <div class="tsc-header" onclick="this.nextElementSibling.classList.toggle('open')"> |
| <span class="tsc-icon">${t.icon}</span> |
| <span class="tsc-name">${esc(t.name)}</span> |
| <span style="font-size:10px;color:var(--green)">built-in</span> |
| </div> |
| <div class="tsc-body"> |
| <div class="tsc-desc">${esc(t.description)}</div> |
| </div> |
| </div>`).join(''); |
| }catch(e){} |
| } |
| |
| |
| |
| |
| function uid(){ return Date.now().toString(36)+Math.random().toString(36).slice(2) } |
| |
| function newChat(){ |
| const id=uid(); |
| S.convs.unshift({id,title:'New Chat',msgs:[],ts:Date.now()}); |
| saveConvs(); |
| loadConv(id); |
| closeSidebar(); |
| } |
| |
| 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 saveConvs(){ localStorage.setItem('pc_convs',JSON.stringify(S.convs)) } |
| |
| 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,42)+(first.length>42?'β¦':''); |
| 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 conversations?'))return;S.convs=[];S.curId=null;S.msgs=[];saveConvs();renderMsgs();renderConvs()} |
| |
| function renderConvs(){ |
| const el=document.getElementById('tab-chats'); |
| if(!S.convs.length){el.innerHTML='<div class="empty-state">No conversations yet</div>';return} |
| el.innerHTML='<div class="group-label">Recent</div>'+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(''); |
| } |
| |
| |
| |
| |
| function upsertAgent(a){ |
| S.agents[a.name]=Object.assign(S.agents[a.name]||{},a); |
| renderAgents(); |
| } |
| function updateAgentStatus(name,status,extra={}){ |
| if(S.agents[name]) Object.assign(S.agents[name],{status,...extra}); |
| renderAgents(); |
| } |
| function clearAllAgents(){S.agents={};S.tools={};renderAgents();renderTools();showToast('Agents & tools cleared')} |
| |
| function renderAgents(){ |
| const el=document.getElementById('agents-list'); |
| const em=document.getElementById('agents-empty'); |
| const items=Object.values(S.agents); |
| em.style.display=items.length?'none':''; |
| el.innerHTML=items.map(a=>{ |
| const st=a.status||'idle'; |
| const statusLabel={working:'β‘ Workingβ¦',done:'β
Done',error:'β Error',idle:'β³ Waiting'}[st]||st; |
| const tools=(a.tools||[]).map(t=>`<span class="tool-chip" onclick="showToolDetail('${esc(t)}')">${esc(t)}</span>`).join(''); |
| return `<div class="agent-sidebar-card ${st}"> |
| <div class="asc-header" onclick="this.parentElement.classList.toggle('expanded')"> |
| <div class="asc-dot"></div> |
| <div class="asc-name">π€ ${esc(a.name)}</div> |
| <button class="asc-del" onclick="delAgent('${esc(a.name)}',event)">β</button> |
| </div> |
| <div class="asc-role">${esc(a.role||'')}</div> |
| ${a.task?`<div class="asc-task">"${esc(a.task.slice(0,80))}β¦"</div>`:''} |
| ${tools?`<div class="asc-tools">${tools}</div>`:''} |
| <div class="asc-status ${st}">${statusLabel}</div> |
| </div>`; |
| }).join(''); |
| } |
| |
| function delAgent(name,e){ |
| e.stopPropagation(); |
| delete S.agents[name]; |
| renderAgents(); |
| } |
| |
| function registerTool(spec){ |
| S.tools[spec.name]=spec; |
| renderTools(); |
| } |
| |
| function renderTools(){ |
| const el=document.getElementById('tools-list'); |
| const em=document.getElementById('tools-empty'); |
| const items=Object.values(S.tools); |
| em.style.display=items.length?'none':''; |
| el.innerHTML=items.map(t=>` |
| <div class="tool-sidebar-card"> |
| <div class="tsc-header" onclick="showToolDetail('${esc(t.name)}')"> |
| <span class="tsc-icon">π§</span> |
| <span class="tsc-name">${esc(t.name)}</span> |
| <span style="font-size:10px;color:var(--txt3)">${esc(t.agent||'')}</span> |
| </div> |
| <div class="tsc-body"> |
| <div class="tsc-desc">${esc(t.description||t.desc||'')}</div> |
| </div> |
| </div>`).join(''); |
| } |
| |
| function showToolDetail(name){ |
| const t=S.tools[name]; |
| if(!t){showToast('Tool details not available');return} |
| document.getElementById('tool-modal-title').textContent='π§ '+name; |
| document.getElementById('tool-modal-desc').textContent=t.description||t.desc||'No description'; |
| document.getElementById('tool-code-view').textContent=t.implementation||t.impl||'(built-in)'; |
| document.getElementById('tool-modal').classList.add('open'); |
| } |
| |
| |
| |
| |
| 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'); |
| } |
| |
| |
| |
| |
| 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>Multi-agent AI with dynamic sub-agent spawning, real-time tool creation, voice responses, and 320K context via LongCat Flash Lite.</p> |
| <div class="feature-badges"> |
| <div class="fbadge">π€ Dynamic Agents</div> |
| <div class="fbadge">π§ Auto Tool Creation</div> |
| <div class="fbadge">π Voice Output</div> |
| <div class="fbadge">π Real-time Tools</div> |
| <div class="fbadge">π Code Execution</div> |
| </div> |
| <div class="suggestion-grid"> |
| <div class="sc" onclick="useSuggestion(this)"><strong>π Voice Response</strong>Explain quantum entanglement and give me the answer as voice audio</div> |
| <div class="sc" onclick="useSuggestion(this)"><strong>π Date & Time</strong>What is today's date and time? Also tell me what day of the week it is</div> |
| <div class="sc" onclick="useSuggestion(this)"><strong>π¬ Deep Research</strong>Research the top 5 AI breakthroughs of 2025 and write a detailed report</div> |
| <div class="sc" onclick="useSuggestion(this)"><strong>π» Code & Run</strong>Write and execute a Python script that generates a Fibonacci sequence up to 1000</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'; |
| let bubbleInner=isUser |
| ? esc(msg.content).replace(/\n/g,'<br>') |
| : renderMD(msg.content||''); |
| row.innerHTML=` |
| <div class="av ${isUser?'uav':'aav'}">${isUser?'π€':'π€'}</div> |
| <div class="mcontent"> |
| <div class="bubble">${bubbleInner}</div> |
| ${!isUser?`<div class="mmeta"> |
| <span>${fmtTime(msg.ts)}</span> |
| <button class="mcpbtn" onclick="copyTxt(this,'${escA(msg.content)}')">π Copy</button> |
| <button class="speak-btn" onclick="speakMsg(this,'${escA(msg.content)}')">π Speak</button> |
| </div>`:'<div class="mmeta"><span>'+fmtTime(msg.ts)+'</span></div>'} |
| ${msg.audioB64?buildAudioPlayer(msg.audioB64,msg.id):''} |
| </div>`; |
| el.appendChild(row); |
| hljs.highlightAll(); |
| scrollBot(); |
| return row; |
| } |
| |
| function updateBubble(row,content){ |
| if(!row)return; |
| const b=row.querySelector('.bubble'); |
| if(!b)return; |
| b.innerHTML=renderMD(content); |
| hljs.highlightAll(); |
| scrollBot(); |
| } |
| |
| function addAudioToMsg(row,b64){ |
| if(!row)return; |
| const mc=row.querySelector('.mcontent'); |
| if(!mc||mc.querySelector('.audio-player'))return; |
| mc.insertAdjacentHTML('beforeend',buildAudioPlayer(b64,'ap-'+Date.now())); |
| } |
| |
| function buildAudioPlayer(b64,id){ |
| return `<div class="audio-player" id="apl-${id}"> |
| <button class="audio-play-btn" onclick="toggleAudio('${id}','${b64}')">βΆ</button> |
| <div class="audio-label">π Voice Response</div> |
| <div class="audio-wave" id="aw-${id}"> |
| <span style="height:40%"></span><span></span><span></span><span></span><span></span> |
| </div> |
| </div>`; |
| } |
| |
| let audioEls={}; |
| function toggleAudio(id,b64){ |
| let audio=audioEls[id]; |
| const wave=document.getElementById('aw-'+id); |
| const btn=document.querySelector(`#apl-${id} .audio-play-btn`); |
| if(!audio){ |
| const bytes=atob(b64); |
| const arr=new Uint8Array(bytes.length); |
| for(let i=0;i<bytes.length;i++)arr[i]=bytes.charCodeAt(i); |
| const blob=new Blob([arr],{type:'audio/mpeg'}); |
| audio=new Audio(URL.createObjectURL(blob)); |
| audioEls[id]=audio; |
| audio.onended=()=>{wave?.classList.remove('playing');if(btn)btn.textContent='βΆ'}; |
| } |
| if(audio.paused){audio.play();wave?.classList.add('playing');if(btn)btn.textContent='βΈ'} |
| else{audio.pause();wave?.classList.remove('playing');if(btn)btn.textContent='βΆ'} |
| } |
| |
| function speakMsg(btn,text){ |
| const clean=text.replace(/[#*`]/g,'').slice(0,500); |
| if('speechSynthesis' in window){ |
| window.speechSynthesis.cancel(); |
| const u=new SpeechSynthesisUtterance(clean); |
| u.rate=0.95;window.speechSynthesis.speak(u); |
| btn.textContent='π Speakingβ¦'; |
| u.onend=()=>{btn.textContent='π Speak'}; |
| }else{showToast('Speech synthesis not available in this browser')} |
| } |
| |
| |
| |
| |
| async function sendMessage(override){ |
| const inp=document.getElementById('msg-input'); |
| const text=override||inp.value.trim(); |
| if(!text||S.generating)return; |
| if(!S.settings.apiKey){openSettings();showToast('β οΈ Enter 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.generating=true; S.abort=new AbortController(); |
| document.getElementById('send-btn').disabled=true; |
| document.getElementById('stop-btn').style.display='flex'; |
| setLive(true,'Thinkingβ¦'); |
| clearActivity(); |
| S.actCount=0; |
| document.getElementById('ap-badge').textContent='0'; |
| document.getElementById('ap-badge').classList.remove('show'); |
| |
| let msgRow=null; |
| let started=false; |
| |
| try{ |
| const body={ |
| messages:S.msgs.slice(0,-1).map(m=>({role:m.role,content:m.content})).concat([{role:'user',content:text}]), |
| api_key:S.settings.apiKey, |
| model:S.settings.model||'LongCat-Flash-Lite', |
| temperature:S.settings.temperature||0.7, |
| }; |
| const resp=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body),signal:S.abort.signal}); |
| if(!resp.ok){const e=await resp.json().catch(()=>({detail:resp.statusText}));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} |
| handleEvent(ev); |
| } |
| } |
| }catch(e){ |
| if(e.name!=='AbortError'){ |
| typing?.remove(); |
| am.content='β '+e.message; |
| if(!started)msgRow=appendMsg(am); |
| else updateBubble(msgRow,am.content); |
| } |
| }finally{ |
| S.generating=false;S.abort=null; |
| document.getElementById('send-btn').disabled=false; |
| document.getElementById('stop-btn').style.display='none'; |
| setLive(false,'Ready'); |
| syncConv();scrollBot(); |
| } |
| |
| function handleEvent(ev){ |
| switch(ev.type){ |
| case 'thinking': |
| setLive(true,ev.text); |
| addActivity({type:'thinking',icon:'π§ ',html:`<strong>Planning:</strong> ${esc(ev.text)}`}); |
| break; |
| case 'step': |
| setLive(true,ev.text); |
| addActivity({type:'step',icon:'βΆ',html:esc(ev.text)}); |
| break; |
| case 'tool_call': |
| setLive(true,`Calling ${ev.tool}β¦`); |
| addActivity({type:'tool_call',icon:'π§',html:`Calling built-in tool <code>${esc(ev.tool)}</code>`}); |
| break; |
| case 'tool_result': |
| addActivity({type:'tool_result',icon:'β
',html:`<strong>${esc(ev.tool)}</strong> returned:`,result:ev.result}); |
| break; |
| case 'agent_created': |
| upsertAgent({name:ev.name,role:ev.role,goal:ev.goal||'',tools:ev.tools||[],status:'idle'}); |
| (ev.tool_specs||[]).forEach(t=>registerTool({...t,agent:ev.name})); |
| addActivity({type:'thinking',icon:'π€',html:`Created agent <strong>${esc(ev.name)}</strong> (${esc(ev.role)}) with tools: ${(ev.tools||[]).map(t=>`<code>${esc(t)}</code>`).join(', ')||'none'}`}); |
| switchTab('agents',document.querySelectorAll('.stab')[1]); |
| break; |
| case 'tool_building': |
| setLive(true,`${ev.agent}: building ${ev.tool}β¦`); |
| addActivity({type:'tool_call',icon:'π¨',html:`<strong>${esc(ev.agent)}</strong> building tool <code>${esc(ev.tool)}</code>: ${esc(ev.description||'')}`}); |
| break; |
| case 'tool_ready': |
| addActivity({type:'tool_result',icon:ev.error?'β':'β
',html:`Tool <code>${esc(ev.tool)}</code> ${ev.error?'failed: '+esc(ev.error):'ready'}`}); |
| break; |
| case 'agent_working': |
| updateAgentStatus(ev.name,'working',{task:ev.task}); |
| setLive(true,`${ev.name} workingβ¦`); |
| addActivity({type:'agent_working',icon:'β‘',html:`<strong>${esc(ev.name)}</strong>: ${esc(ev.task)}`}); |
| break; |
| case 'agent_done': |
| updateAgentStatus(ev.name,'done'); |
| addActivity({type:'agent_done',icon:'β
',html:`<strong>${esc(ev.name)}</strong> completed`,result:ev.preview}); |
| break; |
| case 'agent_error': |
| updateAgentStatus(ev.name,'error'); |
| addActivity({type:'agent_error',icon:'β',html:`<strong>${esc(ev.name)}</strong> error: ${esc(ev.error)}`}); |
| 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': |
| am.audioB64=ev.audio_b64; |
| if(msgRow)addAudioToMsg(msgRow,ev.audio_b64); |
| addActivity({type:'tool_result',icon:'π',html:'Voice audio generated and attached to response'}); |
| break; |
| case 'voice_fallback': |
| |
| if(ev.text){ |
| const u=new SpeechSynthesisUtterance(ev.text.slice(0,500)); |
| u.rate=0.95; window.speechSynthesis?.speak(u); |
| addActivity({type:'tool_result',icon:'π',html:'Voice response playing via browser TTS'}); |
| } |
| break; |
| case 'done': |
| if(!started){typing?.remove();msgRow=appendMsg(am)} |
| break; |
| case 'error': |
| typing?.remove(); |
| am.content='β '+ev.message; |
| if(!started){msgRow=appendMsg(am)}else{updateBubble(msgRow,am.content)} |
| addActivity({type:'agent_error',icon:'β',html:esc(ev.message)}); |
| break; |
| } |
| } |
| } |
| |
| function stopGen(){if(S.abort)S.abort.abort()} |
| |
| |
| |
| |
| function addActivity({type,icon,html,result}){ |
| const el=document.getElementById('ap-body'); |
| const now=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}); |
| const resultHTML=result?`<div class="act-result">${esc(result)}</div>`:''; |
| el.insertAdjacentHTML('beforeend',` |
| <div class="act-item ${type}"> |
| <div class="act-icon">${icon}</div> |
| <div class="act-content">${html}${resultHTML}</div> |
| <div class="act-time">${now}</div> |
| </div>`); |
| el.scrollTop=el.scrollHeight; |
| |
| S.actCount++; |
| const badge=document.getElementById('ap-badge'); |
| badge.textContent=S.actCount; |
| badge.classList.add('show'); |
| } |
| |
| function clearActivity(){document.getElementById('ap-body').innerHTML=''} |
| |
| function setLive(working,text){ |
| const dot=document.getElementById('live-dot'); |
| const txt=document.getElementById('live-text'); |
| dot.className='live-dot'+(working?' working':''); |
| txt.textContent=text; |
| } |
| |
| function toggleActivity(){ |
| const p=document.getElementById('activity-panel'); |
| p.classList.toggle('hidden'); |
| document.getElementById('ap-badge').classList.remove('show'); |
| S.actCount=0; |
| } |
| |
| 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="mcontent"><div class="bubble"><div class="typing-dots"><span></span><span></span><span></span></div></div></div>'; |
| el.appendChild(row);scrollBot(); |
| return row; |
| } |
| |
| |
| |
| |
| function toggleSidebar(){ |
| const sb=document.getElementById('sidebar'); |
| sb.classList.toggle('open'); |
| } |
| function closeSidebar(){document.getElementById('sidebar').classList.remove('open')} |
| |
| |
| |
| |
| function toggleModelDd(){document.getElementById('model-dd').classList.toggle('open')} |
| function selectModel(id,name,el){ |
| S.settings.model=id; |
| document.getElementById('cur-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'); |
| saveSettingsStore();showToast('Model: '+name); |
| } |
| document.addEventListener('click',e=>{ |
| if(!e.target.closest('#model-badge'))document.getElementById('model-dd')?.classList.remove('open'); |
| }); |
| |
| |
| |
| |
| function openSettings(){ |
| document.getElementById('s-apikey').value=S.settings.apiKey||''; |
| document.getElementById('s-temp').value=S.settings.temperature||0.7; |
| document.getElementById('s-temp-val').textContent=S.settings.temperature||0.7; |
| document.getElementById('s-sysprompt').value=S.settings.systemPrompt||''; |
| document.getElementById('settings-modal').classList.add('open'); |
| } |
| function saveSettings(){ |
| S.settings.apiKey=document.getElementById('s-apikey').value.trim(); |
| S.settings.temperature=parseFloat(document.getElementById('s-temp').value); |
| S.settings.systemPrompt=document.getElementById('s-sysprompt').value.trim(); |
| saveSettingsStore();closeModal('settings-modal');showToast('β
Settings saved'); |
| } |
| function saveSettingsStore(){localStorage.setItem('pc_settings',JSON.stringify(S.settings))} |
| function applySettings(){ |
| const s=S.settings; |
| if(s.model){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('cur-model-name');if(el)el.textContent=names[s.model]||s.model;} |
| } |
| |
| |
| |
| |
| function closeModal(id){document.getElementById(id).classList.remove('open')} |
| function closeModalOutside(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(text){ |
| if(!text)return''; |
| marked.setOptions({breaks:true,gfm:true}); |
| let h=marked.parse(text); |
| h=h.replace(/<pre><code(.*?)>([\s\S]*?)<\/code><\/pre>/g,(_,attrs,code)=>{ |
| const lang=(attrs.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${attrs}>${code}</code></pre></div>`; |
| }); |
| return h; |
| } |
| function copyCode(btn){ |
| const code=btn.closest('.cbw').querySelector('code').innerText; |
| navigator.clipboard.writeText(code).then(()=>{btn.textContent='Copied!';setTimeout(()=>btn.textContent='Copy',2000)}); |
| } |
| function copyTxt(btn,text){ |
| navigator.clipboard.writeText(text).then(()=>{btn.textContent='β
';setTimeout(()=>btn.textContent='π Copy',2000)}); |
| } |
| 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,' ')} |
| 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();sendMessage()}} |
| function autoResize(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,160)+'px'} |
| function useSuggestion(el){const t=el.querySelector('strong').nextSibling?.textContent?.trim()||el.textContent.trim();sendMessage(t)} |
| function showToast(msg){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2600)} |
| |
| |
| |
| |
| const savedTheme=localStorage.getItem('pc_theme'); |
| if(savedTheme)document.documentElement.setAttribute('data-theme',savedTheme); |
| |
| if(window.innerWidth<900)document.getElementById('activity-panel').classList.add('hidden'); |
| init(); |
| </script> |
| </body> |
| </html> |