Files
hermes-webui/static/share.js
T
nesquena-hermes e061e8534c Rebase #5337 (public read-only conversation sharing) onto master
Rebased 363 commits forward (branch-point 2026-07-05). Applied the PR's code
diff cleanly across 16 files; resolved api/models.py additively (both master's
process_wakeup_pause/anchor_scene_index fields AND the PR's share_token/
share_created_at fields kept in the Session constructor, metadata list, and
to_dict). CI workflow (.github/workflows/tests.yml) intentionally NOT taken from
the stale branch — master owns it (the branch's apparent removal of Playwright/
Office steps was a PAT-workflow-scope revert; rebase restores master's CI).

Co-authored-by: MicoRobot. Concept approved by maintainer (opt-in public share,
2026-07-11). Note: 4 dynamic share tests fail identically on the original
unrebased head (pre-existing test-env Session.load fragility, not rebase-caused);
to be resolved during the security re-gate in a proper sandbox.
2026-07-11 01:39:42 +00:00

120 lines
3.7 KiB
JavaScript

function _shareTokenFromPath(){
const path=window.location.pathname||'';
const m=path.match(/\/share\/([^/?#]+)/);
return m?decodeURIComponent(m[1]):'';
}
function _shareEscapeHtml(text){
return String(text==null?'':text)
.replace(/&/g,'&')
.replace(/</g,'&lt;')
.replace(/>/g,'&gt;')
.replace(/"/g,'&quot;')
.replace(/'/g,'&#39;');
}
function _shareRoleLabel(role){
if(role==='user') return 'User';
if(role==='assistant') return 'Assistant';
return 'System';
}
function _shareRenderMessages(messages){
const wrap=$('shareTranscript');
if(!wrap) return;
if(!Array.isArray(messages)||!messages.length){
wrap.innerHTML='<div class="share-empty">This shared conversation has no visible messages.</div>';
return;
}
wrap.innerHTML='';
messages.forEach(msg=>{
const row=document.createElement('article');
row.className='share-message';
row.dataset.role=String(msg.role||'assistant');
const bodyHtml=(typeof renderMd==='function')
? renderMd(String(msg.content||''))
: `<p>${_shareEscapeHtml(msg.content||'')}</p>`;
row.innerHTML=
`<div class="share-role"><span class="share-role-badge">${_shareEscapeHtml(_shareRoleLabel(msg.role))}</span></div>`+
`<div class="msg-body share-message-body">${bodyHtml}</div>`;
wrap.appendChild(row);
});
if(typeof highlightCode==='function') highlightCode(wrap);
}
function _shareSetError(message){
const title=$('shareTitle');
const meta=$('shareMeta');
const wrap=$('shareTranscript');
if(title) title.textContent='Share unavailable';
if(meta) meta.textContent='This public snapshot could not be loaded.';
if(wrap) wrap.innerHTML=`<div class="share-error"><strong>Could not open this share.</strong><div style="margin-top:8px">${_shareEscapeHtml(message||'The link may have expired or been revoked.')}</div></div>`;
}
async function _shareLoad(){
const token=_shareTokenFromPath();
if(!token){
_shareSetError('Missing share token.');
return;
}
try{
const data=await fetch(new URL(`/api/share/${encodeURIComponent(token)}`,window.location.origin).href,{credentials:'same-origin',cache:'no-store'});
if(!data.ok){
let message='The link may have expired or been revoked.';
try{
const payload=await data.json();
if(payload&&payload.error) message=payload.error;
}catch(_){}
_shareSetError(message);
return;
}
const payload=await data.json();
const share=payload&&payload.share;
if(!share||!Array.isArray(share.messages)){
_shareSetError('Malformed share payload.');
return;
}
const title=$('shareTitle');
const meta=$('shareMeta');
if(title) title.textContent=share.title||'Untitled';
if(meta){
const count=Number(share.message_count||share.messages.length||0);
meta.textContent=`${count} message${count===1?'':'s'} - public read-only snapshot`;
}
_shareRenderMessages(share.messages);
}catch(err){
_shareSetError(err&&err.message?err.message:String(err||'Failed to load share.'));
}
}
async function _shareCopyLink(){
const btn=$('shareCopyBtn');
const text=window.location.href;
const done=()=>{
if(btn){
const original=btn.textContent;
btn.textContent='Copied!';
setTimeout(()=>{btn.textContent=original;},1200);
}
};
if(typeof _copyText==='function'){
try{
await _copyText(text);
done();
return;
}catch(_){}
}
try{
if(navigator&&navigator.clipboard&&navigator.clipboard.writeText){
await navigator.clipboard.writeText(text);
done();
}
}catch(_){}
}
document.addEventListener('DOMContentLoaded',()=>{
const copyBtn=$('shareCopyBtn');
if(copyBtn) copyBtn.addEventListener('click',_shareCopyLink);
_shareLoad();
});