'use strict'; /** ScholarFlow Project #19: fully local, deterministic research organizer. */ (() => { const KEY = 'scholarflow-project-19-v1'; const PAGES = ['overview','library','screening','matrix','gaps','export']; const STATUSES = ['include','maybe','exclude','unscreened']; const DEFAULTS = Array.isArray(window.SCHOLARFLOW_SEED) ? window.SCHOLARFLOW_SEED : []; const q = s => document.querySelector(s); const qa = s => Array.from(document.querySelectorAll(s)); const clone = obj => JSON.parse(JSON.stringify(obj)); let data = readRecords(); let currentPage = 'overview'; let toastTimeout; function escapeHTML(s){return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));} function display(s){return escapeHTML(s == null || s === '' ? 'Not provided' : s);} function safeStatus(status){return STATUSES.includes(status) ? status : 'unscreened';} function recordFrom(raw,index){ if(!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`Record ${index+1} must be an object.`); if(typeof raw.title !== 'string' || !raw.title.trim()) throw new Error(`Record ${index+1} needs a title.`); const t = k => String(raw[k] ?? '').slice(0,k==='findings'||k==='limitations'||k==='notes'?2500:500); const y = Number(raw.year); const n = raw.participants == null || raw.participants === '' ? null : Number(raw.participants); return {id:t('id') || `IMP-${index+1}`,title:t('title'),authors:t('authors'),year:Number.isInteger(y)&&y>=1900&&y<=2100?y:null,method:t('method')||'Other',region:t('region')||'Not specified',language:t('language')||'Not specified',participants:Number.isInteger(n)&&n>=0?n:null,topic:t('topic'),findings:t('findings'),limitations:t('limitations'),source:t('source'),synthetic:raw.synthetic===true,status:safeStatus(raw.status),notes:t('notes')}; } function readRecords(){try{let raw=localStorage.getItem(KEY);if(!raw)return clone(DEFAULTS);let parsed=JSON.parse(raw);return Array.isArray(parsed)?parsed.map(recordFrom):clone(DEFAULTS);}catch{return clone(DEFAULTS);}} function persist(){try{localStorage.setItem(KEY,JSON.stringify(data));}catch{toast('Browser storage is unavailable. Export your work to keep it.');}} function toast(message){const el=q('#toast');el.textContent=message;el.classList.add('show');clearTimeout(toastTimeout);toastTimeout=setTimeout(()=>el.classList.remove('show'),3400);} function showPage(page){if(!PAGES.includes(page))return;currentPage=page;qa('.navitem').forEach(n=>{let yes=n.dataset.page===page;n.classList.toggle('active',yes);n.setAttribute('aria-current',yes?'page':'false');});qa('.page').forEach(n=>n.classList.toggle('active',n.id===`page-${page}`));let subtitles={overview:['Research, organized.
Evidence, clarified.','A practical workspace for screening studies, synthesizing findings, and documenting research gaps.'],library:['Every study.
One organized library.','Search, inspect and curate records with traceable metadata.'],screening:['Make every decision
auditable.','Screen research records with consistent decisions and notes.'],matrix:['Compare your evidence.
Side by side.','Review the included records across common dimensions.'],gaps:['Look for gaps.
Describe, don’t assume.','Explore descriptive imbalances in your included records.'],export:['Keep your work.
Own your evidence.','Download reproducible datasets and screening logs.']};q('#page-title').innerHTML=subtitles[page][0];q('#page-subtitle').textContent=subtitles[page][1];renderPage(page);} function countBy(items,key){return items.reduce((out,r)=>{const name=String(r[key]||'Not specified');out[name]=(out[name]||0)+1;return out;},{});} function barList(counts,css=''){let entries=Object.entries(counts).sort((a,b)=>b[1]-a[1]||a[0].localeCompare(b[0]));let total=entries.reduce((n,a)=>n+a[1],0);if(!entries.length)return '
No records to analyze.
';return entries.map(([name,n])=>`
${escapeHTML(name)}${n} · ${Math.round(100*n/total)}%
`).join('');} function fieldCompleteness(r){return [r.source,r.method,r.findings,r.limitations,r.participants].filter(x=>x!==null && x!==undefined && String(x).trim()!=='' && !String(x).startsWith('SYNTHETIC')).length;} function renderOverview(){const included=data.filter(x=>x.status==='include');const done=data.filter(x=>x.status!=='unscreened');const synthetic=data.filter(x=>x.synthetic);q('#metrics').innerHTML=[['Records',data.length,'Current library'],['Screened',done.length,`${data.length?Math.round(100*done.length/data.length):0}% of all records`],['Included',included.length,'Manually selected'],['Synthetic',synthetic.length,'Demo records only']].map(([name,n,note])=>`
${name}
${n}
${note}
`).join('');q('#method-bars').innerHTML=barList(countBy(data,'method'));q('#screen-bars').innerHTML=barList(countBy(data,'status'),'violet');} function refreshMethods(){const select=q('#filter-method'),v=select.value;const methods=[...new Set(data.map(r=>r.method))].sort();select.innerHTML=''+methods.map(m=>``).join('');select.value=methods.includes(v)?v:'all';} function renderLibrary(){refreshMethods();let term=q('#search').value.trim().toLowerCase(),method=q('#filter-method').value,status=q('#filter-status').value;const list=data.filter(r=>(method==='all'||r.method===method)&&(status==='all'||r.status===status)&&[r.title,r.authors,r.topic,r.region,r.findings,r.source].some(v=>String(v||'').toLowerCase().includes(term)));q('#library-count').textContent=`${list.length} / ${data.length} RECORDS`;q('#library-list').innerHTML=list.length?list.map(r=>`

${escapeHTML(r.title)}

${display(r.authors)} · ${display(r.year)} · ${escapeHTML(r.method)} · ${escapeHTML(r.region)} · ${escapeHTML(r.language)}

${display(r.findings)}

${r.synthetic?'SYNTHETIC':'USER SUPPLIED'}
${escapeHTML(r.status.toUpperCase())}
`).join(''):'
No matching studies. Adjust filters or add a record.
';} function renderScreening(){q('#screening-list').innerHTML=data.length?data.map(r=>`
${escapeHTML(r.title)}
${display(r.year)} · ${escapeHTML(r.method)} · ${escapeHTML(r.region)} ${r.synthetic?' · SYNTHETIC RECORD':''}
${STATUSES.map(st=>``).join('')}
`).join(''):'
The study library is empty. Add studies to begin screening.
';} function renderMatrix(){const included=data.filter(r=>r.status==='include');q('#matrix-empty').hidden=included.length!==0;q('#matrix-body').innerHTML=included.map(r=>`${escapeHTML(r.title)} ${r.synthetic?'
SYNTHETIC':''}${display(r.year)}${escapeHTML(r.method)}${escapeHTML(r.region)}${escapeHTML(r.language)}${display(r.participants)}${fieldCompleteness(r)} / 5 populated fields${escapeHTML(r.source||'Unverified')}`).join('');} function renderGaps(){const included=data.filter(r=>r.status==='include');q('#region-bars').innerHTML=barList(countBy(included,'region'));q('#language-bars').innerHTML=barList(countBy(included,'language'),'violet');let insights=[];if(!included.length)insights.push(['No included evidence','Screen records and mark relevant studies as Included to begin descriptive gap analysis.']);else{const m=countBy(included,'method'),regions=countBy(included,'region'),langs=countBy(included,'language');if(Object.keys(m).length<3)insights.push(['Limited methodological mix',`Your included set covers only ${Object.keys(m).length} method categor${Object.keys(m).length===1?'y':'ies'}. Review whether diverse designs are relevant to your question.`]);if(Object.keys(regions).length<3)insights.push(['Narrow regional coverage',`Only ${Object.keys(regions).length} geographic categor${Object.keys(regions).length===1?'y is':'ies are'} represented. Check your search strategy for geographical scope.`]);if(Object.keys(langs).length<3)insights.push(['Language coverage to inspect',`Your included set contains ${Object.keys(langs).length} research language categor${Object.keys(langs).length===1?'y':'ies'}. Consider justified multilingual searches.`]);const missing=included.filter(x=>fieldCompleteness(x)<4);if(missing.length)insights.push(['Metadata completeness',`${missing.length} included record(s) have fewer than 4 of the 5 tracked metadata fields populated. A populated field does not guarantee validity.`]);const synthetic=included.filter(x=>x.synthetic).length;if(synthetic)insights.unshift(['Demonstration-only evidence',`${synthetic} included record(s) are synthetic. They must be replaced before any academic or policy evidence claim.`]);if(included.length<10)insights.push(['Small included set',`Only ${included.length} records are included. Counts here should not be generalized to the published literature.`]);}if(!insights.length)insights.push(['No predefined gaps flagged','This means the simple thresholds did not flag a coverage imbalance. It is not proof that a literature gap is absent.']);q('#gap-insights').innerHTML=insights.map(([h,p])=>`

${escapeHTML(h)}

${escapeHTML(p)}

`).join('');} function renderPage(name){({overview:renderOverview,library:renderLibrary,screening:renderScreening,matrix:renderMatrix,gaps:renderGaps,export:()=>{}})[name]();} function download(filename,content,type){let blob=new Blob([content],{type}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=filename;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);toast(`Prepared ${filename}`);} function metadata(){return {project:'ScholarFlow Research Intelligence Studio',project_number:19,schema_version:'1.0',exported_at:new Date().toISOString(),warning:'Synthetic demonstration records are not publications or empirical findings. Verify external references independently.',records:data};} function csvCell(v){const s=String(v==null?'':v);return '"'+s.replace(/"/g,'""')+'"';} function exportCSV(){const columns=['id','title','authors','year','method','region','language','participants','topic','findings','limitations','source','synthetic','status','notes'];download('scholarflow-19-studies.csv',[columns.join(','),...data.map(r=>columns.map(k=>csvCell(r[k])).join(','))].join('\r\n'),'text/csv;charset=utf-8');} function exportMarkdown(){const date=new Date().toISOString().slice(0,10);const lines=['# ScholarFlow Project 19: Screening Review',`Export date: ${date}`,'','> **Integrity warning:** Initial demo records are synthetic. Their findings do not constitute actual research evidence.','',`Records: ${data.length} | Included: ${data.filter(r=>r.status==='include').length} | Synthetic: ${data.filter(r=>r.synthetic).length}`,''];for(const r of data){lines.push(`## ${r.title}`,`- ID: ${r.id}` ,`- Decision: ${r.status}`,`- Synthetic: ${r.synthetic?'YES':'No (user entry, not independently verified)'}`,`- Author(s): ${r.authors||'Not provided'}`,`- Year: ${r.year??'Not provided'}`,`- Method: ${r.method}`,`- Region: ${r.region}`,`- Language: ${r.language}`,`- Participants: ${r.participants??'Not provided'}`,`- Source: ${r.source||'Not provided'}`,`- Field completeness: ${fieldCompleteness(r)}/5 (not a quality grade)`,`- Findings: ${r.findings||'Not provided'}`,`- Limitations: ${r.limitations||'Not provided'}`,`- Reviewer notes: ${r.notes||'Not provided'}`,'');}download('scholarflow-19-review.md',lines.join('\n'),'text/markdown;charset=utf-8');} function addRecord(form){const f=new FormData(form);const r={id:`USR-${Date.now()}-${Math.random().toString(36).slice(2,7)}`,title:String(f.get('title')||'').trim(),authors:String(f.get('authors')||'').trim(),year:Number(f.get('year'))||null,method:String(f.get('method')),region:String(f.get('region')||'').trim()||'Not specified',language:String(f.get('language')||'').trim()||'Not specified',participants:f.get('participants')===''?null:Number(f.get('participants')),source:String(f.get('source')||'').trim(),topic:String(f.get('topic')||'').trim(),findings:String(f.get('findings')||'').trim(),limitations:String(f.get('limitations')||'').trim(),synthetic:false,status:'unscreened',notes:''};if(!r.title)return;data.unshift(r);persist();q('#study-dialog').close();form.reset();showPage('library');toast('Study added. Verify source details independently.');} q('#nav').addEventListener('click',e=>{const b=e.target.closest('[data-page]');if(b)showPage(b.dataset.page);}); document.addEventListener('click',e=>{const b=e.target.closest('[data-goto]');if(b)showPage(b.dataset.goto);}); q('#add-record-top').addEventListener('click',()=>q('#study-dialog').showModal()); q('#close-dialog').addEventListener('click',()=>q('#study-dialog').close());q('#cancel-dialog').addEventListener('click',()=>q('#study-dialog').close()); q('#study-form').addEventListener('submit',e=>{e.preventDefault();if(e.target.reportValidity())addRecord(e.target);}); q('#dismiss-notice').addEventListener('click',()=>{q('.notice').hidden=true;}); for(const selector of ['#search','#filter-method','#filter-status'])q(selector).addEventListener(selector==='#search'?'input':'change',renderLibrary); q('#library-list').addEventListener('click',e=>{const b=e.target.closest('[data-delete]');if(!b)return;const r=data.find(x=>x.id===b.dataset.delete);if(r&&confirm(`Delete “${r.title}”? This cannot be undone unless you exported a backup.`)){data=data.filter(x=>x.id!==r.id);persist();renderLibrary();toast('Record deleted.');}}); q('#screening-list').addEventListener('click',e=>{const b=e.target.closest('[data-decision]');if(!b)return;const r=data.find(x=>x.id===b.dataset.id);if(r){r.status=safeStatus(b.dataset.decision);persist();renderScreening();toast('Screening decision saved.');}}); q('#screening-list').addEventListener('input',e=>{if(!e.target.matches('[data-notes]'))return;const r=data.find(x=>x.id===e.target.dataset.notes);if(r){r.notes=e.target.value.slice(0,2500);persist();}}); q('#export-json').addEventListener('click',()=>download('scholarflow-19-workspace.json',JSON.stringify(metadata(),null,2),'application/json')); q('#export-csv').addEventListener('click',exportCSV);q('#export-md').addEventListener('click',exportMarkdown); q('#import-json').addEventListener('change',async e=>{const file=e.target.files?.[0];if(!file)return;try{if(file.size>3_000_000)throw new Error('Maximum import size is 3 MB.');const obj=JSON.parse(await file.text());const arr=Array.isArray(obj)?obj:obj.records;if(!Array.isArray(arr)||arr.length>2000)throw new Error('Expected an array of up to 2,000 records.');const parsed=arr.map(recordFrom);if(new Set(parsed.map(r=>r.id)).size!==parsed.length)throw new Error('Record IDs must be unique.');if(confirm(`Replace the current library of ${data.length} records with ${parsed.length} imported records?`)){data=parsed;persist();showPage('overview');toast(`Imported ${parsed.length} records.`);}}catch(err){toast(`Import failed: ${err.message}`);}finally{e.target.value='';}}); q('#restore-sample').addEventListener('click',()=>{if(confirm('Replace your current records and notes with the synthetic demo dataset?')){data=clone(DEFAULTS);persist();showPage('overview');toast('Synthetic demo restored.');}}); q('#clear-library').addEventListener('click',()=>{if(confirm('Erase all current records and notes from this browser? Export first if you need a backup.')){data=[];persist();showPage('overview');toast('Library cleared.');}}); showPage('overview'); })();