πŸ“„
No document loaded
Evidence files are stored locally β€” the database only holds metadata and annotations. Use πŸ“„ Load file to open the evidence file from disk. Existing annotations are loaded automatically and will overlay once the file is open.
Annotations (0) Unsaved changes
Loading…
⏳ Loading annotations…
H HighlightP PinN Note T TickX CrossEsc Pointer Ctrl+Z UndoCtrl+S Save
πŸ“„ Workpaper
Loading workpaper…
'); w.document.close(); }else{ // popup blocked β†’ copy to clipboard navigator.clipboard.writeText(txt).then(()=>showToast('Summary copied to clipboard','success'),()=>showToast('Could not export','error')); } } /* ─────────────────────────── Share ─────────────────────────── */ function shareLink(){ const u=new URL(window.location.href); u.searchParams.set('readonly','1'); const link=u.toString(); navigator.clipboard.writeText(link).then( ()=>showToast('πŸ“€ Read-only link copied β€” share with external reviewers','success'), ()=>{ prompt('Copy this read-only link:',link); } ); } /* ─────────────────────────── Keyboard shortcuts ─────────────────────────── */ document.addEventListener('keydown',e=>{ const tag=(e.target.tagName||'').toLowerCase(); const typing=tag==='input'||tag==='textarea'||tag==='select'; if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='s'){ e.preventDefault(); if(!READONLY)saveAll(); return; } if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='z'){ if(!typing){ e.preventDefault(); undoLast(); } return; } if(typing) return; if(READONLY) return; const k=e.key.toLowerCase(); if(k==='h') setTool('highlight'); else if(k==='p') setTool('pin'); else if(k==='n') setTool('note'); else if(k==='t') setTool('tick'); else if(k==='x') setTool('cross'); else if(e.key==='Escape') setTool('pointer'); }); /* warn before leaving with unsaved changes */ window.addEventListener('beforeunload',e=>{ if(dirty&&!READONLY){ e.preventDefault(); e.returnValue=''; } }); /* re-render PDF markers on resize? keep simple β€” markers are %, so they track automatically */ /* ═══════════════════════════════════════════════════════════════════════════ 2b: ANNOTATION TEMPLATES ═══════════════════════════════════════════════════════════════════════════ */ const TEMPLATES={ uar:{ name:'User Access Review', markers:[ {type:'tick', label:'User exists'}, {type:'tick', label:'Access appropriate'}, {type:'cross', label:'Inappropriate access'}, {type:'pin', label:'Unusual access'} ] }, jet:{ name:'Journal Entry Testing', markers:[ {type:'tick', label:'Normal JE'}, {type:'cross', label:'Manual override'}, {type:'pin', label:'Unusual account'}, {type:'pin', label:'Round number'}, {type:'cross', label:'Weekend posting'} ] }, brec:{ name:'Bank Reconciliation', markers:[ {type:'tick', label:'Reconciled item'}, {type:'cross', label:'Unreconciled'}, {type:'pin', label:'Aged item'}, {type:'pin', label:'Unusual'} ] }, inv:{ name:'Invoice Approval', markers:[ {type:'tick', label:'Approved'}, {type:'cross', label:'Missing approval'}, {type:'pin', label:'Duplicate'}, {type:'pin', label:'Round number'} ] } }; const TEMPLATE_IC_COLORS={tick:'var(--green)',cross:'var(--red)',pin:'var(--blue)',highlight:'var(--amber)',note:'var(--amber)'}; const TEMPLATE_IC_TEXT={tick:'βœ“',cross:'βœ—',pin:'πŸ“Œ',highlight:'πŸ–ŠοΈ',note:'✏️'}; function applyTemplate(key){ activeTemplate=key&&TEMPLATES[key]?TEMPLATES[key]:null; const legend=document.getElementById('tmplLegend'); const legendTitle=document.getElementById('tmplLegendTitle'); const legendItems=document.getElementById('tmplLegendItems'); if(!activeTemplate){ legend.classList.remove('on'); return; } legendTitle.textContent=activeTemplate.name+' β€” Legend'; legendItems.innerHTML=activeTemplate.markers.map(m=>{ const ic=TEMPLATE_IC_TEXT[m.type]||'?'; const col=TEMPLATE_IC_COLORS[m.type]||'#888'; return '
'+ic+''+esc(m.label)+'
'; }).join(''); legend.classList.add('on'); /* Pre-fill test_attribute suggestions: set the first marker label as default attribute hint. Actual pre-fill happens when user creates a new annotation β€” see createAnnotation override. */ showToast('Template: '+activeTemplate.name,'success'); } /* ═══════════════════════════════════════════════════════════════════════════ 2c: SPLIT VIEW β€” side-by-side workpaper sections ═══════════════════════════════════════════════════════════════════════════ */ async function toggleSplitView(){ splitViewOn=!splitViewOn; const wrap=document.querySelector('.wrap'); const wpPanel=document.getElementById('wpSplit'); const btn=document.getElementById('splitBtn'); if(splitViewOn){ wrap.classList.add('split-view'); wpPanel.classList.add('on'); btn.classList.add('active'); await loadWorkpaperForSplit(); } else { wrap.classList.remove('split-view'); wpPanel.classList.remove('on'); btn.classList.remove('active'); } renderList(); // re-render to show/hide "Copy to WP" buttons } async function loadWorkpaperForSplit(){ const wpId=WORKPAPER_ID; const container=document.getElementById('wpSections'); const idLabel=document.getElementById('wpSplitId'); if(idLabel) idLabel.textContent=wpId||'(none)'; if(!wpId){ container.innerHTML='
No workpaper_id in URL. Open this page from a workpaper.
'; return; } container.innerHTML='
Loading…
'; let wpRow=null; try{ const {data,error}=await sb.from('workpapers').select('*').eq('id',wpId).maybeSingle(); if(!error&&data) wpRow=data; }catch(e){} if(!wpRow){ container.innerHTML='
Could not load workpaper '+esc(wpId)+'.
'; return; } /* Parse the content JSON β€” it may be a string or already an object */ let content={}; try{ content=typeof wpRow.content==='string'?JSON.parse(wpRow.content):(wpRow.content||{}); }catch(e){ content={}; } /* Also try top-level columns if content is empty */ const SEC_KEYS=['objective','scope','procedures','evidence','exceptions','conclusion']; SEC_KEYS.forEach(k=>{ if(!content[k]&&wpRow[k]) content[k]=wpRow[k]; }); wpData=content; renderWpSections(); } function renderWpSections(){ const container=document.getElementById('wpSections'); const SEC_KEYS=['objective','scope','procedures','evidence','exceptions','conclusion']; const SEC_LABELS={objective:'Objective',scope:'Scope',procedures:'Procedures',evidence:'Evidence',exceptions:'Exceptions',conclusion:'Conclusion'}; container.innerHTML=''; SEC_KEYS.forEach(k=>{ const div=document.createElement('div'); div.className='wp-sec'; div.dataset.sec=k; const lbl=document.createElement('div'); lbl.className='wp-sec-label'; lbl.textContent=SEC_LABELS[k]||k; const ta=document.createElement('textarea'); ta.className='wp-sec-ta'; ta.rows=4; ta.value=wpData[k]||''; ta.placeholder='(empty)'; /* Keep wpData in sync when user edits (client-side only; saving back to the workpaper requires the workflow.html save flow or a separate explicit save β€” we don't auto-save to avoid conflicts) */ ta.addEventListener('input',()=>{ wpData[k]=ta.value; }); div.appendChild(lbl); div.appendChild(ta); container.appendChild(div); }); } /* Highlight the workpaper section matching the annotation's workpaper_section field */ function highlightWpSection(sectionName){ if(!splitViewOn) return; document.querySelectorAll('.wp-sec').forEach(el=>el.classList.remove('hl')); if(!sectionName) return; const key=sectionName.toLowerCase(); const target=document.querySelector('.wp-sec[data-sec="'+key+'"]'); if(target){ target.classList.add('hl'); target.scrollIntoView({behavior:'smooth',block:'nearest'}); } } /* Copy annotation text into the matching workpaper section textarea (client-side) */ function copyAnnotationToWorkpaper(lid){ const a=annotations.find(x=>x._localId===lid); if(!a||!a.workpaper_section) return; const key=a.workpaper_section.toLowerCase(); const ta=document.querySelector('.wp-sec[data-sec="'+key+'"] .wp-sec-ta'); if(!ta){ showToast('Workpaper section "'+a.workpaper_section+'" not found','error'); return; } const insert=(a.text||a.test_attribute||('Annotation: '+a.annotation_type)); ta.value=(ta.value?ta.value+'\n':'')+insert; wpData[key]=ta.value; highlightWpSection(a.workpaper_section); showToast('Copied to '+a.workpaper_section+' section','success'); /* NOTE: to persist these changes back to the workpaper record in Supabase, the user should save from workflow.html. This tool only updates the textarea in-memory. */ } /* ═══════════════════════════════════════════════════════════════════════════ 2d: EXPORT ALL β€” bulk CSV of all annotations in engagement ═══════════════════════════════════════════════════════════════════════════ */ /* Guard against CSV formula injection by prefixing dangerous chars */ function csvSafeCell(val){ const s=String(val==null?'':val); return /^[=+\-@]/.test(s)?("'"+s):s; } function buildEngagementCsv(rows){ const headers=['Workpaper Ref','Evidence Name','Annotation Type','Test Attribute','Workpaper Section','Annotation Text','Annotated By','Date']; const lines=[headers.map(h=>'"'+h+'"').join(',')]; rows.forEach(r=>{ lines.push([ r.workpaper_id||'',r.evidence_request_id||'',r.annotation_type||'', r.test_attribute||'',r.workpaper_section||'',r.text||'', r.annotated_by_email||'',r.created_at?r.created_at.slice(0,10):'' ].map(v=>'"'+csvSafeCell(v).replace(/"/g,'""')+'"').join(',')); }); return lines.join('\r\n'); } async function exportAllAnnotations(){ if(!ENGAGEMENT_ID){ showToast('No engagement_id or project_id in URL','error'); return; } showToast('Fetching all annotations…'); /* Query all annotations where workpaper is in this engagement. We join via workpaper_id -> workpapers.engagement_id (or project_id). Fall back to a direct filter on evidence_annotations if the join isn't available. */ let rows=[]; try{ /* Strategy: load all evidence_annotations where workpaper_id in (select id from workpapers where engagement_id = ENGAGEMENT_ID) */ const {data:wps}=await sb.from('workpapers').select('id').eq('engagement_id',ENGAGEMENT_ID); if(wps&&wps.length){ const ids=wps.map(w=>w.id); const {data}=await sb.from('evidence_annotations').select('*').in('workpaper_id',ids); rows=data||[]; } }catch(e){ showToast('Could not fetch annotations: '+(e.message||''),'error'); return; } if(!rows.length){ showToast('No annotations found for this engagement','error'); return; } const csv=buildEngagementCsv(rows); const blob=new Blob([csv],{type:'text/csv'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='annotations-engagement-'+ENGAGEMENT_ID+'.csv'; a.click(); showToast('Exported '+rows.length+' annotations','success'); } /* ═══════════════════════════════════════════════════════════════════════════ 2e: ANNOTATION REVIEW WORKFLOW ═══════════════════════════════════════════════════════════════════════════ */ function updateReviewBanner(){ const banner=document.getElementById('reviewAwaiting'); if(!banner||!IS_REVIEWER) return; const count=annotations.filter(a=>!a._deleted&&a.status==='submitted').length; if(count>0){ banner.textContent='πŸ”” '+count+' annotation'+(count===1?' is':' are')+' awaiting review'; banner.classList.add('on'); } else { banner.classList.remove('on'); } } async function submitForReview(lid){ const a=annotations.find(x=>x._localId===lid); if(!a||!a.id){ showToast('Save annotations first before submitting for review','error'); return; } try{ const {error}=await sb.from('evidence_annotations').update({status:'submitted'}).eq('id',a.id); if(error) throw error; a.status='submitted'; showToast('Annotation submitted for review','success'); renderList(); updateReviewBanner(); }catch(e){ /* Column may not exist pre-migration β€” degrade gracefully */ a.status='submitted'; // optimistic local update showToast('Submitted (review columns may need migration)'); renderList(); updateReviewBanner(); } } async function approveAnnotation(lid){ const a=annotations.find(x=>x._localId===lid); if(!a||!a.id) return; const now=new Date().toISOString(); try{ const {error}=await sb.from('evidence_annotations').update({ status:'approved',reviewed_by:currentUserEmail||currentUserId||null,reviewed_at:now }).eq('id',a.id); if(error) throw error; }catch(e){ /* pre-migration: update locally only */ } a.status='approved'; a.reviewed_by=currentUserEmail||null; a.reviewed_at=now; showToast('Annotation approved','success'); renderList(); updateReviewBanner(); } async function returnAnnotation(lid){ const a=annotations.find(x=>x._localId===lid); if(!a||!a.id) return; const comment=window.prompt('Return comment (explain what needs fixing):',''); if(comment===null) return; // cancelled const now=new Date().toISOString(); try{ const {error}=await sb.from('evidence_annotations').update({ status:'returned',reviewed_by:currentUserEmail||currentUserId||null, reviewed_at:now,review_comment:comment||null }).eq('id',a.id); if(error) throw error; }catch(e){ /* pre-migration: update locally only */ } a.status='returned'; a.reviewed_by=currentUserEmail||null; a.reviewed_at=now; a.review_comment=comment||null; showToast('Annotation returned with comment'); renderList(); updateReviewBanner(); } /* ═══════════════════════════════════════════════════════════════════════════ WORKPAPER MODE β€” load workpaper, evidence queue, auto-annotations ═══════════════════════════════════════════════════════════════════════════ */ async function loadWorkpaperMode(){ const meta=document.getElementById('rmeta'); meta.innerHTML='Loading workpaper…'; try{ const {data:wp,error}=await sb.from('workpapers').select('*').eq('id',WORKPAPER_ID).maybeSingle(); if(error||!wp) throw new Error((error&&error.message)||'Workpaper not found'); workpaperModeData=wp; let content={}; try{ content=typeof wp.content==='string'?JSON.parse(wp.content):(wp.content||{}); }catch(e){} workpaperModeData._content=content; // Nav updates const navRef=document.getElementById('wp-nav-ref'); if(navRef){ navRef.textContent=wp.ref||'WP'; navRef.style.display='inline'; } const backBtn=document.getElementById('back-to-wp-btn'); if(backBtn){ backBtn.href='/workflow.html#wp-'+WORKPAPER_ID; backBtn.style.display='inline'; } const evMgr=document.getElementById('ev-mgr-link'); if(evMgr) evMgr.style.display='none'; // Update meta meta.innerHTML='Workpaper: '+esc(wp.title||'')+'' +(wp.ref?' Β· '+esc(wp.ref):'') +(wp.status?' Β· '+esc(wp.status):''); // Show evidence queue from workpaper content const evAnnotations=content.evidence_annotations||[]; const evList=Array.isArray(content.evidence)?content.evidence:[]; showEvidenceQueue(evAnnotations,evList); // Show AI summary panel if(evAnnotations.length) showWpSummaryPanel(evAnnotations); // Load any existing annotations for this workpaper await loadAnnotationsForWorkpaper(); }catch(e){ const meta2=document.getElementById('rmeta'); if(meta2) meta2.innerHTML='Could not load workpaper: '+esc(e.message)+''; (document.getElementById('annEmpty') || {}).textContent ='Could not load workpaper.'; } } function showEvidenceQueue(evAnnotations,evList){ const bar=document.getElementById('ev-queue-bar'); const items=document.getElementById('ev-queue-items'); if(!bar||!items) return; const docs=evAnnotations.length>0 ? evAnnotations.map(a=>a.document_name||'').filter(Boolean) : evList.filter(Boolean); if(!docs.length) return; bar.style.display='block'; items.innerHTML=docs.map((name,i)=> '' ).join(''); } function selectQueueDocument(idx,name){ selectedQueueDoc={idx,name}; document.querySelectorAll('.ev-queue-item').forEach((b,i)=>{ b.classList.toggle('active',i===idx); }); showAutoAnnotations(name); showToast('Load "'+name+'" using the πŸ“„ Load file button',''); } function showAutoAnnotations(docName){ if(!workpaperModeData||!workpaperModeData._content) return; const evAnnotations=workpaperModeData._content.evidence_annotations||[]; const match=evAnnotations.find(a=>(a.document_name||'').toLowerCase()===(docName||'').toLowerCase()); const panel=document.getElementById('autoAnnotPanel'); const contentEl=document.getElementById('autoAnnotContent'); if(!panel||!contentEl) return; if(!match){ panel.style.display='none'; return; } panel.style.display='block'; let html=''; let procCount=0,findCount=0,excCount=0; // Procedures (green βœ“) const procs=(match.procedures_performed||'').split(/\d+\.\s+|;\s+/).filter(p=>p.trim().length>3); if(procs.length){ html+='
Procedures Performed
'; procs.slice(0,8).forEach(proc=>{ html+='
' +'βœ“' +''+esc(proc.trim())+'' +'
'; procCount++; }); } // Key findings (blue πŸ“Œ) const findings=(match.key_findings||'').split(/[;.]\s+|\d+\.\s+/).filter(f=>f.trim().length>3); if(findings.length){ html+='
Key Findings
'; findings.slice(0,6).forEach(finding=>{ html+='
' +'πŸ“Œ' +''+esc(finding.trim())+'' +'
'; findCount++; }); } // Exceptions (red βœ—) const exc=match.exceptions_noted||''; if(exc&&exc.toLowerCase()!=='none'&&exc.trim()){ const excParts=exc.split(/[;.]\s+|\d+\.\s+/).filter(e=>e.trim().length>3); if(excParts.length){ html+='
Exceptions
'; excParts.slice(0,5).forEach(ex=>{ html+='
' +'βœ—' +''+esc(ex.trim())+'' +'
'; excCount++; }); } } // Conclusion if(match.conclusion&&match.conclusion.toLowerCase()!=='none'&&match.conclusion.trim()){ html+='
' +'Conclusion: '+esc(match.conclusion) +'
'; } contentEl.innerHTML=html; // FIX 4: update Annotations count header const annCountEl=document.getElementById('annCount'); if(annCountEl) annCountEl.textContent=procCount+findCount+excCount; } function showWpSummaryPanel(evAnnotations){ const panel=document.getElementById('wp-summary-panel'); const items=document.getElementById('wp-summary-items'); if(!panel||!items) return; panel.style.display='block'; items.innerHTML=evAnnotations.map(a=>{ const hasExc=a.exceptions_noted&&a.exceptions_noted.toLowerCase()!=='none'&&a.exceptions_noted.trim(); return '
' +'
'+esc(a.document_name||'')+'
' +'
' +(hasExc?'⚠ '+esc((a.exceptions_noted||'').substring(0,80)):'βœ“ No exceptions')+'
' +'
'+esc((a.conclusion||'').substring(0,80))+'
' +'
'; }).join(''); } async function loadAnnotationsForWorkpaper(){ let rows=[]; try{ const r=await sb.from('evidence_annotations').select('*').eq('workpaper_id',WORKPAPER_ID); rows=r.data||[]; }catch(e){} annotations=(rows||[]).map(r=>({ id:r.id,_localId:uid(),_new:false,_deleted:false, evidence_request_id:r.evidence_request_id,workpaper_id:r.workpaper_id, annotation_type:r.annotation_type||'highlight',page_number:r.page_number||1, x_position:+r.x_position||0,y_position:+r.y_position||0, width:r.width==null?null:+r.width,height:r.height==null?null:+r.height, text:r.text||'',test_attribute:r.test_attribute||'', workpaper_section:r.workpaper_section||'', color:r.color||(TYPE_META[r.annotation_type||'highlight']||TYPE_META.highlight).color, cell_ref:r.cell_ref||null,row_ref:r.row_ref||null, status:r.status||null,reviewed_by:r.reviewed_by||null, reviewed_at:r.reviewed_at||null,review_comment:r.review_comment||null })); annotations.forEach(a=>{if(a.id) savedIds.add(a.id);}); updateMeta(); renderList(); updateReviewBanner(); } function checkAutoAnnotate(fileName){ if(!workpaperModeData||!workpaperModeData._content) return; const evAnnotations=workpaperModeData._content.evidence_annotations||[]; const match=evAnnotations.find(a=> (a.document_name||'').toLowerCase()===fileName.toLowerCase() ); if(!match) return; // Mark this queue item as selected const idx=evAnnotations.indexOf(match); selectedQueueDoc={idx,name:fileName}; document.querySelectorAll('.ev-queue-item').forEach((b,i)=>b.classList.toggle('active',i===idx)); // Slight delay to allow file to render setTimeout(()=>autoAnnotateDocument(match),600); } function autoAnnotateDocument(annotData){ // Clear previous auto-annotations autoAnnotations.forEach(a=>{ annotations=annotations.filter(x=>x._localId!==a._localId); }); autoAnnotations=[]; let tickCount=0,crossCount=0,pinCount=0; // Conclusion note at top if(annotData.conclusion&&annotData.conclusion.toLowerCase()!=='none'){ const a=makeAutoAnnotation('note',5,3,null,null,1, (annotData.conclusion||'').substring(0,200),'Conclusion'); autoAnnotations.push(a); annotations.push(a); undoStack.push(a._localId); } // Ticks for procedures performed const procs=(annotData.procedures_performed||'').split(/[;.]\s+/).filter(p=>p.trim().length>5); procs.slice(0,5).forEach((proc,i)=>{ const a=makeAutoAnnotation('tick',5,12+i*11,null,null,1,proc.trim().substring(0,150),'Procedure performed'); autoAnnotations.push(a); annotations.push(a); undoStack.push(a._localId); tickCount++; }); // Crosses for exceptions const exc=annotData.exceptions_noted||''; if(exc&&exc.toLowerCase()!=='none'&&exc.trim()){ const excParts=exc.split(/[;.]\s+/).filter(e=>e.trim().length>5); excParts.slice(0,3).forEach((ex,i)=>{ const a=makeAutoAnnotation('cross',5,62+i*11,null,null,1,ex.trim().substring(0,150),'Exception'); autoAnnotations.push(a); annotations.push(a); undoStack.push(a._localId); crossCount++; }); } // Pins for key findings const findings=(annotData.key_findings||'').split(/[;.]\s+/).filter(f=>f.trim().length>5); findings.slice(0,4).forEach((finding,i)=>{ const a=makeAutoAnnotation('pin',12+i*20,78+i*6,null,null,1,finding.trim().substring(0,150),'Key finding'); autoAnnotations.push(a); annotations.push(a); undoStack.push(a._localId); pinCount++; }); setDirty(true); renderAllMarkers(); renderList(); // Attempt text highlighting for text files tryHighlightTextFile(annotData); // Update bar const bar=document.getElementById('auto-annot-bar'); const summary=document.getElementById('auto-annot-summary'); if(bar) bar.style.display='flex'; if(summary) summary.textContent= 'Auto-annotations generated: '+tickCount+' βœ“ ticks, '+crossCount+' βœ— exceptions, '+pinCount+' πŸ“Œ pins'; showToast('βœ“ '+autoAnnotations.length+' auto-annotations applied from workpaper','success'); } function makeAutoAnnotation(type,x,y,w,h,page,text,attr){ return { id:null,_localId:uid(),_new:true,_deleted:false, evidence_request_id:EVIDENCE_ID||null, workpaper_id:WORKPAPER_ID||null, annotation_type:type,page_number:page||1, x_position:x,y_position:y, width:w,height:h, text:text||'',test_attribute:attr||'', workpaper_section:'', color:TYPE_META[type]?TYPE_META[type].color:'#0057ff', cell_ref:null,row_ref:null, status:null,reviewed_by:null,reviewed_at:null,review_comment:null }; } function tryHighlightTextFile(annotData){ // Find any pre-shell element and try to highlight matching text const pre=document.querySelector('.pre-shell'); if(!pre) return; const originalText=pre.textContent; if(!originalText) return; // Build keywordβ†’color map from annotation data const highlights=[]; const exc=annotData.exceptions_noted||''; if(exc&&exc.toLowerCase()!=='none'){ const names=exc.match(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b/g)||[]; const nums=exc.match(/\b\d+\s*(?:hour|day|week)s?\b/gi)||[]; [...names,...nums].forEach(w=>highlights.push({word:w,bg:'rgba(239,68,68,0.25)',border:'#ef4444'})); } const findings=annotData.key_findings||''; if(findings){ const words=findings.match(/\b\d+\b|\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b/g)||[]; words.slice(0,6).forEach(w=>highlights.push({word:w,bg:'rgba(245,158,11,0.25)',border:'#f59e0b'})); } if(!highlights.length) return; const lines=originalText.split('\n'); const htmlLines=lines.map(line=>{ let safe=line.replace(/&/g,'&').replace(//g,'>'); highlights.forEach(h=>{ if(!h.word||!line.includes(h.word)) return; const escaped=h.word.replace(/&/g,'&').replace(//g,'>'); const re=new RegExp(escaped.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),'g'); safe=safe.replace(re,''+escaped+''); }); return safe; }); // Replace pre with div to allow innerHTML const div=document.createElement('div'); div.className='pre-shell'; div.style.cssText=pre.style.cssText||''; div.innerHTML=htmlLines.join('\n'); pre.replaceWith(div); } function acceptAutoAnnotations(){ const bar=document.getElementById('auto-annot-bar'); if(bar) bar.style.display='none'; showToast('All auto-annotations accepted','success'); saveAutoAnnotationsToWorkpaper(); } function clearAutoAnnotations(){ const lids=new Set(autoAnnotations.map(a=>a._localId)); annotations=annotations.filter(a=>!lids.has(a._localId)); autoAnnotations=[]; setDirty(false); renderAllMarkers(); renderList(); const bar=document.getElementById('auto-annot-bar'); if(bar) bar.style.display='none'; showToast('Auto-annotations cleared',''); } async function saveAutoAnnotationsToWorkpaper(){ if(!workpaperModeData||!WORKPAPER_ID) return; const content=workpaperModeData._content||{}; const docAnnotations=content.document_annotations||{}; const docName=selectedQueueDoc?selectedQueueDoc.name:'unknown'; docAnnotations[docName]={ auto_generated:true, ticks:autoAnnotations.filter(a=>a.annotation_type==='tick').map(a=>({text:a.text,attr:a.test_attribute})), crosses:autoAnnotations.filter(a=>a.annotation_type==='cross').map(a=>({text:a.text,attr:a.test_attribute})), highlights:autoAnnotations.filter(a=>a.annotation_type==='highlight').map(a=>({text:a.text})), pins:autoAnnotations.filter(a=>a.annotation_type==='pin').map(a=>({text:a.text,attr:a.test_attribute})), generated_at:new Date().toISOString() }; const updatedContent={...content,document_annotations:docAnnotations}; try{ const {error}=await sb.from('workpapers').update({content:updatedContent}).eq('id',WORKPAPER_ID); if(error) throw error; showToast('Auto-annotations saved to workpaper','success'); workpaperModeData._content=updatedContent; }catch(e){ showToast('Could not save to workpaper: '+(e.message||''),'error'); } } /* ─────────────────────────── Boot ─────────────────────────── */ (async function boot(){ applyReadonly(); const ok=await initAuth(); if(ok===false) return; updateMeta(); if(WORKPAPER_ID && !EVIDENCE_ID){ await loadWorkpaperMode(); } else { await loadAnnotations(); } })();