Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,15 @@
@keyframes banner-in{from{opacity:0;transform:translateY(-100%)}to{opacity:1;transform:translateY(0)}}
.sse-banner{position:sticky;top:0;z-index:10;padding:6px 12px;font-size:12px;text-align:center;background:var(--danger-tint);color:var(--text-danger);border-bottom:1px solid var(--danger);display:none}
.sse-banner.visible{display:block}
/* Context-usage warning — same shape as .sse-banner but warning-coloured
and dismissible. A real <button> handles the close (keyboard + AT), not
click-anywhere-on-the-banner. Uses the same `--warning` / `--text-warning`
pair as the dead `.banner.warn` rule that was already in this file. */
.context-banner{position:sticky;top:0;z-index:9;padding:6px 12px;font-size:12px;background:var(--warning);color:var(--text-warning);border-bottom:1px solid var(--warning);display:none;align-items:center;justify-content:center;gap:8px}
.context-banner.visible{display:flex}
.context-banner .context-banner-msg{flex:1;text-align:center}
.context-banner button{padding:2px 6px;min-height:22px;background:transparent;border:1px solid currentColor;color:inherit;font-size:11px;box-shadow:none;flex-shrink:0}
.context-banner button:hover{background:var(--fill-hover);border-color:currentColor}
.footer{padding:8px 16px;font-size:11px;color:var(--text-subtle);text-align:center;border-top:1px solid var(--line);background:var(--base);display:flex;align-items:center;justify-content:center;gap:12px}
.token-summary{font-size:11px;color:var(--text-subtle)}

Expand Down Expand Up @@ -656,7 +665,10 @@ <h2>Config</h2>
</div>
</div>
<div id="sse-banner" class="sse-banner">Connection lost — reconnecting…</div>
<div id="context-warning" style="display:none;background:#e67e22;color:#fff;text-align:center;padding:6px 12px;font-size:12px;cursor:pointer" onclick="this.style.display='none'"></div>
<div id="context-warning" class="context-banner" role="status" aria-live="polite">
<span class="context-banner-msg" id="context-warning-msg"></span>
<button type="button" id="context-warning-dismiss" aria-label="Dismiss context warning">Dismiss</button>
</div>
<div class="chat" id="chat">
<div class="onboarding" id="onboarding">
<img src="/favicon.svg" alt="Dodo" class="dodo-logo lg dodo-logo-img" style="margin-bottom:12px"/>
Expand Down
42 changes: 32 additions & 10 deletions public/js/dodo-chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,26 +457,39 @@ function updateTokenSummary(state){
el.textContent='';
el.title='';
}
const banner=$("context-warning");
if(banner)banner.style.display="none";
hideContextWarning();
return;
}
// Color code by context usage
// Color code by context usage — use design tokens, not hardcoded hex
let color='var(--text-subtle)';
if(pct>80)color='#e74c3c';
else if(pct>50)color='#e67e22';
if(pct>80)color='var(--text-danger)';
else if(pct>50)color='var(--text-warning)';
el.style.color=color;
const budgetStr=budget?`${Math.round(budget/1000)}k`:'?';
el.textContent=pct>0?`Context: ${pct}% of ${budgetStr} · ${(ti/1000).toFixed(1)}k in / ${(to/1000).toFixed(1)}k out`:`${(ti/1000).toFixed(1)}k in / ${(to/1000).toFixed(1)}k out`;
el.title=`Context window: ${state.contextWindow??0} tokens\nBudget (80%): ${budget} tokens\nUsage: ~${pct}%\nModel: ${state.model??'unknown'}`;
// Show/hide context warning banner
const banner=$("context-warning");
if(banner){
if(pct>80){banner.style.display="block";banner.textContent=`Context is ${pct}% full. Consider starting a new session for a fresh topic.`}
else{banner.style.display="none"}
if(pct>80){
showContextWarning(`Context is ${pct}% full. Consider starting a new session for a fresh topic.`);
} else {
hideContextWarning();
}
}

// Show/hide the sticky context-usage banner. Writes only the message span so
// the dismiss button (and its aria-label) are preserved across updates.
function showContextWarning(message){
const banner=$("context-warning");
if(!banner)return;
const msg=$("context-warning-msg");
if(msg)msg.textContent=message;
banner.classList.add("visible");
}
function hideContextWarning(){
const banner=$("context-warning");
if(banner)banner.classList.remove("visible");
}

