Advanced mode: chat-driven form filling (fill_form tool), 10 official WA forms auto-grabbed + auto-mapped, doc vault, web search, per-user doc RAG
This commit is contained in:
@@ -83,6 +83,18 @@
|
||||
.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); }
|
||||
.progbar { height:6px; background:var(--panel2); border-radius:3px; overflow:hidden; margin:14px 0 18px; }
|
||||
.progbar > div { height:100%; background:linear-gradient(90deg,#6ea8ff,#a78bfa); border-radius:3px; transition:width .2s; }
|
||||
.intake-q { font-size:16px; font-weight:600; margin-bottom:12px; line-height:1.4; }
|
||||
.intake-nav { display:flex; justify-content:space-between; margin-top:14px; gap:10px; }
|
||||
.intake-nav .btn { flex:1; }
|
||||
.modal.wide { width:min(880px,94vw); max-height:90vh; overflow:auto; }
|
||||
.file-list { max-height:240px; overflow:auto; display:flex; flex-direction:column; gap:6px; }
|
||||
.file-item { display:flex; align-items:center; gap:8px; padding:8px 10px; background:var(--panel2); border-radius:8px; font-size:13px; }
|
||||
.file-item .nm { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.file-item .cat { color:var(--muted); font-size:11px; }
|
||||
.file-item button { background:none; border:1px solid var(--line); color:var(--text); border-radius:6px; padding:4px 9px; cursor:pointer; font-size:12px; }
|
||||
.file-item button:hover { background:var(--line); }
|
||||
.switch { display:flex; align-items:center; gap:10px; }
|
||||
.switch input { width:auto; }
|
||||
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:13px; }
|
||||
@@ -121,6 +133,7 @@
|
||||
<div class="messages" id="messages"></div>
|
||||
<div class="tools-row">
|
||||
<button class="btn" onclick="openDocs()">📄 Prep a document</button>
|
||||
<button class="btn" onclick="openFill()">📝 Fill a form</button>
|
||||
<button class="btn" onclick="openMission()">📨 Send on my behalf</button>
|
||||
<button class="btn" onclick="clearConv()">🗑 Clear chat</button>
|
||||
</div>
|
||||
@@ -141,10 +154,45 @@
|
||||
<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>
|
||||
<div class="fld"><label>Or upload your own photo</label><input type="file" id="pfPic" accept="image/*" onchange="uploadPic()"><span id="pfPicPreview"></span></div>
|
||||
<div class="fld"><label>About Me — everything your attorney should know</label><button class="btn" style="width:100%;margin-bottom:8px" onclick="openIntake()">✍️ Guided intake — answer questions step by step</button><textarea id="pfAbout" placeholder="…or write it freely here. 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>
|
||||
<div class="fld"><label>📁 Case documents — upload & organize your paperwork</label>
|
||||
<div style="display:flex;gap:8px;margin-bottom:8px"><select id="fileCat" style="width:auto"></select><input type="file" id="fileInput" multiple onchange="uploadFiles()" style="flex:1"></div>
|
||||
<div id="fileList" class="file-list"><div class="sub">No documents yet — upload your petition, orders, agreements, and evidence.</div></div>
|
||||
</div>
|
||||
<button class="btn primary" style="width:100%" onclick="saveProfile()">Save profile</button>
|
||||
</div></div>
|
||||
|
||||
<!-- Guided intake modal -->
|
||||
<div class="modal-bg" id="intakeModal"><div class="modal">
|
||||
<button class="close" onclick="closeModal('intakeModal')">×</button>
|
||||
<h2>Guided intake</h2><div class="sub">Answer a few questions — your attorney uses these as context for every answer.</div>
|
||||
<div class="progbar"><div id="intakeBar"></div></div>
|
||||
<div class="intake-q" id="intakeQ"></div>
|
||||
<input id="intakeA" placeholder="">
|
||||
<div class="intake-nav">
|
||||
<button class="btn" id="intakeBack" onclick="intakePrev()">← Back</button>
|
||||
<button class="btn primary" id="intakeNext" onclick="intakeNext()">Next →</button>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<!-- Document viewer modal -->
|
||||
<div class="modal-bg" id="viewerModal"><div class="modal wide">
|
||||
<button class="close" onclick="closeModal('viewerModal')">×</button>
|
||||
<h2 id="viewerTitle"></h2>
|
||||
<div id="viewerBody" style="margin-top:12px"></div>
|
||||
</div></div>
|
||||
|
||||
<!-- Fill a form modal -->
|
||||
<div class="modal-bg" id="fillModal"><div class="modal">
|
||||
<button class="close" onclick="closeModal('fillModal')">×</button>
|
||||
<h2>Fill a court form</h2><div class="sub">Your answers are typed onto the official WA form PDF, ready to print and file.</div>
|
||||
<div class="fld"><label>Form</label><select id="fillFormSelect"></select></div>
|
||||
<button class="btn" style="width:100%;margin-bottom:12px" onclick="autofillForm()">✨ Auto-fill from my info</button>
|
||||
<div id="fillFields"></div>
|
||||
<button class="btn primary" style="width:100%" onclick="generateForm()">⬇ Generate filled PDF</button>
|
||||
</div></div>
|
||||
|
||||
<!-- Settings modal -->
|
||||
<div class="modal-bg" id="settingsModal"><div class="modal">
|
||||
<button class="close" onclick="closeModal('settingsModal')">×</button>
|
||||
@@ -284,8 +332,126 @@
|
||||
|
||||
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'); }
|
||||
function renderMyAvatar(){ const el=document.getElementById('myAvatar'); if(myProfile.profile_pic){ el.textContent=''; el.style.background=`url('${myProfile.profile_pic}') center/cover`; } else { const a=avatars[myProfile.avatar]||avatars[0]; if(a){ el.style.background=`linear-gradient(135deg,${a.c1},${a.c2})`; el.textContent=a.glyph; } } }
|
||||
async function uploadPic(){
|
||||
const f=document.getElementById('pfPic').files[0];
|
||||
if(!f) return;
|
||||
const fd=new FormData(); fd.append('file',f);
|
||||
const pv=document.getElementById('pfPicPreview'); pv.textContent='Uploading…';
|
||||
try{ const r=await fetch('/api/profile/pic',{method:'POST',headers:authHeaders(),body:fd}); const d=await r.json();
|
||||
if(d.error){ pv.textContent=d.error; pv.style.color='#f87171'; return; }
|
||||
myProfile=d; renderMyAvatar(); pv.textContent='✓ uploaded'; pv.style.color='#4ade80';
|
||||
}catch(e){ pv.textContent='Upload failed'; pv.style.color='#f87171'; }
|
||||
}
|
||||
const INTAKE=[
|
||||
{label:"My name", q:"What's your full name?", p:"Jane Marie Doe"},
|
||||
{label:"Spouse's name", q:"What's your spouse's full name?", p:"John Robert Doe"},
|
||||
{label:"Married", q:"When and where did you get married?", p:"June 15, 2011, Seattle, WA"},
|
||||
{label:"Separated", q:"When did you separate?", p:"June 2025"},
|
||||
{label:"Children", q:"How many children do you share, and their names and ages?", p:"Two — Emma (12) and Liam (9)"},
|
||||
{label:"Current custody", q:"Where do the children live now, and what's the current schedule?", p:"They live with me; dad has them every other weekend"},
|
||||
{label:"Home", q:"Do you own a home or other real estate? Address, when bought, whose name is on the title/mortgage?", p:"House at 123 Main St, bought 2012, both names on title"},
|
||||
{label:"Assets", q:"What are your main assets? (bank accounts, retirement, vehicles, business, investments)", p:"Joint savings ~$20k, my 401k ~$85k, two cars"},
|
||||
{label:"Debts", q:"What are your main debts? (mortgage, credit cards, loans)", p:"Mortgage $280k remaining, $9k credit card, $12k car loan"},
|
||||
{label:"Income", q:"What's your employment and income? And your spouse's?", p:"I make $72k/yr as a nurse; spouse makes $95k as an engineer"},
|
||||
{label:"Case status", q:"Has either of you filed for divorce yet, and what does each side want?", p:"Spouse filed; wants full custody and to keep the house"},
|
||||
{label:"Safety", q:"Is there any domestic violence, abuse, or safety concern? Any protection orders?", p:"No / Yes — details"},
|
||||
{label:"Goals", q:"What outcome matters most to you? What are you most worried about?", p:"Keeping the kids with me and keeping the house"},
|
||||
];
|
||||
let intakeIdx=0; const intakeAnswers={};
|
||||
function openIntake(){ intakeIdx=0; for(const k in intakeAnswers) delete intakeAnswers[k]; closeModal('profileModal'); renderIntake(); document.getElementById('intakeModal').classList.add('open'); }
|
||||
function renderIntake(){ const q=INTAKE[intakeIdx]; document.getElementById('intakeQ').textContent=(intakeIdx+1)+'. '+q.q; const a=document.getElementById('intakeA'); a.value=intakeAnswers[intakeIdx]||''; a.placeholder=q.p; document.getElementById('intakeBar').style.width=Math.round((intakeIdx/(INTAKE.length-1))*100)+'%'; document.getElementById('intakeBack').style.visibility=intakeIdx===0?'hidden':'visible'; document.getElementById('intakeNext').textContent=intakeIdx===INTAKE.length-1?'Finish ✓':'Next →'; }
|
||||
function intakeNext(){ intakeAnswers[intakeIdx]=document.getElementById('intakeA').value.trim(); if(intakeIdx<INTAKE.length-1){ intakeIdx++; renderIntake(); } else { finishIntake(); } }
|
||||
function intakePrev(){ intakeAnswers[intakeIdx]=document.getElementById('intakeA').value.trim(); if(intakeIdx>0){ intakeIdx--; renderIntake(); } }
|
||||
function finishIntake(){
|
||||
const parts=[]; INTAKE.forEach((q,i)=>{ const v=(intakeAnswers[i]||'').trim(); if(v) parts.push(q.label+': '+v); });
|
||||
document.getElementById('pfAbout').value=parts.join('\n');
|
||||
closeModal('intakeModal'); document.getElementById('profileModal').classList.add('open');
|
||||
}
|
||||
let myFiles=[], myCats=['Other'];
|
||||
function escapeHtml(s){ return (s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
||||
function fmtSize(n){ return n>1048576?(n/1048576).toFixed(1)+' MB':n>1024?(n/1024).toFixed(0)+' KB':(n||0)+' B'; }
|
||||
async function loadFiles(){
|
||||
if(!token) return;
|
||||
try{ const r=await fetch('/api/files',{headers:authHeaders()}); const d=await r.json(); myFiles=d.files||[]; myCats=d.categories||['Other'];
|
||||
const sel=document.getElementById('fileCat'); if(sel){ sel.innerHTML=''; myCats.forEach(c=>{ const o=document.createElement('option'); o.value=c; o.textContent=c; sel.appendChild(o); }); }
|
||||
renderFiles();
|
||||
}catch(e){}
|
||||
}
|
||||
function renderFiles(){
|
||||
const el=document.getElementById('fileList'); if(!el) return;
|
||||
if(!myFiles.length){ el.innerHTML='<div class="sub">No documents yet — upload your petition, orders, agreements, and evidence.</div>'; return; }
|
||||
const groups={}; myFiles.forEach(f=>{ (groups[f.category]=groups[f.category]||[]).push(f); });
|
||||
el.innerHTML='';
|
||||
Object.keys(groups).forEach(cat=>{
|
||||
const h=document.createElement('div'); h.style.cssText='color:var(--gold);font-size:12px;font-weight:700;margin-top:10px'; h.textContent=cat+' ('+groups[cat].length+')'; el.appendChild(h);
|
||||
groups[cat].forEach(f=>{
|
||||
const it=document.createElement('div'); it.className='file-item';
|
||||
const nm=document.createElement('span'); nm.className='nm'; nm.textContent=f.orig_name; it.appendChild(nm);
|
||||
const sz=document.createElement('span'); sz.className='cat'; sz.textContent=fmtSize(f.size); it.appendChild(sz);
|
||||
const v=document.createElement('button'); v.textContent='View'; v.onclick=()=>viewFile(f.id); it.appendChild(v);
|
||||
const d=document.createElement('button'); d.textContent='✕'; d.onclick=()=>delFile(f.id); it.appendChild(d);
|
||||
el.appendChild(it);
|
||||
});
|
||||
});
|
||||
}
|
||||
async function uploadFiles(){
|
||||
const fs=document.getElementById('fileInput').files; if(!fs.length) return;
|
||||
const cat=document.getElementById('fileCat').value;
|
||||
for(const f of fs){ const fd=new FormData(); fd.append('file',f); fd.append('category',cat); try{ await fetch('/api/files',{method:'POST',headers:authHeaders(),body:fd}); }catch(e){} }
|
||||
document.getElementById('fileInput').value='';
|
||||
await loadFiles();
|
||||
}
|
||||
async function delFile(id){ try{ await fetch('/api/files/'+id,{method:'DELETE',headers:authHeaders()}); }catch(e){} await loadFiles(); }
|
||||
async function viewFile(id){
|
||||
const rec=myFiles.find(f=>f.id===id); if(!rec) return;
|
||||
const ext=(rec.orig_name.split('.').pop()||'').toLowerCase();
|
||||
document.getElementById('viewerTitle').textContent=rec.orig_name;
|
||||
const body=document.getElementById('viewerBody'); body.innerHTML='<div class="sub">Loading…</div>';
|
||||
document.getElementById('viewerModal').classList.add('open');
|
||||
if(['png','jpg','jpeg','gif','webp','svg'].includes(ext)){
|
||||
const r=await fetch('/api/files/'+id+'/raw',{headers:authHeaders()}); const b=await r.blob(); const u=URL.createObjectURL(b);
|
||||
body.innerHTML='<img src="'+u+'" style="max-width:100%;border-radius:8px">';
|
||||
} else if(ext==='pdf'){
|
||||
const r=await fetch('/api/files/'+id+'/raw',{headers:authHeaders()}); const b=await r.blob(); const u=URL.createObjectURL(b);
|
||||
body.innerHTML='<iframe src="'+u+'" style="width:100%;height:70vh;border:0;border-radius:8px"></iframe>';
|
||||
} else {
|
||||
let txt=''; try{ const r=await fetch('/api/files/'+id+'/text',{headers:authHeaders()}); const d=await r.json(); txt=d.text||''; }catch(e){}
|
||||
body.innerHTML='<pre style="white-space:pre-wrap;font-family:inherit;font-size:13px;line-height:1.55">'+escapeHtml(txt||'(no text preview — this file type downloads instead)')+'</pre>';
|
||||
}
|
||||
}
|
||||
function openProfile(){ document.getElementById('pfName').value=myProfile.display_name||''; document.getElementById('pfAbout').value=myProfile.about_me||''; renderAvatarSel(); document.getElementById('profileModal').classList.add('open'); loadFiles(); }
|
||||
let allForms=[];
|
||||
async function openFill(){
|
||||
try{ const r=await fetch('/api/forms'); allForms=await r.json();
|
||||
const sel=document.getElementById('fillFormSelect'); sel.innerHTML='';
|
||||
allForms.forEach(f=>{ const o=document.createElement('option'); o.value=f.id; o.textContent=f.name; sel.appendChild(o); });
|
||||
document.getElementById('fillFields').innerHTML='<div class="sub">Click "Auto-fill from my info" to pre-fill the fields.</div>';
|
||||
document.getElementById('fillModal').classList.add('open');
|
||||
}catch(e){}
|
||||
}
|
||||
async function autofillForm(){
|
||||
const fid=document.getElementById('fillFormSelect').value;
|
||||
const el=document.getElementById('fillFields'); el.innerHTML='<div class="sub">Filling…</div>';
|
||||
const r=await fetch('/api/forms/autofill',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({form_id:fid})});
|
||||
const d=await r.json();
|
||||
const f=allForms.find(x=>x.id===fid);
|
||||
el.innerHTML='';
|
||||
(f.fields||[]).forEach(fd=>{
|
||||
const row=document.createElement('div'); row.style.cssText='margin-bottom:8px';
|
||||
const lb=document.createElement('label'); lb.style.cssText='font-size:12px;color:var(--muted)'; lb.textContent=fd.label; row.appendChild(lb);
|
||||
const inp=document.createElement('input'); inp.dataset.key=fd.key; inp.value=(d.values||{})[fd.key]||''; row.appendChild(inp);
|
||||
el.appendChild(row);
|
||||
});
|
||||
}
|
||||
async function generateForm(){
|
||||
const fid=document.getElementById('fillFormSelect').value;
|
||||
const values={}; document.querySelectorAll('#fillFields input').forEach(i=>values[i.dataset.key]=i.value);
|
||||
const r=await fetch('/api/forms/fill',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({form_id:fid,values})});
|
||||
if(!r.ok){ alert('Could not generate form'); return; }
|
||||
const blob=await r.blob(); const url=URL.createObjectURL(blob);
|
||||
const a=document.createElement('a'); a.href=url; a.download=fid+'_filled.pdf'; document.body.appendChild(a); a.click(); a.remove();
|
||||
}
|
||||
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)});
|
||||
|
||||
Reference in New Issue
Block a user