Files
astraea/templates/index.html

336 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Astraea — Your Attorney</title>
<style>
:root { --bg:#070b1a; --panel:#101832; --panel2:#141d3d; --line:#233055; --text:#e8edfb; --muted:#8b98c4; --gold:#f5d06f; }
* { box-sizing:border-box; margin:0; padding:0; }
body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; background:radial-gradient(1200px 700px at 50% -10%,#18254f 0%,var(--bg) 60%); color:var(--text); min-height:100vh; }
canvas { position:fixed; inset:0; z-index:0; pointer-events:none; }
.app { position:relative; z-index:1; max-width:1120px; margin:0 auto; padding:0 20px 60px; }
header { padding:20px 0; display:flex; align-items:center; justify-content:space-between; gap:14px; }
.brand { display:flex; align-items:center; gap:11px; }
.logo { width:40px; height:40px; border-radius:11px; background:linear-gradient(135deg,#6ea8ff,#a78bfa); display:grid; place-items:center; font-size:20px; }
.brand h1 { font-size:20px; font-weight:800; }
.brand h1 span { color:var(--gold); }
.top-actions { display:flex; align-items:center; gap:10px; }
.avatar-sm { width:36px; height:36px; border-radius:50%; display:grid; place-items:center; font-size:17px; color:#fff; cursor:pointer; }
.btn { border:1px solid var(--line); background:var(--panel2); color:var(--text); border-radius:10px; padding:9px 15px; font-size:13.5px; font-weight:600; cursor:pointer; transition:.15s; }
.btn:hover { border-color:#3b82f6; }
.btn.primary { background:linear-gradient(135deg,#3b82f6,#6366f1); border:none; color:#fff; }
.hero { padding:12px 0 20px; }
.hero h2 { font-size:clamp(24px,4vw,34px); font-weight:800; letter-spacing:-.5px; }
.hero p { color:var(--muted); margin-top:8px; font-size:15px; }
.label { font-size:11.5px; text-transform:uppercase; letter-spacing:2px; color:var(--muted); margin:14px 0 12px; }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(235px,1fr)); gap:13px; }
.card { background:linear-gradient(180deg,var(--panel2),var(--panel)); border:1px solid var(--line); border-radius:15px; padding:16px; cursor:pointer; position:relative; overflow:hidden; transition:.18s; }
.card::before { content:""; position:absolute; inset:0 0 auto 0; height:3px; background:var(--c,#7dd3fc); }
.card:hover { transform:translateY(-3px); border-color:var(--c,#7dd3fc); }
.card.active { border-color:var(--c,#7dd3fc); box-shadow:0 0 0 1px var(--c,#7dd3fc),0 14px 34px rgba(0,0,0,.5); }
.card .emoji { font-size:27px; }
.card h3 { font-size:15.5px; font-weight:700; margin:10px 0 3px; }
.card .tag { font-size:12px; color:var(--c,#7dd3fc); font-weight:600; margin-bottom:6px; }
.card p { font-size:12.5px; color:var(--muted); line-height:1.45; }
.chat-panel { margin-top:30px; background:linear-gradient(180deg,var(--panel2),var(--panel)); border:1px solid var(--line); border-radius:18px; display:flex; flex-direction:column; overflow:hidden; }
.chat-head { padding:14px 18px; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:11px; flex-wrap:wrap; }
.chat-head .emoji { font-size:24px; }
.chat-head .who { font-weight:700; font-size:15px; }
.chat-head .scope { color:var(--muted); font-weight:500; font-size:12px; }
.chips { margin-left:auto; display:flex; gap:6px; flex-wrap:wrap; }
.chip { font-size:11.5px; padding:5px 10px; border-radius:999px; border:1px solid var(--line); color:var(--muted); background:transparent; cursor:pointer; white-space:nowrap; }
.chip.on { color:#06121f; background:var(--c,#7dd3fc); border-color:var(--c,#7dd3fc); font-weight:700; }
.messages { padding:20px; min-height:220px; max-height:520px; overflow-y:auto; display:flex; flex-direction:column; gap:15px; }
.msg { max-width:80%; display:flex; flex-direction:column; }
.msg.user { align-self:flex-end; align-items:flex-end; }
.msg.agent { align-self:flex-start; }
.bubble { padding:12px 15px; border-radius:15px; font-size:14.5px; line-height:1.6; white-space:pre-wrap; word-break:break-word; }
.msg.user .bubble { background:linear-gradient(135deg,#3b82f6,#6366f1); color:#fff; border-bottom-right-radius:4px; }
.msg.agent .bubble { background:var(--panel2); border:1px solid var(--line); border-bottom-left-radius:4px; }
.msg .who-label { font-size:11px; color:var(--muted); margin:0 4px 4px; }
.speak { margin-top:6px; background:none; border:none; color:var(--gold); cursor:pointer; font-size:12px; }
.citations { margin-top:9px; }
.cite-toggle { font-size:12px; color:var(--gold); cursor:pointer; background:none; border:none; }
.cite-list { margin-top:7px; display:none; flex-direction:column; gap:7px; }
.cite-list.open { display:flex; }
.cite { background:#0c142e; border:1px solid var(--line); border-radius:9px; padding:9px 11px; font-size:12px; color:var(--muted); }
.cite b { color:var(--text); display:block; margin-bottom:2px; }
.typing { display:inline-flex; gap:5px; padding:6px 2px; }
.typing i { width:7px; height:7px; border-radius:50%; background:var(--muted); animation:bl 1.2s infinite; }
.typing i:nth-child(2){ animation-delay:.2s; } .typing i:nth-child(3){ animation-delay:.4s; }
@keyframes bl { 0%,100%{opacity:.25;} 50%{opacity:1;} }
.composer { border-top:1px solid var(--line); padding:13px 15px; display:flex; gap:9px; align-items:flex-end; }
.composer textarea { flex:1; resize:none; background:#0c142e; border:1px solid var(--line); color:var(--text); border-radius:11px; padding:11px 13px; font-size:14.5px; font-family:inherit; line-height:1.5; max-height:130px; outline:none; }
.composer textarea:focus { border-color:var(--c,#7dd3fc); }
.send { border:none; border-radius:11px; padding:12px 18px; font-size:14px; font-weight:700; color:#06121f; background:var(--c,#7dd3fc); cursor:pointer; }
.send:disabled { opacity:.45; }
.tools-row { display:flex; gap:8px; padding:0 15px 12px; flex-wrap:wrap; }
.tools-row .btn { font-size:12.5px; padding:8px 13px; }
.modal-bg { position:fixed; inset:0; background:rgba(3,5,12,.8); z-index:50; display:none; align-items:center; justify-content:center; padding:20px; }
.modal-bg.open { display:flex; }
.modal { background:linear-gradient(180deg,var(--panel2),var(--panel)); border:1px solid var(--line); border-radius:18px; width:100%; max-width:620px; max-height:90vh; overflow-y:auto; padding:26px; }
.modal h2 { font-size:20px; font-weight:800; margin-bottom:4px; }
.modal .sub { color:var(--muted); font-size:13px; margin-bottom:18px; }
.modal .close { float:right; background:none; border:none; color:var(--muted); font-size:22px; cursor:pointer; }
.fld { margin-bottom:15px; }
.fld label { display:block; font-size:12.5px; color:var(--muted); margin-bottom:6px; }
.fld input, .fld textarea, .fld select { width:100%; background:#0c142e; border:1px solid var(--line); color:var(--text); border-radius:10px; padding:11px 13px; font-size:14px; outline:none; font-family:inherit; }
.fld textarea { min-height:110px; resize:vertical; }
.fld input:focus, .fld textarea:focus { border-color:#6366f1; }
.avatar-grid { display:grid; grid-template-columns:repeat(6,1fr); gap:10px; margin-bottom:18px; }
.avatar-opt { aspect-ratio:1; border-radius:50%; display:grid; place-items:center; font-size:22px; color:#fff; cursor:pointer; border:2px solid transparent; }
.avatar-opt.sel { border-color:#fff; box-shadow:0 0 0 3px rgba(255,255,255,.2); }
.switch { display:flex; align-items:center; gap:10px; }
.switch input { width:auto; }
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:13px; }
.doc-result { background:#0c142e; border:1px solid var(--line); border-radius:12px; padding:16px; font-size:13px; white-space:pre-wrap; max-height:50vh; overflow-y:auto; line-height:1.6; margin-top:14px; }
footer { text-align:center; margin-top:26px; font-size:12px; color:var(--muted); }
footer a { color:var(--muted); }
@media (max-width:640px){ .msg{max-width:92%;} .grid2{grid-template-columns:1fr;} .avatar-grid{grid-template-columns:repeat(4,1fr);} }
</style>
</head>
<body>
<canvas id="stars"></canvas>
<div class="app">
<header>
<div class="brand"><div class="logo">⚖️</div><div><h1>ASTRA<span>EA</span></h1></div></div>
<div class="top-actions">
<button class="btn" onclick="openDocs()">📄 Documents</button>
<button class="btn" onclick="openSettings()">⚙️ Settings</button>
<div class="avatar-sm" id="myAvatar" onclick="openProfile()" style="background:linear-gradient(135deg,#6ea8ff,#a78bfa)">⚖</div>
</div>
</header>
<div class="hero">
<h2>Eight specialists. Your case.</h2>
<p>Ask anything. Every answer is grounded in Washington statute and verified case law — and it already knows your situation.</p>
</div>
<div class="label">Choose your specialist</div>
<div class="grid" id="grid"></div>
<section class="chat-panel" id="chatPanel">
<div class="chat-head">
<div class="emoji" id="headEmoji">🧭</div>
<div><div class="who" id="headName">Navigator</div><div class="scope" id="headScope">Where to start</div></div>
<div class="chips" id="chips"></div>
</div>
<div class="messages" id="messages"></div>
<div class="tools-row">
<button class="btn" onclick="openDocs()">📄 Prep a document</button>
<button class="btn" onclick="openMission()">📨 Send on my behalf</button>
<button class="btn" onclick="clearConv()">🗑 Clear chat</button>
</div>
<div class="composer">
<button class="btn" id="micBtn" onclick="toggleMic()" title="Speak" style="flex:0 0 auto">🎤</button>
<textarea id="input" rows="1" placeholder="Ask your attorney…"></textarea>
<button class="send" id="sendBtn">Ask</button>
</div>
</section>
<footer>Astraea gives practical legal guidance grounded in Washington law — not a substitute for a licensed attorney. · <a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener">☕ Support</a></footer>
</div>
<!-- Profile modal -->
<div class="modal-bg" id="profileModal"><div class="modal">
<button class="close" onclick="closeModal('profileModal')">×</button>
<h2>Your profile</h2><div class="sub">This context shapes every answer — your attorney already knows your situation.</div>
<div class="fld"><label>Display name</label><input id="pfName" placeholder="Your name"></div>
<div class="fld"><label>Display picture</label></div>
<div class="avatar-grid" id="avatarGrid"></div>
<div class="fld"><label>About Me — everything your attorney should know (your story, dates, names, what you're going through)</label><textarea id="pfAbout" placeholder="e.g. Married 2011, two children (ages 9 and 12), separated June 2025, own a home in King County, spouse filed a parenting plan…"></textarea></div>
<button class="btn primary" style="width:100%" onclick="saveProfile()">Save profile</button>
</div></div>
<!-- Settings modal -->
<div class="modal-bg" id="settingsModal"><div class="modal">
<button class="close" onclick="closeModal('settingsModal')">×</button>
<h2>Settings</h2><div class="sub">Comms channels your attorney can use for missions, and safety.</div>
<div class="fld"><label>Twilio Account SID</label><input id="twSid" placeholder="AC…"></div>
<div class="fld"><label>Twilio Auth Token</label><input id="twToken" type="password" placeholder="••••"></div>
<div class="fld"><label>Twilio From number (E.164)</label><input id="twFrom" placeholder="+14255550123"></div>
<div class="grid2">
<div class="fld"><label>SMTP host</label><input id="smHost" placeholder="smtp.gmail.com"></div>
<div class="fld"><label>SMTP port</label><input id="smPort" value="587"></div>
</div>
<div class="grid2">
<div class="fld"><label>SMTP username</label><input id="smUser"></div>
<div class="fld"><label>SMTP password</label><input id="smPass" type="password"></div>
</div>
<div class="fld"><label>Email "from" address</label><input id="emFrom" placeholder="you@example.com"></div>
<div class="fld"><label>TTS voice</label><select id="ttsVoice"></select></div>
<div class="fld switch"><label style="margin:0">🔒 No-contact order in effect</label><input type="checkbox" id="ncOrder"></div>
<div class="fld"><label>Protected person's name (blocked from contact)</label><input id="ncName" placeholder="Full name of the person you must not contact"></div>
<div class="fld switch"><label style="margin:0">⚠️ Domestic violence is a factor in my case</label><input type="checkbox" id="dvFactor"></div>
<div class="fld switch"><label style="margin:0">🛡️ Active protection order in effect (RCW 7.105)</label><input type="checkbox" id="poOrder"></div>
<div class="fld"><label>Safety notes — threats, incidents, what you're afraid of</label><textarea id="safetyNotes" style="min-height:70px" placeholder="e.g. There has been physical violence. I'm afraid for my safety and the children's safety."></textarea></div>
<button class="btn primary" style="width:100%" onclick="saveSettings()">Save settings</button>
</div></div>
<!-- Documents modal -->
<div class="modal-bg" id="docsModal"><div class="modal">
<button class="close" onclick="closeModal('docsModal')">×</button>
<h2>Prepare a document</h2><div class="sub">Court-ready Washington divorce paperwork, drafted to your facts.</div>
<div class="fld"><label>Document</label><select id="docType"></select></div>
<div class="fld"><label>Specific instructions (optional)</label><textarea id="docInst" placeholder="e.g. I want primary residential time with the children, propose alternate weekends for the other parent."></textarea></div>
<button class="btn primary" style="width:100%" id="docBtn" onclick="genDoc()">Generate document</button>
<div class="doc-result" id="docResult" style="display:none"></div>
</div></div>
<!-- Mission modal -->
<div class="modal-bg" id="missionModal"><div class="modal">
<button class="close" onclick="closeModal('missionModal')">×</button>
<h2>Send on your behalf</h2><div class="sub">Your attorney will format this professionally. A no-contact order blocks protected recipients.</div>
<div class="fld"><label>Type</label><select id="msType"><option value="sms">Text (SMS)</option><option value="email">Email</option><option value="call">Phone call</option></select></div>
<div class="fld"><label>Recipient (phone E.164 or email)</label><input id="msRecipient" placeholder="+14255550123 or name@example.com"></div>
<div class="fld" id="msSubjectWrap" style="display:none"><label>Subject</label><input id="msSubject"></div>
<div class="fld"><label>Message</label><textarea id="msBody" placeholder="What do you want to say?"></textarea></div>
<button class="btn primary" style="width:100%" onclick="doMission()">Send</button>
<div id="msStatus" style="margin-top:12px;font-size:13px"></div>
</div></div>
<script>
const AGENTS = {{ agents | tojson }};
const token = localStorage.getItem('astraea_token') || '';
let active = AGENTS[0].id;
let avatars = [];
let myProfile = {display_name:'', avatar:0, about_me:''};
let mySettings = {};
const history = {};
AGENTS.forEach(a => history[a.id] = []);
// starfield
const cvs = document.getElementById('stars'), ctx = cvs.getContext('2d');
let stars = [];
(function size(){ cvs.width=innerWidth; cvs.height=innerHeight; stars=Array.from({length:120},()=>({x:Math.random()*cvs.width,y:Math.random()*cvs.height,r:Math.random()*1.3+.3,t:Math.random()*6.28,s:Math.random()*.02+.005})); })();
addEventListener('resize',()=>{cvs.width=innerWidth;cvs.height=innerHeight;});
(function draw(){ ctx.clearRect(0,0,cvs.width,cvs.height); for(const s of stars){ s.t+=s.s; const a=.3+Math.abs(Math.sin(s.t))*.7; ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,6.28); ctx.fillStyle=`rgba(190,210,255,${a})`; ctx.fill(); } requestAnimationFrame(draw); })();
function authHeaders(){ return token ? {'Authorization':'Bearer '+token} : {}; }
function accent(id){ const a=AGENTS.find(x=>x.id===id); return a?a.accent:'#7dd3fc'; }
function closeModal(id){ document.getElementById(id).classList.remove('open'); }
function renderGrid(){
const g=document.getElementById('grid'); g.innerHTML='';
AGENTS.forEach(a=>{ const c=document.createElement('div'); c.className='card'+(a.id===active?' active':''); c.style.setProperty('--c',a.accent); c.innerHTML=`<div class="emoji">${a.emoji}</div><h3>${a.name}</h3><div class="tag">${a.tagline}</div><p>${a.description}</p>`; c.onclick=()=>selectAgent(a.id,true); g.appendChild(c); });
}
function renderChips(){
const c=document.getElementById('chips'); c.innerHTML='';
AGENTS.forEach(a=>{ const b=document.createElement('button'); b.className='chip'+(a.id===active?' on':''); b.style.setProperty('--c',a.accent); b.textContent=a.emoji+' '+a.name.split(' & ')[0].split(' ')[0]; b.onclick=()=>selectAgent(a.id,false); c.appendChild(b); });
}
async function selectAgent(id,scroll){
active=id; const a=AGENTS.find(x=>x.id===id);
document.getElementById('headEmoji').textContent=a.emoji;
document.getElementById('headName').textContent=a.name;
document.getElementById('headScope').textContent=a.tagline;
const cp=document.getElementById('chatPanel'); cp.style.setProperty('--c',a.accent); cp.style.borderColor=a.accent+'55';
renderGrid(); renderChips();
const m=document.getElementById('messages'); m.innerHTML='';
if(token && !(history[id]||[]).length){
try{ const r=await fetch('/api/conversation/'+id,{headers:authHeaders()}); const d=await r.json(); history[id]=d.history||[]; }catch(e){}
}
(history[id]||[]).forEach(t=>addBubble(t.role,t.content,t.citations));
if(scroll) cp.scrollIntoView({behavior:'smooth',block:'start'});
}
async function clearConv(){
if(token){ try{ await fetch('/api/conversation/'+active,{method:'DELETE',headers:authHeaders()}); }catch(e){} }
history[active]=[]; document.getElementById('messages').innerHTML='';
}
let rec=null;
function toggleMic(){
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;
const mb=document.getElementById('micBtn');
if(!SR){ mb.title='Voice input needs Chrome/Edge'; return; }
if(rec){ rec.stop(); rec=null; mb.classList.remove('on'); mb.style.borderColor=''; return; }
rec=new SR(); rec.lang='en-US'; rec.interimResults=false;
rec.onresult=e=>{ const t=e.results[0][0].transcript; const i=document.getElementById('input'); i.value=(i.value?i.value+' ':'')+t; };
rec.onend=()=>{ rec=null; mb.classList.remove('on'); mb.style.borderColor=''; };
rec.onerror=()=>{ rec=null; mb.classList.remove('on'); mb.style.borderColor=''; };
rec.start(); mb.classList.add('on'); mb.style.borderColor='#f87171';
}
function addBubble(role,text,citations){
const m=document.createElement('div'); m.className='msg '+role;
const l=document.createElement('div'); l.className='who-label'; l.textContent=role==='user'?'You':(AGENTS.find(x=>x.id===active)?.name||'Attorney');
const b=document.createElement('div'); b.className='bubble'; b.textContent=text;
m.appendChild(l); m.appendChild(b);
if(role==='assistant'){
const sp=document.createElement('button'); sp.className='speak'; sp.textContent='🔊 Listen'; sp.onclick=()=>speak(text); m.appendChild(sp);
if(citations&&citations.length){ const cw=document.createElement('div'); cw.className='citations'; const t=document.createElement('button'); t.className='cite-toggle'; t.textContent='🔗 '+citations.length+' source'+(citations.length>1?'s':''); const cl=document.createElement('div'); cl.className='cite-list'; citations.forEach(c=>{ const it=document.createElement('div'); it.className='cite'; it.innerHTML=`<b>${c.title}</b>${c.snippet}<div style="color:var(--gold);font-size:10.5px">${c.source}</div>`; cl.appendChild(it); }); t.onclick=()=>cl.classList.toggle('open'); cw.appendChild(t); cw.appendChild(cl); m.appendChild(cw); }
}
document.getElementById('messages').appendChild(m);
document.getElementById('messages').scrollTop=document.getElementById('messages').scrollHeight;
}
function typing(on){ document.querySelectorAll('.typing').forEach(e=>e.remove()); if(!on)return; const m=document.createElement('div'); m.className='msg agent'; const t=document.createElement('div'); t.className='typing'; t.innerHTML='<i></i><i></i><i></i>'; m.appendChild(t); document.getElementById('messages').appendChild(m); }
async function speak(text){
try{ const r=await fetch('/api/speak',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:text.replace(/\n+/g,'. ').substring(0,2500),voice:mySettings.tts_voice||'aria'})}); const b=await r.blob(); new Audio(URL.createObjectURL(b)).play(); }catch(e){}
}
async function send(){
const t=document.getElementById('input').value.trim(); if(!t)return;
document.getElementById('input').value='';
addBubble('user',t); history[active].push({role:'user',content:t});
typing(true); document.getElementById('sendBtn').disabled=true;
try{
const r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({agent_id:active,message:t,history:history[active].slice(0,-1)})});
const d=await r.json(); typing(false);
if(d.answer){ addBubble('assistant',d.answer,d.citations); history[active].push({role:'assistant',content:d.answer,citations:d.citations}); }
else addBubble('assistant',d.error||'Something went wrong.',null);
}catch(e){ typing(false); addBubble('assistant','Could not reach the engine.',null); }
finally{ document.getElementById('sendBtn').disabled=false; }
}
async function loadAvatars(){ const r=await fetch('/api/avatars'); avatars=await r.json(); const g=document.getElementById('avatarGrid'); g.innerHTML=''; avatars.forEach(a=>{ const d=document.createElement('div'); d.className='avatar-opt'; d.style.background=`linear-gradient(135deg,${a.c1},${a.c2})`; d.textContent=a.glyph; d.title=a.label; d.onclick=()=>{ myProfile.avatar=a.id; renderAvatarSel(); }; g.appendChild(d); }); }
function renderAvatarSel(){ document.querySelectorAll('.avatar-opt').forEach((d,i)=>d.classList.toggle('sel',i===myProfile.avatar)); }
function renderMyAvatar(){ const a=avatars[myProfile.avatar]||avatars[0]; const el=document.getElementById('myAvatar'); if(a){ el.style.background=`linear-gradient(135deg,${a.c1},${a.c2})`; el.textContent=a.glyph; } }
function openProfile(){ document.getElementById('pfName').value=myProfile.display_name||''; document.getElementById('pfAbout').value=myProfile.about_me||''; renderAvatarSel(); document.getElementById('profileModal').classList.add('open'); }
async function saveProfile(){
const body={display_name:document.getElementById('pfName').value, about_me:document.getElementById('pfAbout').value, avatar:myProfile.avatar};
const r=await fetch('/api/profile',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify(body)});
myProfile=await r.json(); renderMyAvatar(); closeModal('profileModal');
}
function openSettings(){
const s=mySettings; document.getElementById('twSid').value=s.twilio_sid||''; document.getElementById('twToken').value=''; document.getElementById('twFrom').value=s.twilio_from||''; document.getElementById('smHost').value=s.smtp_host||''; document.getElementById('smPort').value=s.smtp_port||587; document.getElementById('smUser').value=s.smtp_user||''; document.getElementById('emFrom').value=s.email_from||''; document.getElementById('ttsVoice').value=s.tts_voice||'aria'; document.getElementById('ncOrder').checked=!!s.no_contact_order; document.getElementById('ncName').value=s.partner_name||''; document.getElementById('dvFactor').checked=!!s.domestic_violence; document.getElementById('poOrder').checked=!!s.protection_order; document.getElementById('safetyNotes').value=s.safety_notes||'';
document.getElementById('settingsModal').classList.add('open');
}
async function saveSettings(){
const body={ twilio_sid:document.getElementById('twSid').value, twilio_from:document.getElementById('twFrom').value, smtp_host:document.getElementById('smHost').value, smtp_port:parseInt(document.getElementById('smPort').value||587), smtp_user:document.getElementById('smUser').value, email_from:document.getElementById('emFrom').value, tts_voice:document.getElementById('ttsVoice').value, no_contact_order:document.getElementById('ncOrder').checked?1:0, partner_name:document.getElementById('ncName').value, domestic_violence:document.getElementById('dvFactor').checked?1:0, protection_order:document.getElementById('poOrder').checked?1:0, safety_notes:document.getElementById('safetyNotes').value };
const tw=document.getElementById('twToken').value; if(tw) body.twilio_auth_token=tw;
const sp=document.getElementById('smPass').value; if(sp) body.smtp_pass=sp;
const r=await fetch('/api/settings',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify(body)});
mySettings=await r.json(); closeModal('settingsModal');
}
async function openDocs(){ const r=await fetch('/api/documents/types'); const types=await r.json(); const s=document.getElementById('docType'); s.innerHTML=''; types.forEach(t=>{ const o=document.createElement('option'); o.value=t.id; o.textContent=t.name+' ('+t.form+')'; s.appendChild(o); }); document.getElementById('docResult').style.display='none'; document.getElementById('docsModal').classList.add('open'); }
async function genDoc(){
document.getElementById('docBtn').disabled=true;
const body={doc_type:document.getElementById('docType').value, instructions:document.getElementById('docInst').value};
const r=await fetch('/api/documents',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify(body)});
const d=await r.json(); document.getElementById('docBtn').disabled=false;
const el=document.getElementById('docResult'); el.style.display='block'; el.textContent=d.document||d.error;
}
function openMission(){ document.getElementById('missionModal').classList.add('open'); }
document.getElementById('msType').onchange=function(){ document.getElementById('msSubjectWrap').style.display=this.value==='email'?'block':'none'; };
async function doMission(){
const type=document.getElementById('msType').value, recipient=document.getElementById('msRecipient').value;
const body={type,recipient,message:document.getElementById('msBody').value,subject:document.getElementById('msSubject').value,body:document.getElementById('msBody').value};
const r=await fetch('/api/mission',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify(body)});
const d=await r.json(); document.getElementById('msStatus').textContent=d.ok?'✓ Sent.':('✗ '+(d.error||'Failed')); document.getElementById('msStatus').style.color=d.ok?'#4ade80':'#fca5a5';
}
document.getElementById('sendBtn').onclick=send;
document.getElementById('input').addEventListener('keydown',e=>{ if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();} });
async function boot(){
if(!token){ window.location.href='/'; return; }
try{ const r=await fetch('/api/me',{headers:authHeaders()}); if(!r.ok){ window.location.href='/'; return; } const d=await r.json(); myProfile=d.profile; mySettings=d.settings; }catch(e){}
await loadAvatars(); renderMyAvatar(); renderGrid(); renderChips(); selectAgent(active,false);
// populate tts voices
try{ const vr=await fetch('/api/voices'); const vd=await vr.json(); const s=document.getElementById('ttsVoice'); s.innerHTML=''; vd.voices.forEach(v=>{ const o=document.createElement('option'); o.value=v.id; o.textContent=v.label; s.appendChild(o); }); s.value=mySettings.tts_voice||'aria'; }catch(e){}
}
boot();
</script>
</body>
</html>