// --- Chat actions ---
async function sendMessage(){
const content=$("msg-input").value.trim();
Expand Down Expand Up @@ -519,7 +532,7 @@ async function sendMessage(){
}
async function abortPrompt(){if(!currentSession)return;await jsonSafe(`/session/${currentSession}/abort`,{});setProcessing(false);hideThinking()}
async function forkSession(){if(!currentSession)return;const d=await jsonSafe(`/session/${currentSession}/fork`,{});if(!d)return;const{id}=d;currentSession=id;await selectSession(id)}
async function deleteSession(){if(!currentSession)return;const ok=await appConfirm("Delete this session? This can\u2019t be undone.");if(!ok)return;await apiSafe(`/session/${currentSession}`,{method:"DELETE"});currentSession=null;clearPendingImages();$("chat").innerHTML="";$("session-title-display").textContent="No session";$("session-id-display").textContent="";$("token-summary").textContent="";$("token-summary").style.color="";const cw=$("context-warning");if(cw)cw.style.display="none";$("presence-bar").innerHTML="";if(typeof renderSessionTodos==='function')renderSessionTodos([]);history.replaceState(null,"",location.pathname);await loadSessions();if(window.innerWidth<=900)switchTab('chat')}
async function deleteSession(){if(!currentSession)return;const ok=await appConfirm("Delete this session? This can\u2019t be undone.");if(!ok)return;await apiSafe(`/session/${currentSession}`,{method:"DELETE"});currentSession=null;clearPendingImages();$("chat").innerHTML="";$("session-title-display").textContent="No session";$("session-id-display").textContent="";$("token-summary").textContent="";$("token-summary").style.color="";hideContextWarning();$("presence-bar").innerHTML="";if(typeof renderSessionTodos==='function')renderSessionTodos([]);history.replaceState(null,"",location.pathname);await loadSessions();if(window.innerWidth<=900)switchTab('chat')}

// --- Session rename ---
async function renameSession(){
Expand Down Expand Up @@ -666,3 +679,12 @@ function clearPendingImages(){
_pendingImages.length=0;
renderImagePreviews();
}

// Wire up the context-warning dismiss button. The banner returns next time
// updateTokenSummary() decides usage is still > 80%, so this is an "until I
// stop typing" dismissal, not a permanent suppression — which matches the
// pre-refactor click-anywhere behaviour.
window.addEventListener('load',()=>{
const btn=$("context-warning-dismiss");
if(btn)btn.addEventListener('click',hideContextWarning);
});
10 changes: 8 additions & 2 deletions public/js/dodo-sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ function connectUserEvents(){
};
}
function _sessionItemHtml(s){
return `<div class="session-item ${currentSession===s.id?'active':''}" data-sid="${esc(s.id)}" onclick="selectSession('${esc(s.id)}')" role="button" tabindex="0" onkeydown="if(event.key==='Enter')selectSession('${esc(s.id)}')"><div class="session-title">${esc(s.title||s.id.slice(0,8))}</div><div class="session-meta">${esc(s.status)} &middot; ${esc(new Date(s.updatedAt).toLocaleString())}</div></div>`
// Both Enter and Space must activate `role=button` elements (ARIA spec).
// preventDefault() on Space stops the page from scrolling.
// No `aria-label` — when set, AT announces only the label and skips inner
// text, which would hide the `.session-meta` status + timestamp. Letting
// accessible-name computation fall through to the title + meta children
// matches what sighted users see.
return `<div class="session-item ${currentSession===s.id?'active':''}" data-sid="${esc(s.id)}" onclick="selectSession('${esc(s.id)}')" role="button" tabindex="0" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();selectSession('${esc(s.id)}')}"><div class="session-title">${esc(s.title||s.id.slice(0,8))}</div><div class="session-meta">${esc(s.status)} &middot; ${esc(new Date(s.updatedAt).toLocaleString())}</div></div>`
}
function renderSessionList(sessions){
const el=$("session-list");
Expand Down Expand Up @@ -82,7 +88,7 @@ async function selectSession(id){
// to whatever the user was about to send there, not here.
if(typeof clearPendingImages==='function')clearPendingImages();
$("chat").innerHTML="";$("onboarding")?.remove();
const cw=$("context-warning");if(cw)cw.style.display="none";
if(typeof hideContextWarning==='function')hideContextWarning();
showSkeleton($("chat"),5);
$("session-id-display").textContent=id.slice(0,8);
$("session-title-display").textContent=id.slice(0,8);
Expand Down
Loading