Files
sausage/userland/chat/index.html
T

79 lines
6.0 KiB
HTML

<!doctype html>
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Sausage chat</title>
<style>
body{font:16px system-ui;margin:0;color:#202124;background:#fafafa}header{padding:1rem;border-bottom:1px solid #ccc}main{display:flex;height:calc(100dvh - 5rem)}nav{width:16rem;overflow:auto;padding:.6rem;border-right:1px solid #ccc}button,textarea{font:inherit}button{cursor:pointer;padding:.5rem}nav button{display:block;width:100%;text-align:left;margin-bottom:.4rem}button[aria-current=true]{background:#dceaff}section{flex:1;min-width:0;display:flex;flex-direction:column;padding:1rem}#entries{flex:1;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere}.entry{padding:.7rem;border-bottom:1px solid #ddd}.user{background:#eef4ff}pre{white-space:pre-wrap;overflow-wrap:anywhere}form{display:flex;gap:.5rem;padding-top:1rem}textarea{flex:1;min-width:0}#approvals{background:#fff1c1}#error{color:#a00}small{display:block}a{margin-right:1rem}@media(max-width:600px){nav{width:8rem}section{padding:.5rem}form{flex-wrap:wrap}}
</style>
<header><strong>Sausage chat</strong><span id="error" role="status"></span><div id="approvals" aria-live="polite"></div></header>
<main><nav id="conversations" aria-label="Conversations"></nav><section><h1 id="title">Conversation</h1><div id="entries" role="log" aria-live="polite"></div><form id="compose"><textarea id="input" aria-label="Message" rows="3" required></textarea><button>Send</button><button id="stop" type="button">Stop</button></form></section></main>
<script>
'use strict';
const $=id=>document.getElementById(id);
let selected=null,after=0,pending=null,sequence=0;
async function api(path,body){
const response=await fetch('/api/'+path,body===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const value=await response.json();
if(!response.ok)throw new Error(value.error||response.statusText);
return value;
}
const text=value=>typeof value==='string'?value:Array.isArray(value)?value.map(part=>part.type==='text'?part.text:'').join(''):'';
function entry(record){
if(!['pi.user','pi.assistant','pi.tool-result'].includes(record.kind))return;
const box=document.createElement('div');box.className='entry '+(record.kind==='pi.user'?'user':'');
const messages=Array.isArray(record.model)?record.model:[record.model||record.data||record];
for(const message of messages){
const calls=Array.isArray(message.content)?message.content.filter(part=>part.type==='toolCall'):[];
if(record.kind==='pi.tool-result'||calls.length){
const details=document.createElement('details'),summary=document.createElement('summary'),result=document.createElement('pre');
const name=message.toolName||message.name||calls.map(call=>call.name).join(', ')||'Tool';
const content=text(message.content)||JSON.stringify(message);
summary.textContent=name+' — '+content.slice(0,140);result.textContent=content;
details.append(summary,result);box.append(details);
}
const content=text(message.content)||text(message.text);
if(content&&record.kind!=='pi.tool-result'){
const label=document.createElement('strong'),paragraph=document.createElement('div');
label.textContent=record.kind==='pi.user'?'You':'Assistant';paragraph.textContent=content;box.append(label,paragraph);
}
}
if(box.childNodes.length)$('entries').append(box);
}
function select(id){selected=id;after=0;$('entries').replaceChildren();}
async function refresh(){
try{
const {conversations}=await api('conversations');
conversations.sort((a,b)=>a.id-b.id);
if(selected===null&&conversations.length)select((conversations.find(c=>!c.parent&&!c.owner)||conversations[0]).id);
$('conversations').replaceChildren();
const parent=c=>c.parent?.conversationId||c.owner?.conversationId||c.starter;
for(const c of conversations){
const button=document.createElement('button'),label=document.createElement('small'),p=parent(c);
button.type='button';button.setAttribute('aria-current',String(c.id===selected));
button.textContent=c.title||'Conversation #'+c.id;
label.textContent=(c.running?'Running':c.concluded?'Concluded':'Idle')+(p?' · parent #'+p+' · child '+(conversations.filter(other=>parent(other)===p).findIndex(other=>other.id===c.id)+1):' · root');
button.append(label);button.onclick=()=>select(c.id);$('conversations').append(button);
if(c.id===selected)$('title').textContent=c.title||'Conversation #'+c.id;
}
if(selected!==null){
const id=selected,wasNearBottom=$('entries').scrollHeight-$('entries').scrollTop-$('entries').clientHeight<80;
const {entries}=await api('conversations/'+id+'/entries?after='+after);
if(id===selected){for(const item of entries){entry(item);after=Math.max(after,item.id);}if(wasNearBottom)$('entries').scrollTop=$('entries').scrollHeight;}
}
const {requests}=await api('credential-requests');$('approvals').replaceChildren();
for(const request of requests){
const link=document.createElement('a');link.textContent='Approve: '+(request.summary||request.id);link.href=request.approvalUrl;link.target='_blank';link.rel='noopener';$('approvals').append(link);
}
$('error').textContent='';
}catch(error){$('error').textContent=' '+error.message;}
setTimeout(refresh,1000);
}
$('compose').onsubmit=async event=>{
event.preventDefault();const content=$('input').value;
if(selected===null||!content.trim())return;
if(!pending||pending.content!==content||pending.conversation!==selected)pending={content,conversation:selected,requestId:Date.now().toString(36)+'-'+(++sequence)+'-'+Math.random().toString(36).slice(2)};
try{await api('conversations/'+pending.conversation+'/submit',pending);if($('input').value===content)$('input').value='';pending=null;}catch(error){$('error').textContent=' '+error.message;}
};
$('stop').onclick=async()=>{if(selected!==null)try{await api('conversations/'+selected+'/stop',{});}catch(error){$('error').textContent=' '+error.message;}};
refresh();
</script></html>