@buzzie-ai/jannal 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/bin/jannal.js +3 -0
- package/lib/plugins.js +67 -0
- package/lib/tokens.js +176 -0
- package/package.json +52 -0
- package/public/assets/index-B8dfyj9-.css +1 -0
- package/public/assets/index-CzXZ1AkJ.js +23 -0
- package/public/favicon.png +0 -0
- package/public/index.html +144 -0
- package/public/jannal-1.png +0 -0
- package/public/logo.png +0 -0
- package/server.js +1142 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
var e=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var t={connected:!1,reqs:[],selectedReq:null,profiles:{},activeProfile:`All Tools`,premium:!1,routerMode:`off`,toolsUsed:new Set,groups:{},groupView:!0,expandedGroups:{}},n={segment:null,segIndex:null,view:`formatted`,fullContent:``,parsedTools:null,loading:!1},r={system:`#60A5FA`,tools:`#FB923C`,message:`#22D3EE`,assistant:`#34D399`,tool_result:`#FBBF24`,tool_use:`#A78BFA`},i={system:`--seg-system`,tools:`--seg-tools`,message:`--seg-message`,assistant:`--seg-assistant`,tool_result:`--seg-tool-result`,tool_use:`--seg-tool-use`};function a(e){let t=i[e];if(t){let e=getComputedStyle(document.documentElement).getPropertyValue(t).trim();if(e)return e}return r[e]||`#64748B`}var o=e(((e,t)=>{var n=3.8;function r(e){if(!e)return 0;let t=typeof e==`string`?e:JSON.stringify(e);return Math.ceil(t.length/n)}function i(e){return r(e)}var a={"gpt-4o":128e3,"gpt-4-turbo":128e3,"gpt-3.5":16385,gemini:1e6},o=[128e3,2e5,1e6];function s(e){if(!e)return 2e5;let t=e.toLowerCase();if(t.includes(`1m`)||t.includes(`opus-4-5`)||t.includes(`opus-4-6`)||t.includes(`opus-4.5`)||t.includes(`opus-4.6`))return 1e6;if(t.includes(`claude`))return 2e5;for(let[e,n]of Object.entries(a))if(t.includes(e))return n;return 2e5}function c(e,t){let n=s(e);if(t<=n)return n;for(let e of o)if(t<=e)return e;return Math.max(n,t)}function l(e){let t=[],n=e.model||`unknown`;if(e.system){let n=typeof e.system==`string`?e.system:JSON.stringify(e.system,null,2);t.push({type:`system`,name:`System Prompt`,tokens:r(n),charLength:n.length})}if(e.tools&&e.tools.length>0){let n=JSON.stringify(e.tools);t.push({type:`tools`,name:`Tools (${e.tools.length})`,tokens:r(n),count:e.tools.length})}if(e.messages)for(let n=0;n<e.messages.length;n++){let i=e.messages[n],a=typeof i.content==`string`?i.content:JSON.stringify(i.content),o=Array.isArray(i.content)&&i.content.some(e=>e.type===`tool_result`),s=Array.isArray(i.content)&&i.content.some(e=>e.type===`tool_use`),c=`message`,l=`${i.role} message`;o?(c=`tool_result`,l=`Tool Result`):s&&(c=`tool_use`,l=`Tool Use (assistant)`),t.push({type:c,role:i.role,name:l,tokens:r(a),charLength:a.length,index:n})}let i=t.reduce((e,t)=>e+t.tokens,0);return{segments:t,totalEstimatedTokens:i,budget:c(n,i)}}t.exports={CHARS_PER_TOKEN:n,estimateTokens:r,estimateToolTokens:i,getBudget:s,inferBudget:c,MODEL_BUDGETS:a,CONTEXT_TIERS:o,analyzeSegments:l}}))();function s(e){return e.type===`message`&&e.role===`assistant`?a(`assistant`):e.type===`message`?a(`message`):a(e.type)}function c(e){return e.type===`message`?e.role===`user`?`User Message`:`Assistant Message`:e.type===`tool_result`?`Tool Result`:e.type===`tool_use`?`Tool Use`:e.type===`system`?`System Prompt`:e.type===`tools`?`Tool Definitions`:e.type}function l(e){return e>=1e6?(e/1e6).toFixed(1)+`M`:e>=1e3?(e/1e3).toFixed(1)+`k`:e.toString()}function u(e){return e>=1?`$`+e.toFixed(2):e>=.01?`$`+e.toFixed(3):`$`+e.toFixed(4)}function d(e){return e?e.includes(`opus-4-6`)||e.includes(`opus-4.6`)||e.includes(`opus-4-5`)||e.includes(`opus-4.5`)?5:e.includes(`opus`)?15:e.includes(`haiku-4`)?1:e.includes(`haiku`)?.8:3:3}function f(e){let t=document.createElement(`div`);return t.textContent=e,t.innerHTML}function p(e,t,n){return n||!t||!t.tools||t.tools.length===0?!0:t.mode===`blocklist`?!t.tools.includes(e):t.tools.includes(e)}function m(e){let t=e?.name||``;if(t.startsWith(`mcp__`)){let e=t.split(`__`);if(e.length>=3){let t=e[1].split(`_`);return t[t.length-1].toLowerCase()}}let n=t.match(/^([a-zA-Z0-9]+)[_\/]/);return n?n[1].toLowerCase():`other`}function h(e){let t=new Map;for(let n of e){let e=m(n);t.has(e)||t.set(e,[]),t.get(e).push(n)}let n=new Map,r=[...t.keys()].sort((e,t)=>e===`other`?1:t===`other`?-1:e.localeCompare(t));for(let e of r)n.set(e,t.get(e));return n}var g=`jannal_session`,_=`jannal_daily_costs`,v=`jannal_daily_savings`,y=500,b=null;function x(e){b&&clearTimeout(b),b=setTimeout(()=>{try{let t={reqs:e.reqs,selectedReq:e.selectedReq,groupView:e.groupView,savedAt:Date.now()};localStorage.setItem(g,JSON.stringify(t))}catch(e){console.warn(`Failed to persist session:`,e.message)}b=null},y)}function ee(e){try{let t=localStorage.getItem(g);if(!t)return!1;let n=JSON.parse(t),r=n.reqs||n.turns,i=n.selectedReq??n.selectedTurn;if(r&&Array.isArray(r)&&r.length>0){if(e.reqs=r,e.selectedReq=i!=null&&i<e.reqs.length?i:e.reqs.length-1,n.groupView!=null&&(e.groupView=n.groupView),e.toolsUsed){e.toolsUsed.clear();for(let t of e.reqs)t.toolsUsed?.length&&t.toolsUsed.forEach(t=>e.toolsUsed.add(t))}return!0}}catch(e){console.warn(`Failed to restore session:`,e.message)}return!1}function te(e){let t=0,n=e.reqs.map(e=>{let n=e.actualCost?.totalCost??e.estimatedCost?.totalCost??0;return t+=n,{request:e.turn,model:e.model,timestamp:e.timestamp,inputTokens:e.actualUsage?.input_tokens??e.totalEstimatedTokens,outputTokens:e.actualUsage?.output_tokens??0,cost:n,segments:e.segments?.map(e=>({name:e.name,type:e.type,tokens:e.tokens}))??[]}}),r={exportedAt:new Date().toISOString(),requestCount:n.length,totalCost:t,requests:n};return JSON.stringify(r,null,2)}function S(e){let t=[`Request`,`Model`,`Timestamp`,`Input Tokens`,`Output Tokens`,`Cost ($)`],n=e.reqs.map(e=>[e.turn,e.model,new Date(e.timestamp).toISOString(),e.actualUsage?.input_tokens??e.totalEstimatedTokens??``,e.actualUsage?.output_tokens??0,(e.actualCost?.totalCost??e.estimatedCost?.totalCost??0).toFixed(4)]);return[t.join(`,`),...n.map(e=>e.map((e,t)=>t===5?e:`"${String(e)}"`).join(`,`))].join(`
|
|
2
|
+
`)}function ne(e){if(!(!e||e<=0))try{let t=new Date().toISOString().slice(0,10),n=JSON.parse(localStorage.getItem(_)||`{}`);n[t]=(n[t]||0)+e,localStorage.setItem(_,JSON.stringify(n))}catch{}}function re(){try{let e=new Date().toISOString().slice(0,10);return JSON.parse(localStorage.getItem(_)||`{}`)[e]||0}catch{return 0}}function ie(e,t){if(!((!e||e<=0)&&(!t||t<=0)))try{let n=new Date().toISOString().slice(0,10),r=JSON.parse(localStorage.getItem(v)||`{}`);r[n]||(r[n]={cost:0,tokens:0}),typeof r[n]==`number`&&(r[n]={cost:r[n],tokens:0}),r[n].cost+=e||0,r[n].tokens+=t||0,localStorage.setItem(v,JSON.stringify(r))}catch{}}function ae(){try{let e=new Date().toISOString().slice(0,10),t=JSON.parse(localStorage.getItem(v)||`{}`)[e];return t?typeof t==`number`?{cost:t,tokens:0}:{cost:t.cost||0,tokens:t.tokens||0}:{cost:0,tokens:0}}catch{return{cost:0,tokens:0}}}function C(e,t){let n=new Blob([e],{type:`application/octet-stream`}),r=URL.createObjectURL(n),i=document.createElement(`a`);i.href=r,i.download=t,i.click(),URL.revokeObjectURL(r)}function w(e={}){E(),D(),oe(),k(),e.skipDetail||M(),T()}function T(){let e=document.getElementById(`exportBtn`);e&&(e.disabled=t.reqs.length===0,e.title=t.reqs.length===0?`No data to export`:`Export session as JSON or CSV`)}function E(){document.getElementById(`statusDot`).className=`status-dot ${t.connected?`connected`:`disconnected`}`;let e=document.getElementById(`statusText`);e.textContent=t.connected?`Connected`:`Disconnected`,e.style.color=t.connected?`var(--green)`:`var(--red)`;let n=document.getElementById(`reqBadge`);n&&(n.textContent=`Req ${t.reqs.length}`);let r=re(),i=document.getElementById(`dailyCost`);i&&(i.textContent=`Cost: ${u(r)}`);let a=document.getElementById(`dailySaved`);if(a)if(!t.premium)a.textContent=`Saved: Pro`,a.className=`daily-saved premium-locked`,a.title=`Savings intelligence requires Pro`;else{let{cost:e,tokens:t}=ae(),n=t>0?` (${l(t)})`:``;a.textContent=`Saved: ${u(e)}${n}`,a.className=`daily-saved`,a.classList.toggle(`has-savings`,e>0),a.title=`Estimated daily savings from router intelligence`}let o=document.getElementById(`routerBadge`);if(o)if(t.premium){let e=t.routerMode||`off`;o.textContent={off:`Router Off`,shadow:`Router Shadow`,auto:`Router Auto`}[e]||`Router`,o.className=`router-badge router-badge--${e}`;let n=document.getElementById(`routerPopover`);if(n)for(let t of n.querySelectorAll(`.router-popover-opt`))t.classList.toggle(`active`,t.dataset.mode===e),t.classList.remove(`premium-locked`)}else{o.textContent=`Router Pro`,o.className=`router-badge premium-locked`;let e=document.getElementById(`routerPopover`);if(e)for(let t of e.querySelectorAll(`.router-popover-opt`))t.classList.add(`premium-locked`),t.classList.remove(`active`)}}function D(){let e=t.selectedReq===null?null:t.reqs[t.selectedReq],n=document.getElementById(`barInner`),r=document.getElementById(`barOuter`);if(!e){n.innerHTML=`<div class="bar-empty"><span>No data yet</span></div>`,r.className=`bar-outer`,document.getElementById(`barLegend`).innerHTML=``,document.getElementById(`barTotal`).textContent=`0 / 0`,document.getElementById(`barPct`).textContent=`0%`;return}let i=e.budget,a=e.actualUsage?e.actualUsage.input_tokens:e.totalEstimatedTokens,o=a/i*100;r.className=`bar-outer`+(o>95?` pressure-critical`:o>80?` pressure-high`:``);let u=[];for(let t=0;t<e.segments.length;t++){let n=e.segments[t],r=s(n),i=u[u.length-1];i&&i.color===r?(i.tokens+=n.tokens,i.count++,i.endIndex=t):u.push({color:r,tokens:n.tokens,name:n.name,count:1,startIndex:t,endIndex:t})}let d=``;for(let e of u){let t=e.tokens/i*100;if(t<.1)continue;let n=e.count>1?`${e.name} (×${e.count})`:e.name;d+=`<div class="bar-segment" style="width:${t}%;background:linear-gradient(180deg,${e.color}cc,${e.color}88);border-right:1.5px solid var(--bg3)" title="${n}: ${l(e.tokens)} tokens" onclick="openModal(${e.startIndex})">`,t>5&&(d+=`<span>${t>15?n:l(e.tokens)}</span>`),d+=`</div>`}o<100&&(d+=`<div class="bar-empty"><span>${l(i-a)} free</span></div>`),n.innerHTML=d;let f=new Map;for(let t of e.segments){let e=c(t),n=s(t);f.has(e)||f.set(e,n)}document.getElementById(`barLegend`).innerHTML=Array.from(f.entries()).map(([e,t])=>`<div class="legend-item"><div class="legend-dot" style="background:${t}"></div>${e}</div>`).join(``);let p=e.actualUsage?l(e.actualUsage.input_tokens):e.tokenCountSource===`count_tokens`?l(e.totalEstimatedTokens):`~${l(e.totalEstimatedTokens)}`,m=document.getElementById(`barTotal`),h=document.getElementById(`barPct`);m.textContent=`${p} / ${l(i)}`,m.style.color=o>95?`var(--red)`:o>80?`var(--orange)`:`var(--text)`,h.textContent=`${o.toFixed(1)}%`,h.style.color=o>95?`var(--red)`:o>80?`var(--orange)`:`var(--text3)`}function oe(){let e=document.getElementById(`tokenChartContainer`),n=document.getElementById(`tokenChart`);if(!e||!n)return;if(t.reqs.length<2){e.style.display=`none`;return}e.style.display=`block`;let r=t.reqs.map(e=>e.actualUsage?.input_tokens??e.totalEstimatedTokens??0),i=Math.max(...r),a=Math.min(...r),o=i-a||1;n.innerHTML=`
|
|
3
|
+
<svg viewBox="0 0 200 36" preserveAspectRatio="none" class="token-chart-svg">
|
|
4
|
+
<polyline
|
|
5
|
+
fill="none"
|
|
6
|
+
stroke="var(--cyan)"
|
|
7
|
+
stroke-width="2"
|
|
8
|
+
stroke-linecap="round"
|
|
9
|
+
stroke-linejoin="round"
|
|
10
|
+
points="${r.map((e,t)=>`${t/(r.length-1)*200},${36-(e-a)/o*32-2}`).join(` `)}"
|
|
11
|
+
/>
|
|
12
|
+
</svg>
|
|
13
|
+
<div class="token-chart-hint">${r.length} reqs · ${l(a)} → ${l(i)} tokens</div>
|
|
14
|
+
`}function O(){return`ANTHROPIC_BASE_URL=http://localhost:${location.port===`5173`?`4455`:location.port||`4455`} claude`}function se(){navigator.clipboard.writeText(O()).then(()=>{let e=document.getElementById(`copyCommandBtn`);if(e){let t=e.textContent;e.textContent=`Copied!`,e.style.color=`var(--green)`,setTimeout(()=>{e.textContent=t,e.style.color=``},1500)}})}function k(){let e=document.getElementById(`reqList`);if(t.reqs.length===0){e.innerHTML=`<div class="empty"><div class="empty-icon waiting">🔍</div><h2>Waiting for requests...</h2><p>Start Claude Code with:<br><code style="color:var(--cyan);font-size:11px">${O()}</code> <button id="copyCommandBtn" class="copy-command-btn" onclick="copyClaudeCommand()" title="Copy to clipboard">Copy</button></p></div>`;return}let n=document.getElementById(`viewToggleBtn`);n&&(n.textContent=t.groupView?`Grouped`:`Flat`,n.classList.toggle(`active`,t.groupView));let r=e.scrollTop;t.groupView&&Object.keys(t.groups).length>0?j(e):ce(e),e.scrollTop=r}function ce(e){let n=``;for(let e=t.reqs.length-1;e>=0;e--)n+=A(e);e.innerHTML=n}function A(e){let n=t.reqs[e],r=n.actualUsage?n.actualUsage.input_tokens:n.totalEstimatedTokens,i=Math.min(r/n.budget*100,100),a=i>95?`var(--red)`:i>80?`var(--orange)`:`var(--green)`,o=n.model.replace(`claude-`,``).replace(/-\d{8,}$/,``),s=n.actualUsage?l(n.actualUsage.input_tokens):n.tokenCountSource===`count_tokens`?l(n.totalEstimatedTokens):`~`+l(n.totalEstimatedTokens),c=``;n.actualUsage&&(c=`${l(n.actualUsage.input_tokens)} in / ${l(n.actualUsage.output_tokens)} out`);let d=n.actualCost?u(n.actualCost.totalCost):n.estimatedCost?`~`+u(n.estimatedCost.totalCost):``,f=`<div class="req-card${e===t.selectedReq?` selected`:``}" onclick="selectReq(${e})">`;return f+=`<div class="req-card-head"><span class="req-label">Req ${n.turn}</span><span class="req-tokens" style="color:${a}">${s}</span></div>`,f+=`<div class="req-mini-bar"><div class="req-mini-fill" style="width:${i}%;background:${a}"></div></div>`,f+=`<div class="req-meta"><span>${o}</span>`,c&&(f+=`<span class="req-io">${c}</span>`),d&&(f+=`<span class="req-cost-inline">${d}</span>`),f+=`</div>`,f+=`</div>`,f}function j(e){let n=Object.keys(t.groups).map(Number).sort((e,t)=>t-e),r=``;for(let e of n){let n=t.groups[e],i=t.expandedGroups[e]!==!1,a=0,o=0;for(let e of n.reqIndices){let n=t.reqs[e];n&&(n.actualCost?a+=n.actualCost.totalCost:n.estimatedCost&&(a+=n.estimatedCost.totalCost),o+=n.actualUsage?.input_tokens??n.totalEstimatedTokens??0)}let s=n.reqIndices.length,c=Object.keys(n.sessions),d=c.length>1,f=e+1;if(r+=`<div class="group-card">`,r+=`<div class="group-header" onclick="toggleGroup(${e})">`,r+=`<span class="group-chevron ${i?`expanded`:``}">▶</span>`,r+=`<span class="group-title">Turn ${f}</span>`,r+=`<div class="group-summary">`,r+=`<span class="group-tokens">${l(o)}</span>`,r+=`<span class="group-cost">${u(a)}</span>`,r+=`<span class="group-req-count">${s} req${s===1?``:`s`}</span>`,r+=`</div></div>`,r+=`<div class="group-children ${i?``:`collapsed`}">`,d){let e=c.sort((e,t)=>{let r=n.sessions[e].reqIndices.length;return n.sessions[t].reqIndices.length-r}),t=0;for(let i of e){let e=n.sessions[i],a=e.model||`unknown`,o=t===0,s=o?`Main`:`Subagent`,c=o?`main`:`subagent`;r+=`<div class="group-session-label">`,r+=`<span class="session-pill ${c}">${s}</span>`,r+=`<span>${a} · ${e.reqIndices.length} req${e.reqIndices.length===1?``:`s`}</span>`,r+=`</div>`;for(let t=e.reqIndices.length-1;t>=0;t--)r+=A(e.reqIndices[t]);t++}}else for(let e=n.reqIndices.length-1;e>=0;e--)r+=A(n.reqIndices[e]);let p=new Date(n.startTime).toLocaleTimeString(),m=new Date(n.endTime).toLocaleTimeString(),h=p===m?p:`${p} – ${m}`;r+=`<div class="group-time">${h}</div>`,r+=`</div></div>`}e.innerHTML=r}function M(){let e=document.getElementById(`detailBody`),n=document.getElementById(`detailTitle`),r=document.getElementById(`detailMeta`);if(t.selectedReq===null||!t.reqs[t.selectedReq]){n.textContent=`Segment Breakdown`,r.textContent=``,e.innerHTML=`<div class="empty"><div class="empty-icon">📊</div><h2>No request selected</h2><p>Click a request on the left to see its context breakdown.</p></div>`;return}let i=t.reqs[t.selectedReq];n.textContent=`Req ${i.turn} — Segment Breakdown`,r.textContent=`${i.model} | ${i.segments.length} segments | ${i.messageCount} messages`;let a=``;a+=`<div class="stats-grid">`;let o=i.segments?.find(e=>e.type===`system`);if(o&&i.budget){let e=o.tokens/i.budget*100;e>15&&(a+=`<div class="warning-box">`,a+=`<div class="warning-box-title">System prompt is large</div>`,a+=`<div class="usage-row"><span class="usage-label">System prompt</span><span class="usage-value" style="color:var(--orange)">${l(o.tokens)} tokens (${e.toFixed(1)}% of context)</span></div>`,a+=`<div style="margin-top:6px;font-size:10px;color:var(--text3)">Consider trimming to free context for conversation.</div>`,a+=`</div>`)}if(i.filteringActive&&i.removedTools&&i.removedTools.length>0&&(a+=`<div class="filter-box">`,a+=`<div class="filter-box-title">Filtering Active</div>`,a+=`<div class="usage-row"><span class="usage-label">Original tools</span><span class="usage-value" style="color:var(--text2)">${i.originalToolCount}</span></div>`,a+=`<div class="usage-row"><span class="usage-label">After filtering</span><span class="usage-value" style="color:var(--green)">${i.filteredToolCount}</span></div>`,a+=`<div class="usage-row"><span class="usage-label">Removed</span><span class="usage-value" style="color:var(--orange)">${i.removedTools.length} tools</span></div>`,i.tokensSaved&&(a+=`<div class="usage-row"><span class="usage-label">Tokens saved</span><span class="usage-value" style="color:var(--green)">~${l(i.tokensSaved)}</span></div>`),a+=`</div>`),!t.premium)a+=`<div class="router-box premium-locked">`,a+=`<div class="router-box-title">Router Intelligence</div>`,a+=`<div class="premium-locked-msg">Intelligent routing, savings analysis, and auto-filtering.<br>Available in Pro.</div>`,a+=`</div>`;else if(i.router){let e=i.router,t=e.mode===`shadow`?`Shadow (observe only)`:e.mode===`auto`?`Auto`:e.mode||`off`;if(a+=`<div class="router-box">`,a+=`<div class="router-box-title">Router Decision</div>`,a+=`<div class="usage-row"><span class="usage-label">Mode</span><span class="usage-value router-mode-${e.mode}">${t}</span></div>`,e.eligible){let t=e.mode===`shadow`;if(a+=`<div class="usage-row"><span class="usage-label">Matched by</span><span class="usage-value" style="color:var(--cyan)">${f(e.matched_by||`—`)}</span></div>`,e.confidence!=null){let t=e.confidence>=.9?`var(--green)`:e.confidence>=.7?`var(--amber)`:`var(--orange)`;a+=`<div class="usage-row"><span class="usage-label">Confidence</span><span class="usage-value" style="color:${t}">${(e.confidence*100).toFixed(0)}%</span></div>`}if(e.selected_groups&&e.selected_groups.length>0){let n=e.selected_groups.filter(e=>e!==`core`).join(`, `)||`—`;a+=`<div class="usage-row"><span class="usage-label">${t?`Would keep`:`Selected groups`}</span><span class="usage-value" style="color:var(--text2);font-size:10px">${f(n)}</span></div>`}if(e.stripped_groups&&e.stripped_groups.length>0&&(a+=`<div class="usage-row"><span class="usage-label">${t?`Would strip`:`Stripped groups`}</span><span class="usage-value" style="color:var(--text3);font-size:10px">${f(e.stripped_groups.join(`, `))}</span></div>`),e.estimated_tokens_saved>0){let n=i.totalEstimatedTokens>0?(e.estimated_tokens_saved/i.totalEstimatedTokens*100).toFixed(1):`?`;a+=`<div class="usage-row"><span class="usage-label">${t?`Potential savings`:`Est. savings`}</span><span class="usage-value" style="color:var(--green)">~${l(e.estimated_tokens_saved)} tokens (${n}%)</span></div>`}e.sticky_reused&&(a+=`<div style="margin-top:4px;font-size:9px;color:var(--purple)">Sticky route reused</div>`)}else{let t=e.skip_reason===`router_off`?`Router is off`:e.skip_reason===`below_threshold`?`Below threshold`:e.skip_reason===`no_request_data`?`No request data`:e.skip_reason||`Skipped`;a+=`<div class="usage-row"><span class="usage-label">Status</span><span class="usage-value" style="color:var(--text3)">${f(t)}</span></div>`}e.mode===`shadow`&&(a+=`<div class="router-shadow-note">All tools forwarded — shadow mode</div>`),a+=`</div>`}if(i.actualUsage){let e=i.actualUsage,t=e.cache_read_input_tokens||0,n=e.cache_creation_input_tokens||0,r=t>0||n>0,o=e.input_tokens-i.totalEstimatedTokens,s=e.input_tokens?(o/e.input_tokens*100).toFixed(1):`0.0`;if(a+=`<div class="usage-box">`,a+=`<div class="usage-row"><span class="usage-label">Estimated input</span><span class="usage-value estimated">~${i.totalEstimatedTokens.toLocaleString()}</span></div>`,a+=`<div class="usage-row"><span class="usage-label">Actual input</span><span class="usage-value actual">${e.input_tokens.toLocaleString()}</span></div>`,r){a+=`<div class="usage-row"><span class="usage-label" style="padding-left:12px">Cache read</span><span class="usage-value" style="color:var(--green)">${t.toLocaleString()}</span></div>`,a+=`<div class="usage-row"><span class="usage-label" style="padding-left:12px">Cache write</span><span class="usage-value" style="color:var(--cyan,var(--blue))">${n.toLocaleString()}</span></div>`;let r=Math.max(0,e.input_tokens-t-n);a+=`<div class="usage-row"><span class="usage-label" style="padding-left:12px">Uncached</span><span class="usage-value">${r.toLocaleString()}</span></div>`}a+=`<div class="usage-row"><span class="usage-label">Estimation error</span><span class="usage-value" style="color:${Math.abs(parseFloat(s))<15?`var(--green)`:`var(--orange)`}">${o>0?`+`:``}${o.toLocaleString()} (${s}%)</span></div>`,a+=`<div class="usage-row"><span class="usage-label">Output tokens</span><span class="usage-value">${e.output_tokens.toLocaleString()}</span></div>`,i.actualCost&&(a+=`<div style="border-top:1px solid var(--border);margin-top:6px;padding-top:6px">`,a+=`<div class="usage-row"><span class="usage-label">Input cost</span><span class="usage-value" style="color:var(--amber)">${u(i.actualCost.inputCost)}</span></div>`,a+=`<div class="usage-row"><span class="usage-label">Output cost</span><span class="usage-value" style="color:var(--amber)">${u(i.actualCost.outputCost)}</span></div>`,a+=`<div class="usage-row"><span class="usage-label">Total cost</span><span class="usage-value" style="color:var(--amber);font-size:13px">${u(i.actualCost.totalCost)}</span></div>`,a+=`</div>`),a+=`</div>`}else if(i.estimatedCost){let e=i.tokenCountSource===`count_tokens`;a+=`<div class="usage-box">`,a+=`<div class="usage-row"><span class="usage-label">Input tokens ${e?`(exact)`:`(est.)`}</span><span class="usage-value" style="color:${e?`var(--green)`:`var(--text2)`}">${e?``:`~`}${i.totalEstimatedTokens.toLocaleString()}</span></div>`,a+=`<div class="usage-row"><span class="usage-label">Input cost ${e?``:`(est.)`}</span><span class="usage-value" style="color:${e?`var(--amber)`:`var(--text3)`}">${e?``:`~`}${u(i.estimatedCost.totalCost)}</span></div>`,e&&(a+=`<div style="margin-top:4px;font-size:9px;color:var(--text3)">via count_tokens API</div>`),a+=`</div>`}if(i.toolsUsed&&i.toolsUsed.length>0){a+=`<div class="usage-box">`,a+=`<div style="font-size:10px;font-weight:700;color:var(--cyan);margin-bottom:4px">Tools Used (${i.toolsUsed.length})</div>`;for(let e of i.toolsUsed)a+=`<div style="font-size:10px;color:var(--text2);padding:1px 0">${f(e)}</div>`;a+=`</div>`}a+=`</div>`;for(let e=0;e<i.segments.length;e++){let t=i.segments[e],n=s(t),r=(t.tokens/i.totalEstimatedTokens*100).toFixed(1),o=Math.min(r,100),c=t.preview?t.preview.slice(0,80).replace(/\n/g,` `):``,u=t.index===void 0?`#${e}`:`msg #${t.index}`;a+=`<div class="segment-row" onclick="openModal(${e})">`,a+=`<div class="seg-color" style="background:${n}"></div>`,a+=`<div class="seg-info">`,a+=`<div class="seg-name" style="color:${n}">${t.name} <span style="color:var(--text3);font-size:10px;font-weight:400">${u}</span></div>`,a+=`<div class="seg-sub">${f(c)}${t.charLength>80?`...`:``}</div>`,a+=`</div>`,a+=`<div class="seg-bar"><div class="seg-bar-fill" style="width:${o}%;background:${n}"></div></div>`,a+=`<div class="seg-pct">${r}%</div>`,a+=`<div class="seg-tokens">${l(t.tokens)}</div>`,a+=`<span class="seg-expand-hint">View</span>`,a+=`</div>`}e.innerHTML=a}async function le(e,t){return(await fetch(`/api/content/${e}/${t}`)).json()}async function ue(e,t,n){return(await fetch(`/api/profiles`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:e,mode:t,tools:n})})).json()}function N(e){z({type:`set_active_profile`,profile:e})}async function P(e,n,r){try{let i=await ue(e,n,r);return i.success&&(t.profiles[e]=i.profile,F(),N(e)),i}catch(e){return console.error(`Failed to save profile:`,e),{error:e.message}}}function F(){let e=document.getElementById(`profileSelect`),n=document.getElementById(`filterBadge`),r=t.activeProfile;e.innerHTML=``;for(let n of Object.keys(t.profiles)){let t=document.createElement(`option`);t.value=n,t.textContent=n,n===r&&(t.selected=!0),e.appendChild(t)}let i=r!==`All Tools`;e.className=`profile-select`+(i?` filtering`:``),n.style.display=i?`inline`:`none`}function I(e,n){let r=n.groupId;if(r==null)return;if(!t.groups[r]){t.groups[r]={id:r,reqIndices:[],sessions:{},startTime:n.timestamp,endTime:n.timestamp};for(let e of Object.keys(t.expandedGroups))t.expandedGroups[e]=!1;t.expandedGroups[r]=!0}let i=t.groups[r];i.reqIndices.push(e),i.endTime=Math.max(i.endTime,n.timestamp);let a=n.sessionHash||`unknown`;i.sessions[a]||(i.sessions[a]={reqIndices:[],model:n.model}),i.sessions[a].reqIndices.push(e)}function L(){t.groups={},t.expandedGroups={};for(let e=0;e<t.reqs.length;e++)I(e,t.reqs[e]);let e=Object.keys(t.groups).map(Number);if(e.length>0){let n=Math.max(...e);for(let r of e)t.expandedGroups[r]=r===n}}var R;function z(e){R&&R.readyState===1&&R.send(JSON.stringify(e))}function B(){let e=location.protocol===`https:`?`wss:`:`ws:`,n=location.port===`5173`?`localhost:4455`:location.host;R=new WebSocket(`${e}//${n}`),R.onopen=()=>{t.connected=!0,E()},R.onclose=()=>{t.connected=!1,E(),setTimeout(B,2e3)},R.onmessage=e=>{let n=JSON.parse(e.data);if(n.type===`connected`){t.premium=!!n.premium,n.profiles&&(t.profiles=n.profiles),n.activeProfile&&(t.activeProfile=n.activeProfile),n.routerMode!=null&&(t.routerMode=n.routerMode),F(),w();return}if(n.type===`request`){n.toolsUsed&&n.toolsUsed.length&&n.toolsUsed.forEach(e=>t.toolsUsed.add(e)),t.reqs.push(n),t.reqs.length>50?(t.reqs.splice(0,t.reqs.length-50),L()):I(t.reqs.length-1,n);let e=t.selectedReq!==null;e||(t.selectedReq=t.reqs.length-1),w({skipDetail:e}),x(t)}if(n.type===`token_count_update`){let e=t.reqs.find(e=>e.turn===n.turn);e&&(e.exactInputTokens=n.exactInputTokens,e.segments=n.segments,e.totalEstimatedTokens=n.exactInputTokens,e.estimatedCost=n.estimatedCost,e.tokenCountSource=`count_tokens`,w({skipDetail:!(t.selectedReq!==null&&t.reqs[t.selectedReq]?.turn===n.turn)}),x(t))}if(n.type===`response_complete`){let e=n.turn==null?t.reqs[t.reqs.length-1]:t.reqs.find(e=>e.turn===n.turn);e&&(e.actualUsage=n.usage,e.stopReason=n.stopReason,n.cost&&(e.actualCost=n.cost,ne(n.cost.totalCost)),n.toolsUsed&&(e.toolsUsed=n.toolsUsed,n.toolsUsed.forEach(e=>t.toolsUsed.add(e))),w({skipDetail:!(t.selectedReq!==null&&t.reqs[t.selectedReq]?.turn===n.turn)}),x(t))}if(n.type===`router_decision`){let e=t.reqs.find(e=>e.turn===n.turn);if(e){let r=!e.router;if(e.router={mode:n.mode,eligible:n.eligible,skip_reason:n.skip_reason,matched_by:n.matched_by,confidence:n.confidence,selected_groups:n.selected_groups,stripped_groups:n.stripped_groups,estimated_tokens_saved:n.estimated_tokens_saved,sticky_reused:n.sticky_reused},r&&n.estimated_tokens_saved>0){let t=d(e.model);ie(n.estimated_tokens_saved/1e6*t*.1,n.estimated_tokens_saved)}t.selectedReq!==null&&t.reqs[t.selectedReq]?.turn===n.turn&&M(),x(t)}}n.type===`router_mode_changed`&&(t.routerMode=n.mode,E()),n.type===`profiles_updated`&&(t.profiles=n.profiles||{},t.activeProfile=n.active||`All Tools`,F()),n.type===`active_profile_changed`&&(t.activeProfile=n.active||`All Tools`,F())}}async function V(e){let r=t.reqs[t.selectedReq];if(!r)return;let i=r.segments[e];if(!i)return;n.segment=i,n.segIndex=e,n.view=i.type===`tools`?`tools`:`formatted`,n.fullContent=``,n.loading=!0,n.parsedTools=null;let a=s(i);document.getElementById(`modalColorBar`).style.background=a,document.getElementById(`modalTitle`).textContent=i.name,document.getElementById(`modalMeta`).textContent=`${c(i)}${i.role?` (`+i.role+`)`:``}${i.count?` — `+i.count+` tools`:``}`;let o=`
|
|
15
|
+
<div class="modal-stat"><div class="modal-stat-value" style="color:${a}">${l(i.tokens)}</div><div class="modal-stat-label">Tokens</div></div>
|
|
16
|
+
<div class="modal-stat"><div class="modal-stat-value">${(i.charLength||0).toLocaleString()}</div><div class="modal-stat-label">Chars</div></div>
|
|
17
|
+
<div class="modal-stat"><div class="modal-stat-value">${(i.tokens/r.totalEstimatedTokens*100).toFixed(1)}%</div><div class="modal-stat-label">Of total</div></div>
|
|
18
|
+
`;document.getElementById(`modalStats`).innerHTML=o;let u=document.getElementById(`toolsViewBtn`);u.style.display=i.type===`tools`?`inline`:`none`,document.querySelectorAll(`.modal-toolbar button`).forEach(e=>e.classList.remove(`active`)),i.type===`tools`?u.classList.add(`active`):document.getElementById(`formattedBtn`).classList.add(`active`),document.getElementById(`modalSearch`).value=``,document.getElementById(`modalBody`).innerHTML=`<div style="display:flex;align-items:center;justify-content:center;height:200px;color:var(--text3);font-size:13px"><div style="text-align:center"><div style="font-size:24px;margin-bottom:8px;animation:pulse 1s ease-in-out infinite">⏳</div>Loading full content...</div></div>`,document.getElementById(`modalOverlay`).classList.add(`open`);try{let t=await le(r.turn,e);if(t.content){n.fullContent=t.content;let e=document.querySelector(`.modal-stat:nth-child(2) .modal-stat-value`);if(e&&(e.textContent=t.content.length.toLocaleString()),i.type===`tools`)try{n.parsedTools=JSON.parse(t.content)}catch{n.parsedTools=null}}else n.fullContent=i.preview||`(content no longer available — request may have been evicted)`}catch(e){console.error(`Failed to fetch segment content:`,e),n.fullContent=i.preview||`(failed to load content)`}n.loading=!1,W()}function H(){document.getElementById(`modalOverlay`).classList.remove(`open`),n.segment=null,n.parsedTools=null}function U(e,t){n.view=e,document.querySelectorAll(`.modal-toolbar button`).forEach(e=>e.classList.remove(`active`)),t&&t.classList.add(`active`),W()}function W(){let e=document.getElementById(`modalBody`),t=n.fullContent;if(n.loading)return;if(n.view===`tools`&&n.parsedTools){de(e);return}if(n.view===`raw`){e.innerHTML=`<div class="modal-content" style="white-space:pre-wrap;word-break:break-all">${f(t)}</div>`;return}let r=t;try{let e=JSON.parse(t);r=JSON.stringify(e,null,2)}catch{}let i=r.split(`
|
|
19
|
+
`),a=`<div class="modal-content">`;for(let e=0;e<i.length;e++)a+=`<span class="line"><span class="line-num">${e+1}</span>${f(i[e])}</span>`;a+=`</div>`,e.innerHTML=a}function de(e){let r=n.parsedTools;if(!r||!r.length){e.innerHTML=`<div style="padding:20px;color:var(--text3)">No tools found</div>`;return}let i=t.profiles[t.activeProfile],a=t.activeProfile===`All Tools`,s=h(r),c=`<div class="modal-tools">`,u=r.filter(e=>p(e.name,i,a)).length;c+=`<div class="modal-tools-header">`,c+=`<div class="modal-tools-header-left"><strong>${u}</strong> of <strong>${r.length}</strong> tools enabled</div>`,c+=`<div style="display:flex;gap:6px">`,c+=`<button class="btn-secondary" onclick="toggleAllTools(true)" style="padding:3px 8px;border-radius:4px;font-size:10px">All</button>`,c+=`<button class="btn-secondary" onclick="toggleAllTools(false)" style="padding:3px 8px;border-radius:4px;font-size:10px">None</button>`,c+=`</div></div>`;let d=[],m=[];for(let[e,t]of s)e===`other`?m.push([e,t]):d.push([e,t]);function g(e){for(let[n,r]of e){let e=[...r].sort((e,t)=>(0,o.estimateToolTokens)(t)-(0,o.estimateToolTokens)(e)),s=n===`other`?`Other`:n.charAt(0).toUpperCase()+n.slice(1),u=e.reduce((e,t)=>e+(0,o.estimateToolTokens)(t),0),d=e.filter(e=>p(e.name,i,a)).length,m=d===e.length;c+=`<div class="tool-group" data-server="${f(n)}">`,c+=`<div class="tool-group-header">`,c+=`<input type="checkbox" class="tool-group-checkbox" data-server="${f(n)}" ${m?`checked`:``} onclick="event.stopPropagation(); toggleGroupCheckbox('${f(n)}', this.checked)">`,c+=`<span class="tool-group-chevron" onclick="toggleGroupAccordion('${f(n)}')">▶</span>`,c+=`<div class="tool-group-title" onclick="toggleGroupAccordion('${f(n)}')">`,c+=`<span class="tool-group-name">${f(s)}</span>`,c+=`<span class="tool-group-meta">${d}/${e.length} tools · ~${l(u)} tok</span>`,c+=`</div>`,c+=`<div class="tool-group-actions">`,c+=`<button class="btn-secondary" onclick="event.stopPropagation(); toggleGroupTools('${f(n)}', true)" style="padding:2px 6px;font-size:9px">All</button>`,c+=`<button class="btn-secondary" onclick="event.stopPropagation(); toggleGroupTools('${f(n)}', false)" style="padding:2px 6px;font-size:9px">None</button>`,c+=`</div></div>`,c+=`<div class="tool-group-body collapsed">`;for(let r of e){let e=p(r.name,i,a),s=(0,o.estimateToolTokens)(r),u=(r.description||``).slice(0,120),d=!t.toolsUsed?.has(r.name);c+=`<div class="tool-card" data-tool-name="${f(r.name)}">`,c+=`<input type="checkbox" data-tool="${f(r.name)}" data-server="${f(n)}" ${e?`checked`:``} onchange="onToolToggle()">`,c+=`<div class="tool-card-info">`,c+=`<div class="tool-card-name">${f(r.name)}${d?` <span class="never-used-tag" title="Not used this session">never used</span>`:``}</div>`,u&&(c+=`<div class="tool-card-desc">${f(u)}</div>`),c+=`</div>`,c+=`<div class="tool-card-tokens">~${l(s)} tok</div>`,c+=`</div>`}c+=`</div></div>`}}d.length>0&&(c+=`<div class="tool-section-header">MCP Servers</div>`,g(d)),m.length>0&&(c+=`<div class="tool-section-header">Other Tools</div>`,g(m)),c+=`<div id="toolsSavings"></div>`;let _=t.reqs[t.selectedReq]?.toolsUsed||[];c+=`<div class="save-profile-bar">`,c+=`<input type="text" id="profileNameInput" placeholder="Profile name..." value="">`,c+=`<button class="btn-primary" onclick="saveCurrentAsProfile()">Save as Profile</button>`,_.length>0&&(c+=`<button class="btn-secondary" onclick="createProfileFromThisTurn()" title="Create profile from tools used in this request">From this turn</button>`),c+=`</div>`,c+=`</div>`,e.innerHTML=c,G()}function fe(e){let t=document.querySelector(`.tool-group[data-server="${e}"]`);if(!t)return;let n=t.querySelector(`.tool-group-body`),r=t.querySelector(`.tool-group-chevron`);n&&n.classList.toggle(`collapsed`),r&&r.classList.toggle(`expanded`)}function pe(e,t){document.querySelectorAll(`.modal-tools input[type="checkbox"][data-server="${e}"]:not(.tool-group-checkbox)`).forEach(e=>{e.checked=t}),G()}function me(){document.querySelectorAll(`.tool-group-checkbox`).forEach(e=>{let t=e.dataset.server,n=document.querySelectorAll(`.modal-tools input[type="checkbox"][data-server="${t}"]:not(.tool-group-checkbox)`),r=n.length,i=0;n.forEach(e=>{e.checked&&i++}),e.checked=i===r,e.indeterminate=i>0&&i<r;let a=e.closest(`.tool-group`)?.querySelector(`.tool-group-meta`);if(a){let e=a.textContent.match(/·.*$/);a.textContent=`${i}/${r} tools ${e?e[0]:``}`}})}function he(e,t){document.querySelectorAll(`.modal-tools input[data-server="${e}"]:not(.tool-group-checkbox)`).forEach(e=>{e.checked=t}),G()}function ge(e){document.querySelectorAll(`.modal-tools input[type="checkbox"]:not(.tool-group-checkbox)`).forEach(t=>{t.checked=e}),G()}function G(){let e=document.querySelectorAll(`.modal-tools input[type="checkbox"]:not(.tool-group-checkbox)`),t=n.parsedTools;if(!t||!e.length)return;let r=0,i=0,a=0;e.forEach(e=>{let n=t.find(t=>t.name===e.dataset.tool);if(!n)return;let s=(0,o.estimateToolTokens)(n);e.checked?(r+=s,a++):i+=s}),me();let s=document.getElementById(`toolsSavings`);s&&i>0?s.innerHTML=`<div class="savings-banner">Disabling ${t.length-a} tools saves ~${l(i)} tokens per request</div>`:s&&(s.innerHTML=``);let c=document.querySelector(`.modal-tools-header-left`);c&&(c.innerHTML=`<strong>${a}</strong> of <strong>${t.length}</strong> tools enabled`)}async function _e(){let e=t.reqs[t.selectedReq],n=e?.toolsUsed||[];if(n.length!==0&&(await P(`Req ${e?.turn??0} tools`,`allowlist`,n)).success){let e=document.querySelector(`.save-profile-bar`);if(e){let t=e.style.background;e.style.background=`rgba(52,211,153,0.1)`,e.style.borderColor=`rgba(52,211,153,0.3)`,setTimeout(()=>{e.style.background=t,e.style.borderColor=``},1500)}}}async function ve(){let e=document.getElementById(`profileNameInput`),t=(e?.value||``).trim();if(!t){e.style.borderColor=`var(--red)`,e.focus(),setTimeout(()=>e.style.borderColor=``,1500);return}let n=document.querySelectorAll(`.modal-tools input[type="checkbox"]:not(.tool-group-checkbox)`),r=[];if(n.forEach(e=>{e.checked&&r.push(e.dataset.tool)}),(await P(t,`allowlist`,r)).success){e.value=``;let t=document.querySelector(`.save-profile-bar`);if(t){let e=t.style.background;t.style.background=`rgba(16,185,129,0.1)`,t.style.borderColor=`rgba(16,185,129,0.3)`,setTimeout(()=>{t.style.background=e,t.style.borderColor=``},1500)}}}function K(){let e=document.getElementById(`modalSearch`).value.trim().toLowerCase(),t=document.getElementById(`modalBody`);if(!e){W();return}if(n.view===`tools`){document.querySelectorAll(`.tool-group`).forEach(t=>{let n=0;t.querySelectorAll(`.tool-card`).forEach(t=>{let r=t.querySelector(`.tool-card-name`)?.textContent?.toLowerCase()||``,i=t.querySelector(`.tool-card-desc`)?.textContent?.toLowerCase()||``,a=r.includes(e)||i.includes(e);t.style.display=a?`flex`:`none`,a&&n++}),t.style.display=n>0?`block`:`none`;let r=t.querySelector(`.tool-group-body`),i=t.querySelector(`.tool-group-chevron`);n>0?(r?.classList.remove(`collapsed`),i?.classList.add(`expanded`)):(r?.classList.add(`collapsed`),i?.classList.remove(`expanded`))});return}let r=n.fullContent;try{r=JSON.stringify(JSON.parse(r),null,2)}catch{}let i=r.split(`
|
|
20
|
+
`),a=`<div class="modal-content">`;for(let t=0;t<i.length;t++){let n=i[t],r=n.toLowerCase().indexOf(e);if(r!==-1){let i=f(n.slice(0,r)),o=f(n.slice(r,r+e.length)),s=f(n.slice(r+e.length));a+=`<span class="line"><span class="line-num">${t+1}</span>${i}<span class="highlight">${o}</span>${s}</span>`}else a+=`<span class="line" style="opacity:0.25"><span class="line-num">${t+1}</span>${f(n)}</span>`}a+=`</div>`,t.innerHTML=a}function q(){navigator.clipboard.writeText(n.fullContent).then(()=>{let e=document.getElementById(`copyBtn`),t=e.textContent;e.textContent=`Copied!`,e.style.color=`var(--green)`,setTimeout(()=>{e.textContent=t,e.style.color=``},1500)})}var J=`jannal_theme`;function Y(){return localStorage.getItem(J)||`dark`}function X(e){e===`light`?document.documentElement.setAttribute(`data-theme`,`light`):document.documentElement.removeAttribute(`data-theme`);let t=document.getElementById(`themeIconMoon`),n=document.getElementById(`themeIconSun`);t&&n&&(t.style.display=e===`dark`?`block`:`none`,n.style.display=e===`light`?`block`:`none`)}function ye(){let e=Y()===`dark`?`light`:`dark`;return localStorage.setItem(J,e),document.documentElement.classList.add(`theme-transitioning`),X(e),setTimeout(()=>document.documentElement.classList.remove(`theme-transitioning`),350),e}function be(){X(Y())}function Z(e){t.selectedReq=e,D(),k(),M(),x(t)}function Q(){t.reqs=[],t.selectedReq=null,t.groups={},t.expandedGroups={},w(),x(t)}function xe(){t.groupView=!t.groupView,k(),x(t)}function Se(e){t.expandedGroups[e]=!t.expandedGroups[e],k()}function Ce(){t.reqs.length!==0&&document.getElementById(`exportMenu`)?.classList.toggle(`open`)}function we(){t.reqs.length!==0&&(C(te(t),`jannal-session-${new Date().toISOString().slice(0,19).replace(/[:-]/g,``)}.json`),document.getElementById(`exportMenu`)?.classList.remove(`open`))}function Te(){t.reqs.length!==0&&(C(S(t),`jannal-session-${new Date().toISOString().slice(0,19).replace(/[:-]/g,``)}.csv`),document.getElementById(`exportMenu`)?.classList.remove(`open`))}window.openModal=V,window.closeModal=H,window.setModalView=U,window.selectReq=Z,window.clearReqs=Q,window.toggleGroup=Se,window.onProfileChange=N,window.toggleAllTools=ge,window.toggleGroupTools=he,window.toggleGroupAccordion=fe,window.toggleGroupCheckbox=pe,window.onToolToggle=G,window.saveCurrentAsProfile=ve,window.createProfileFromThisTurn=_e,window.copyClaudeCommand=se,window.filterModalContent=K,window.copyModalContent=q;var $=null;function Ee(e){let n=document.getElementById(`globalSearchResults`);if(!e||e.length<2){n.classList.remove(`open`),n.innerHTML=``;return}clearTimeout($),$=setTimeout(async()=>{try{let r=await(await fetch(`/api/search?q=${encodeURIComponent(e)}`)).json();if(r.results.length===0){n.innerHTML=`<div class="search-no-results">No matches found</div>`,n.classList.add(`open`);return}let i=e.toLowerCase();n.innerHTML=r.results.map(e=>{let n=t.reqs.findIndex(t=>t.turn===e.turnId),r=n>=0?`Req ${t.reqs[n].turn}`:`Req ${e.turnId}`,a=e.snippet.replace(/</g,`<`).replace(/>/g,`>`).replace(RegExp(`(${i.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)})`,`gi`),`<mark>$1</mark>`);return`<div class="search-result-item" data-turn="${n}" data-seg="${e.segIndex}">
|
|
21
|
+
<div class="search-result-turn">${r} · segment ${e.segIndex}</div>
|
|
22
|
+
<div class="search-result-snippet">${a}</div>
|
|
23
|
+
</div>`}).join(``),n.classList.add(`open`)}catch(e){console.error(`Search error:`,e)}},250)}document.getElementById(`themeToggle`).addEventListener(`click`,()=>{ye(),w()}),document.getElementById(`profileSelect`).addEventListener(`change`,e=>{N(e.target.value)}),document.getElementById(`clearBtn`).addEventListener(`click`,Q),document.getElementById(`viewToggleBtn`).addEventListener(`click`,xe),document.getElementById(`exportBtn`).addEventListener(`click`,Ce),document.getElementById(`exportMenu`)?.addEventListener(`click`,e=>{let t=e.target.closest(`.export-option`);t&&(t.dataset.format===`json`?we():t.dataset.format===`csv`&&Te())}),document.addEventListener(`click`,e=>{let t=document.getElementById(`exportMenu`),n=document.querySelector(`.export-dropdown`);t?.classList.contains(`open`)&&n&&!n.contains(e.target)&&t.classList.remove(`open`);let r=document.querySelector(`.router-badge-wrapper`),i=document.getElementById(`routerPopover`);i?.classList.contains(`open`)&&r&&!r.contains(e.target)&&i.classList.remove(`open`)}),document.getElementById(`routerBadge`)?.addEventListener(`click`,e=>{e.stopPropagation(),document.getElementById(`routerPopover`)?.classList.toggle(`open`)}),document.getElementById(`routerPopover`)?.addEventListener(`click`,async e=>{let n=e.target.closest(`.router-popover-opt`);if(!n||!t.premium)return;let r=n.dataset.mode;if(r===t.routerMode){document.getElementById(`routerPopover`)?.classList.remove(`open`);return}try{(await fetch(`/api/router/mode`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({mode:r})})).ok&&(t.routerMode=r,E())}catch(e){console.error(`Failed to set router mode:`,e)}document.getElementById(`routerPopover`)?.classList.remove(`open`)}),document.getElementById(`modalOverlay`).addEventListener(`click`,e=>{e.target===e.currentTarget&&H()}),document.getElementById(`modalCloseBtn`).addEventListener(`click`,H),document.getElementById(`formattedBtn`).addEventListener(`click`,e=>{U(`formatted`,e.target)}),document.getElementById(`rawBtn`).addEventListener(`click`,e=>{U(`raw`,e.target)}),document.getElementById(`toolsViewBtn`).addEventListener(`click`,e=>{U(`tools`,e.target)}),document.getElementById(`modalSearch`).addEventListener(`input`,K),document.getElementById(`copyBtn`).addEventListener(`click`,q),document.getElementById(`globalSearch`).addEventListener(`input`,e=>{Ee(e.target.value.trim())}),document.getElementById(`globalSearchResults`).addEventListener(`click`,e=>{let t=e.target.closest(`.search-result-item`);if(!t)return;let n=parseInt(t.dataset.turn),r=parseInt(t.dataset.seg);n>=0&&(Z(n),V(r)),document.getElementById(`globalSearchResults`).classList.remove(`open`),document.getElementById(`globalSearch`).value=``}),document.addEventListener(`click`,e=>{let t=document.querySelector(`.global-search-wrapper`);t&&!t.contains(e.target)&&document.getElementById(`globalSearchResults`).classList.remove(`open`)}),document.addEventListener(`keydown`,e=>{e.key===`Escape`&&(document.getElementById(`globalSearchResults`).classList.remove(`open`),H())}),be(),ee(t),L(),B(),w();
|
|
Binary file
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Jannal</title>
|
|
7
|
+
<link rel="icon" type="image/png" href="/favicon.png">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
11
|
+
<script>(function(){var t=localStorage.getItem('jannal_theme');if(t==='light')document.documentElement.setAttribute('data-theme','light')})()</script>
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-CzXZ1AkJ.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B8dfyj9-.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div class="noise-overlay" aria-hidden="true"></div>
|
|
17
|
+
|
|
18
|
+
<!-- Header -->
|
|
19
|
+
<div class="header">
|
|
20
|
+
<div class="header-left">
|
|
21
|
+
<img class="logo" src="/logo.png" alt="Jannal">
|
|
22
|
+
<div class="header-brand">
|
|
23
|
+
<h1>Jannal</h1>
|
|
24
|
+
<div class="status">
|
|
25
|
+
<div class="status-dot disconnected" id="statusDot"></div>
|
|
26
|
+
<span id="statusText" style="color:var(--text3)">Connecting...</span>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="header-right">
|
|
31
|
+
<div class="global-search-wrapper">
|
|
32
|
+
<input class="global-search" type="text" placeholder="Search requests..." id="globalSearch" />
|
|
33
|
+
<div class="global-search-results" id="globalSearchResults"></div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="hdr-sep"></div>
|
|
36
|
+
<select class="profile-select" id="profileSelect">
|
|
37
|
+
<option value="All Tools">All Tools</option>
|
|
38
|
+
</select>
|
|
39
|
+
<span class="filter-badge" id="filterBadge" style="display:none">FILTERING</span>
|
|
40
|
+
<div class="router-badge-wrapper">
|
|
41
|
+
<span class="router-badge" id="routerBadge"></span>
|
|
42
|
+
<div class="router-popover" id="routerPopover">
|
|
43
|
+
<div class="router-popover-title">Router Mode</div>
|
|
44
|
+
<button class="router-popover-opt" data-mode="off"><span class="router-opt-dot router-opt-dot--off"></span>Off<span class="router-opt-desc">Router disabled</span></button>
|
|
45
|
+
<button class="router-popover-opt" data-mode="shadow"><span class="router-opt-dot router-opt-dot--shadow"></span>Shadow<span class="router-opt-desc">Observe only, no filtering</span></button>
|
|
46
|
+
<button class="router-popover-opt" data-mode="auto"><span class="router-opt-dot router-opt-dot--auto"></span>Auto<span class="router-opt-desc">Route and filter tools</span></button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="hdr-sep"></div>
|
|
50
|
+
<div class="daily-saved" id="dailySaved" title="Estimated daily savings from router intelligence">Saved: $0.00</div>
|
|
51
|
+
<div class="daily-cost" id="dailyCost" title="Total cost today">Cost: $0.00</div>
|
|
52
|
+
<button class="theme-toggle" id="themeToggle" title="Toggle dark/light mode" aria-label="Toggle theme">
|
|
53
|
+
<svg id="themeIconMoon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
54
|
+
<svg id="themeIconSun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Context Bar -->
|
|
60
|
+
<div class="bar-container">
|
|
61
|
+
<div class="bar-outer" id="barOuter">
|
|
62
|
+
<div class="bar-inner" id="barInner"></div>
|
|
63
|
+
<div class="bar-marker" style="left:80%"><span>80%</span></div>
|
|
64
|
+
<div class="bar-marker" style="left:95%"><span>95%</span></div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="bar-stats">
|
|
67
|
+
<div class="bar-legend" id="barLegend"></div>
|
|
68
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
69
|
+
<span class="bar-total" id="barTotal">0 / 0</span>
|
|
70
|
+
<span class="bar-pct" id="barPct">0%</span>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="token-chart-container" id="tokenChartContainer">
|
|
74
|
+
<div class="token-chart-label">Token growth</div>
|
|
75
|
+
<div class="token-chart" id="tokenChart"></div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Main -->
|
|
80
|
+
<div class="main">
|
|
81
|
+
<div class="panel reqs-panel">
|
|
82
|
+
<div class="panel-header">
|
|
83
|
+
<span>Requests</span>
|
|
84
|
+
<div class="panel-actions">
|
|
85
|
+
<button id="viewToggleBtn" class="panel-btn view-toggle active" title="Toggle grouped/flat view">Grouped</button>
|
|
86
|
+
<div class="export-dropdown">
|
|
87
|
+
<button id="exportBtn" class="panel-btn" title="Export session">Export</button>
|
|
88
|
+
<div id="exportMenu" class="export-menu">
|
|
89
|
+
<button class="export-option" data-format="json">JSON</button>
|
|
90
|
+
<button class="export-option" data-format="csv">CSV</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<button id="clearBtn" class="panel-btn">Clear</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="panel-body" id="reqList">
|
|
97
|
+
<div class="empty">
|
|
98
|
+
<div class="empty-icon waiting">🔍</div>
|
|
99
|
+
<h2>Waiting for requests...</h2>
|
|
100
|
+
<p>Start Claude Code with:<br><code style="color:var(--cyan);font-size:11px">ANTHROPIC_BASE_URL=http://localhost:4455 claude</code></p>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="panel detail-panel">
|
|
105
|
+
<div class="panel-header">
|
|
106
|
+
<span id="detailTitle">Segment Breakdown</span>
|
|
107
|
+
<span id="detailMeta" style="font-weight:400;text-transform:none;font-size:10px;color:var(--text3)"></span>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="panel-body" id="detailBody">
|
|
110
|
+
<div class="empty">
|
|
111
|
+
<div class="empty-icon">📊</div>
|
|
112
|
+
<h2>No request selected</h2>
|
|
113
|
+
<p>When requests flow through the proxy, each request will appear on the left. Click one to see the full context breakdown.</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<!-- Modal -->
|
|
120
|
+
<div class="modal-overlay" id="modalOverlay">
|
|
121
|
+
<div class="modal" id="modal">
|
|
122
|
+
<div class="modal-header">
|
|
123
|
+
<div class="modal-color-bar" id="modalColorBar"></div>
|
|
124
|
+
<div class="modal-title">
|
|
125
|
+
<h2 id="modalTitle"></h2>
|
|
126
|
+
<div class="modal-meta" id="modalMeta"></div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="modal-stats" id="modalStats"></div>
|
|
129
|
+
<button class="modal-close" id="modalCloseBtn">×</button>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="modal-toolbar" id="modalToolbar">
|
|
132
|
+
<button class="active" id="formattedBtn">Formatted</button>
|
|
133
|
+
<button id="rawBtn">Raw</button>
|
|
134
|
+
<button id="toolsViewBtn" style="display:none">Tools</button>
|
|
135
|
+
<div class="spacer"></div>
|
|
136
|
+
<input class="search-box" type="text" placeholder="Search content..." id="modalSearch" />
|
|
137
|
+
<button id="copyBtn">Copy</button>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="modal-body" id="modalBody"></div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
</body>
|
|
144
|
+
</html>
|
|
Binary file
|
package/public/logo.png
ADDED
|
Binary file
|