Hermes WebUI v0.1.0 — initial public release
This commit is contained in:
152
static/boot.js
Normal file
152
static/boot.js
Normal file
@@ -0,0 +1,152 @@
|
||||
async function cancelStream(){
|
||||
const streamId = S.activeStreamId;
|
||||
if(!streamId) return;
|
||||
try{
|
||||
await fetch(`/api/chat/cancel?stream_id=${encodeURIComponent(streamId)}`);
|
||||
const btn=$('btnCancel');if(btn)btn.style.display='none';
|
||||
setStatus('Cancelling…');
|
||||
}catch(e){setStatus('Cancel failed: '+e.message);}
|
||||
}
|
||||
|
||||
$('btnSend').onclick=send;
|
||||
$('btnAttach').onclick=()=>$('fileInput').click();
|
||||
$('fileInput').onchange=e=>{addFiles(Array.from(e.target.files));e.target.value='';};
|
||||
$('btnNewChat').onclick=async()=>{await newSession();await renderSessionList();$('msg').focus();};
|
||||
$('btnDownload').onclick=()=>{
|
||||
if(!S.session)return;
|
||||
const blob=new Blob([transcript()],{type:'text/markdown'});
|
||||
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
|
||||
a.download=`hermes-${S.session.session_id}.md`;a.click();URL.revokeObjectURL(a.href);
|
||||
};
|
||||
$('btnExportJSON').onclick=()=>{
|
||||
if(!S.session)return;
|
||||
const url=`/api/session/export?session_id=${encodeURIComponent(S.session.session_id)}`;
|
||||
const a=document.createElement('a');a.href=url;
|
||||
a.download=`hermes-${S.session.session_id}.json`;a.click();
|
||||
};
|
||||
// btnRefreshFiles is now panel-icon-btn in header (see HTML)
|
||||
$('btnClearPreview').onclick=()=>{
|
||||
$('previewArea').classList.remove('visible');
|
||||
$('previewImg').src='';
|
||||
$('previewMd').innerHTML='';
|
||||
$('previewCode').textContent='';
|
||||
$('previewPathText').textContent='';
|
||||
$('fileTree').style.display='';
|
||||
};
|
||||
// workspacePath click handler removed -- use topbar workspace chip dropdown instead
|
||||
$('modelSelect').onchange=async()=>{
|
||||
if(!S.session)return;
|
||||
const selectedModel=$('modelSelect').value;
|
||||
localStorage.setItem('hermes-webui-model', selectedModel);
|
||||
await api('/api/session/update',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,workspace:S.session.workspace,model:selectedModel})});
|
||||
S.session.model=selectedModel;syncTopbar();
|
||||
};
|
||||
$('msg').addEventListener('input',autoResize);
|
||||
$('msg').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();}});
|
||||
// B14: Cmd/Ctrl+K creates a new chat from anywhere
|
||||
document.addEventListener('keydown',async e=>{
|
||||
if((e.metaKey||e.ctrlKey)&&e.key==='k'){
|
||||
e.preventDefault();
|
||||
if(!S.busy){await newSession();await renderSessionList();$('msg').focus();}
|
||||
}
|
||||
if(e.key==='Escape'){
|
||||
// Close workspace dropdown
|
||||
closeWsDropdown();
|
||||
// Clear session search
|
||||
const ss=$('sessionSearch');
|
||||
if(ss&&ss.value){ss.value='';filterSessions();}
|
||||
// Cancel any active message edit
|
||||
const editArea=document.querySelector('.msg-edit-area');
|
||||
if(editArea){
|
||||
const bar=editArea.closest('.msg-row')&&editArea.closest('.msg-row').querySelector('.msg-edit-bar');
|
||||
if(bar){const cancel=bar.querySelector('.msg-edit-cancel');if(cancel)cancel.click();}
|
||||
}
|
||||
}
|
||||
});
|
||||
$('msg').addEventListener('paste',e=>{
|
||||
const items=Array.from(e.clipboardData?.items||[]);
|
||||
const imageItems=items.filter(i=>i.type.startsWith('image/'));
|
||||
if(!imageItems.length)return;
|
||||
e.preventDefault();
|
||||
const files=imageItems.map(i=>{
|
||||
const blob=i.getAsFile();
|
||||
const ext=i.type.split('/')[1]||'png';
|
||||
return new File([blob],`screenshot-${Date.now()}.${ext}`,{type:i.type});
|
||||
});
|
||||
addFiles(files);
|
||||
setStatus(`Image pasted: ${files.map(f=>f.name).join(', ')}`);
|
||||
});
|
||||
document.querySelectorAll('.suggestion').forEach(btn=>{
|
||||
btn.onclick=()=>{$('msg').value=btn.dataset.msg;send();};
|
||||
});
|
||||
|
||||
// Boot: restore last session or start fresh
|
||||
// ── Resizable panels ──────────────────────────────────────────────────────
|
||||
(function(){
|
||||
const SIDEBAR_MIN=180, SIDEBAR_MAX=420;
|
||||
const PANEL_MIN=180, PANEL_MAX=500;
|
||||
|
||||
function initResize(handleId, targetEl, edge, minW, maxW, storageKey){
|
||||
const handle = $(handleId);
|
||||
if(!handle || !targetEl) return;
|
||||
|
||||
// Restore saved width
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if(saved) targetEl.style.width = saved + 'px';
|
||||
|
||||
let startX=0, startW=0;
|
||||
|
||||
handle.addEventListener('mousedown', e=>{
|
||||
e.preventDefault();
|
||||
startX = e.clientX;
|
||||
startW = targetEl.getBoundingClientRect().width;
|
||||
handle.classList.add('dragging');
|
||||
document.body.classList.add('resizing');
|
||||
|
||||
const onMove = ev=>{
|
||||
const delta = edge==='right' ? ev.clientX - startX : startX - ev.clientX;
|
||||
const newW = Math.min(maxW, Math.max(minW, startW + delta));
|
||||
targetEl.style.width = newW + 'px';
|
||||
};
|
||||
const onUp = ()=>{
|
||||
handle.classList.remove('dragging');
|
||||
document.body.classList.remove('resizing');
|
||||
localStorage.setItem(storageKey, parseInt(targetEl.style.width));
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
};
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
}
|
||||
|
||||
// Run after DOM ready (called from boot)
|
||||
window._initResizePanels = function(){
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const rightpanel = document.querySelector('.rightpanel');
|
||||
initResize('sidebarResize', sidebar, 'right', SIDEBAR_MIN, SIDEBAR_MAX, 'hermes-sidebar-w');
|
||||
initResize('rightpanelResize', rightpanel, 'left', PANEL_MIN, PANEL_MAX, 'hermes-panel-w');
|
||||
};
|
||||
})();
|
||||
|
||||
(async()=>{
|
||||
// Restore last-used model preference
|
||||
const savedModel=localStorage.getItem('hermes-webui-model');
|
||||
if(savedModel && $('modelSelect')){
|
||||
$('modelSelect').value=savedModel;
|
||||
// If the value didn't take (model not in list), clear the bad pref
|
||||
if($('modelSelect').value!==savedModel) localStorage.removeItem('hermes-webui-model');
|
||||
}
|
||||
// Pre-load workspace list so sidebar name is correct from first render
|
||||
await loadWorkspaceList();
|
||||
_initResizePanels();
|
||||
const saved=localStorage.getItem('hermes-webui-session');
|
||||
if(saved){
|
||||
try{await loadSession(saved);await renderSessionList();await checkInflightOnBoot(saved);return;}
|
||||
catch(e){localStorage.removeItem('hermes-webui-session');}
|
||||
}
|
||||
// no saved session - show empty state, wait for user to hit +
|
||||
$('emptyState').style.display='';
|
||||
await renderSessionList();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user