PraisonAI / app /static /index.html
Sanyam400's picture
Update app/static/index.html
8813d72 verified
raw
history blame
58.6 kB
<!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">
<!-- ── Left Sidebar ──────────────────────────────────────── -->
<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>
<!-- ── Main ──────────────────────────────────────────────── -->
<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>
<!-- ── Activity Panel ────────────────────────────────────── -->
<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><!-- #app -->
<!-- ── Settings Modal ────────────────────────────────────── -->
<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>
<!-- ── Tool Detail Modal ─────────────────────────────────── -->
<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>
// ════════════════════════════════════════════════════════
// STATE
// ════════════════════════════════════════════════════════
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: {}, // name -> {name,role,goal,tools,status,task,result}
tools: {}, // name -> {name,desc,impl,agent,builtin}
actCount: 0,
};
// ════════════════════════════════════════════════════════
// INIT
// ════════════════════════════════════════════════════════
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){}
}
// ════════════════════════════════════════════════════════
// CONVERSATIONS
// ════════════════════════════════════════════════════════
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('');
}
// ════════════════════════════════════════════════════════
// AGENTS & TOOLS SIDEBAR
// ════════════════════════════════════════════════════════
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');
}
// ════════════════════════════════════════════════════════
// TABS
// ════════════════════════════════════════════════════════
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');
}
// ════════════════════════════════════════════════════════
// RENDER MESSAGES
// ════════════════════════════════════════════════════════
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')}
}
// ════════════════════════════════════════════════════════
// SEND & STREAM
// ════════════════════════════════════════════════════════
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();
// hide welcome
const w=document.getElementById('welcome');
if(w)w.style.display='none';
// user msg
const um={id:uid(),role:'user',content:text,ts:Date.now()};
S.msgs.push(um); appendMsg(um);
inp.value=''; autoResize(inp); syncConv();
// assistant placeholder
const am={id:uid(),role:'assistant',content:'',ts:Date.now()};
S.msgs.push(am);
// typing indicator
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':
// Use browser TTS as 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()}
// ════════════════════════════════════════════════════════
// ACTIVITY PANEL
// ════════════════════════════════════════════════════════
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;
// Badge
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;
}
// ════════════════════════════════════════════════════════
// SIDEBAR TOGGLE
// ════════════════════════════════════════════════════════
function toggleSidebar(){
const sb=document.getElementById('sidebar');
sb.classList.toggle('open');
}
function closeSidebar(){document.getElementById('sidebar').classList.remove('open')}
// ════════════════════════════════════════════════════════
// MODEL DROPDOWN
// ════════════════════════════════════════════════════════
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');
});
// ════════════════════════════════════════════════════════
// SETTINGS
// ════════════════════════════════════════════════════════
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;}
}
// ════════════════════════════════════════════════════════
// MODALS
// ════════════════════════════════════════════════════════
function closeModal(id){document.getElementById(id).classList.remove('open')}
function closeModalOutside(e,id){if(e.target===document.getElementById(id))closeModal(id)}
// ════════════════════════════════════════════════════════
// THEME
// ════════════════════════════════════════════════════════
function toggleTheme(){
const t=document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark';
document.documentElement.setAttribute('data-theme',t);
localStorage.setItem('pc_theme',t);
}
// ════════════════════════════════════════════════════════
// HELPERS
// ════════════════════════════════════════════════════════
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
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)}
// ════════════════════════════════════════════════════════
// BOOT
// ════════════════════════════════════════════════════════
const savedTheme=localStorage.getItem('pc_theme');
if(savedTheme)document.documentElement.setAttribute('data-theme',savedTheme);
// Start with activity panel visible on wide screens
if(window.innerWidth<900)document.getElementById('activity-panel').classList.add('hidden');
init();
</script>
</body>
</html>