Spaces:
Running
Running
Protect notebook history and respect reduced motion
Browse files- UPGRADE_NOTES.md +7 -0
- app.js +15 -5
- test-storage.cjs +11 -0
UPGRADE_NOTES.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Focused upgrade — 2026-10-20
|
| 2 |
+
|
| 3 |
+
Unreadable notebook data is preserved. New results remain exportable when saving is blocked. A best-effort stale-history check rejects saves after another tab changes the notebook. Reduced-motion preferences also apply to result scrolling.
|
| 4 |
+
|
| 5 |
+
Verification: `node test-storage.cjs`
|
| 6 |
+
|
| 7 |
+
Static hosted files changed; existing downloadable companion source archives remain upstream originals. No new runtime dependencies.
|
app.js
CHANGED
|
@@ -6,15 +6,25 @@ const printable = value => typeof value === 'string' ? value : JSON.stringify(va
|
|
| 6 |
function element(tag, content, className) { const node = document.createElement(tag); if(content !== undefined) node.textContent = printable(content); if(className) node.className = className; return node; }
|
| 7 |
const storageKey = 'pocket-phd-static-runs';
|
| 8 |
function saveRun(run, runs) {
|
| 9 |
-
try {
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
return run;
|
| 12 |
}
|
| 13 |
-
function savedRuns() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
async function request(path, options={}) {
|
| 15 |
if(path==='/api/config') return {live_available:Boolean(navigator.gpu),model:'Local WebGPU model — load before running'};
|
| 16 |
let runs;
|
| 17 |
-
try {runs=savedRuns();} catch(error) {if(path!=='/api/research') throw error;runs=
|
| 18 |
if(path==='/api/research') {
|
| 19 |
const input=JSON.parse(options.body);
|
| 20 |
if(input.mode==='live' && !window.PocketResearch.ready()) throw new Error('Load the free local AI first.');
|
|
@@ -63,7 +73,7 @@ function renderHypotheses(run) { const target=$('hypotheses'); target.replaceChi
|
|
| 63 |
function safeURL(value) { try {const url=new URL(value); return ['https:','http:'].includes(url.protocol) ? url.href : null;}catch{return null;} }
|
| 64 |
function renderSources(run) { const target=$('evidence');target.replaceChildren();if(run.retrieval) {const info=element('article',undefined,'notice');info.append(element('h3','Literature search'));fields(info,run.retrieval);target.append(info);}array(run.sources).forEach((source,index)=>{const node=card(source,index,source.id || 'Source');node.classList.add('source-card');node.append(element('p',[source.year,Array.isArray(source.authors)?source.authors.join(', '):source.authors].filter(Boolean).join(' · '),'meta'));if(source.abstract)node.append(element('p',source.abstract));if(source.provenance)node.append(element('p',`Provenance: ${printable(source.provenance)}`,'meta'));const url=safeURL(source.url);if(url){const link=element('a',source.url,'source-link');link.href=url;link.target='_blank';link.rel='noopener noreferrer';node.append(link);}else if(source.url)node.append(element('p',source.url));target.append(node);});if(!array(run.sources).length)target.append(element('p','No sources were returned. Findings without source support should be treated as exploratory proposals.','empty')); }
|
| 65 |
function renderAgents(run) {const target=$('agents');target.replaceChildren();array(run.agents).forEach((agent,index)=>{const node=card(agent,index,'Agent');node.append(element('span',agent.status || 'Recorded','tag'),element('pre',agent.error || printable(agent.output) || 'No output recorded.','agent-output'));target.append(node);});if(!target.childNodes.length)target.append(element('p','No agent outputs were returned.','empty'));}
|
| 66 |
-
function showRun(run) {currentRun=run;$('results').hidden=false;$('results-title').textContent=run.question || 'Research investigation';$('run-mode').textContent=run.mode==='demo'?'METHOD DEMO · ILLUSTRATIVE OUTPUT':'LIVE INVESTIGATION · NOVELTY UNVERIFIED';$('result-notice').textContent=run.mode==='demo'?'This is an illustrative method demonstration. Its deterministic hypotheses are not validated research findings or a comprehensive review.':'Model-generated proposals require source verification, domain review, and replication. Novelty has not been established.';if(run.status && run.status!=='complete'){$('result-notice').textContent=`Investigation ${run.status}. ${run.error || 'Some agent stages did not complete. Review the agent notebook for details.'}`;$('result-notice').classList.add('run-failure');}else{$('result-notice').classList.remove('run-failure');}renderGaps(run);renderHypotheses(run);renderSources(run);renderAgents(run);$('limitations').replaceChildren();array(run.limitations).forEach(limit=>$('limitations').append(element('p',limit)));if(!array(run.limitations).length)$('limitations').append(element('p','No run-specific limitations were returned. This tool cannot establish novelty or validate causal claims.'));$('results').scrollIntoView({behavior:'smooth',block:'start'});}
|
| 67 |
async function history() {try{const runs=await request('/api/runs');const target=$('history-list');target.replaceChildren();runs.forEach(run=>{const button=element('button',run.question || run.id,'history-item');button.type='button';button.append(element('small',[run.mode,run.created_at ? new Date(run.created_at).toLocaleString():null].filter(Boolean).join(' · ')));button.onclick=async()=>{try{showRun(await request(`/api/runs/${encodeURIComponent(run.id)}`));}catch(error){status(error.message,'error');}};target.append(button);});if(!runs.length)target.append(element('p','Your investigations will appear here.','hint'));}catch{$('history-list').replaceChildren(element('p','Research history is temporarily unavailable.','hint'));}}
|
| 68 |
$('research-form').addEventListener('submit',async event=>{event.preventDefault();let sources=[];try{const pasted=$('sources').value.trim();if(pasted){sources=JSON.parse(pasted);if(!Array.isArray(sources))throw new Error('Sources must be a JSON array.');if(sources.some(source=>!source || typeof source!=='object' || typeof source.title!=='string' || !source.title.trim()))throw new Error('Each source needs a title. URLs and excerpts are optional.');}}catch(error){status(`Check your evidence: ${error.message}`,'error');return;}const question=$('question').value.trim();if(!question){status('Enter a research question.','error');return;}const mode=document.querySelector('input[name="mode"]:checked').value;$('run-button').disabled=true;$('research-form').setAttribute('aria-busy','true');status(mode==='live'?'The six-agent investigation is running. Live research may take several minutes; stage outputs will appear when the investigation completes.':'Running the six-role browser demonstration…','pending');try{const run=await request('/api/research',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question,mode,sources,retrieve:$('retrieve').checked,search_query:$('search-query').value.trim()})});showRun(run);status(run.storage_warning || (run.status==='failed'?'The investigation failed. Review the saved agent notebook for details.':run.status==='partial'?'A partial investigation was saved. Review incomplete stages in the agent notebook.':'Investigation saved to your research notebook.'),run.storage_warning || run.status==='failed'?'error':'');await history();}catch(error){status(error.message,'error');}finally{investigationController=null;$('stop-run').hidden=true;$('run-button').disabled=false;$('research-form').removeAttribute('aria-busy');}});
|
| 69 |
$('example').onclick=()=>{$('question').value='How might retrieval practice and sleep timing interact to improve long-term learning in university students?';$('search-query').value='retrieval practice sleep memory';$('question').focus();};
|
|
|
|
| 6 |
function element(tag, content, className) { const node = document.createElement(tag); if(content !== undefined) node.textContent = printable(content); if(className) node.className = className; return node; }
|
| 7 |
const storageKey = 'pocket-phd-static-runs';
|
| 8 |
function saveRun(run, runs) {
|
| 9 |
+
try {
|
| 10 |
+
if(runs===null) throw new Error('Existing notebook could not be read.');
|
| 11 |
+
if(JSON.stringify(savedRuns())!==JSON.stringify(runs)) throw new Error('Notebook changed in another tab.');
|
| 12 |
+
localStorage.setItem(storageKey,JSON.stringify([run,...runs].slice(0,50)));
|
| 13 |
+
}
|
| 14 |
+
catch {run.storage_warning='This result was not saved: storage is unavailable, full, unreadable, or changed in another tab. Existing notebook data was preserved. Export this result before leaving, then reload to inspect the notebook.';}
|
| 15 |
return run;
|
| 16 |
}
|
| 17 |
+
function savedRuns() {
|
| 18 |
+
try {
|
| 19 |
+
const runs=JSON.parse(localStorage.getItem(storageKey) || '[]');
|
| 20 |
+
if(!Array.isArray(runs) || runs.some(run=>!run || typeof run.id!=='string' || typeof run.question!=='string')) throw new Error();
|
| 21 |
+
return runs;
|
| 22 |
+
} catch { throw new Error('Browser notebook storage is unavailable or unreadable. Existing data has been preserved.'); }
|
| 23 |
+
}
|
| 24 |
async function request(path, options={}) {
|
| 25 |
if(path==='/api/config') return {live_available:Boolean(navigator.gpu),model:'Local WebGPU model — load before running'};
|
| 26 |
let runs;
|
| 27 |
+
try {runs=savedRuns();} catch(error) {if(path!=='/api/research') throw error;runs=null;}
|
| 28 |
if(path==='/api/research') {
|
| 29 |
const input=JSON.parse(options.body);
|
| 30 |
if(input.mode==='live' && !window.PocketResearch.ready()) throw new Error('Load the free local AI first.');
|
|
|
|
| 73 |
function safeURL(value) { try {const url=new URL(value); return ['https:','http:'].includes(url.protocol) ? url.href : null;}catch{return null;} }
|
| 74 |
function renderSources(run) { const target=$('evidence');target.replaceChildren();if(run.retrieval) {const info=element('article',undefined,'notice');info.append(element('h3','Literature search'));fields(info,run.retrieval);target.append(info);}array(run.sources).forEach((source,index)=>{const node=card(source,index,source.id || 'Source');node.classList.add('source-card');node.append(element('p',[source.year,Array.isArray(source.authors)?source.authors.join(', '):source.authors].filter(Boolean).join(' · '),'meta'));if(source.abstract)node.append(element('p',source.abstract));if(source.provenance)node.append(element('p',`Provenance: ${printable(source.provenance)}`,'meta'));const url=safeURL(source.url);if(url){const link=element('a',source.url,'source-link');link.href=url;link.target='_blank';link.rel='noopener noreferrer';node.append(link);}else if(source.url)node.append(element('p',source.url));target.append(node);});if(!array(run.sources).length)target.append(element('p','No sources were returned. Findings without source support should be treated as exploratory proposals.','empty')); }
|
| 75 |
function renderAgents(run) {const target=$('agents');target.replaceChildren();array(run.agents).forEach((agent,index)=>{const node=card(agent,index,'Agent');node.append(element('span',agent.status || 'Recorded','tag'),element('pre',agent.error || printable(agent.output) || 'No output recorded.','agent-output'));target.append(node);});if(!target.childNodes.length)target.append(element('p','No agent outputs were returned.','empty'));}
|
| 76 |
+
function showRun(run) {currentRun=run;$('results').hidden=false;$('results-title').textContent=run.question || 'Research investigation';$('run-mode').textContent=run.mode==='demo'?'METHOD DEMO · ILLUSTRATIVE OUTPUT':'LIVE INVESTIGATION · NOVELTY UNVERIFIED';$('result-notice').textContent=run.mode==='demo'?'This is an illustrative method demonstration. Its deterministic hypotheses are not validated research findings or a comprehensive review.':'Model-generated proposals require source verification, domain review, and replication. Novelty has not been established.';if(run.status && run.status!=='complete'){$('result-notice').textContent=`Investigation ${run.status}. ${run.error || 'Some agent stages did not complete. Review the agent notebook for details.'}`;$('result-notice').classList.add('run-failure');}else{$('result-notice').classList.remove('run-failure');}renderGaps(run);renderHypotheses(run);renderSources(run);renderAgents(run);$('limitations').replaceChildren();array(run.limitations).forEach(limit=>$('limitations').append(element('p',limit)));if(!array(run.limitations).length)$('limitations').append(element('p','No run-specific limitations were returned. This tool cannot establish novelty or validate causal claims.'));$('results').scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth',block:'start'});}
|
| 77 |
async function history() {try{const runs=await request('/api/runs');const target=$('history-list');target.replaceChildren();runs.forEach(run=>{const button=element('button',run.question || run.id,'history-item');button.type='button';button.append(element('small',[run.mode,run.created_at ? new Date(run.created_at).toLocaleString():null].filter(Boolean).join(' · ')));button.onclick=async()=>{try{showRun(await request(`/api/runs/${encodeURIComponent(run.id)}`));}catch(error){status(error.message,'error');}};target.append(button);});if(!runs.length)target.append(element('p','Your investigations will appear here.','hint'));}catch{$('history-list').replaceChildren(element('p','Research history is temporarily unavailable.','hint'));}}
|
| 78 |
$('research-form').addEventListener('submit',async event=>{event.preventDefault();let sources=[];try{const pasted=$('sources').value.trim();if(pasted){sources=JSON.parse(pasted);if(!Array.isArray(sources))throw new Error('Sources must be a JSON array.');if(sources.some(source=>!source || typeof source!=='object' || typeof source.title!=='string' || !source.title.trim()))throw new Error('Each source needs a title. URLs and excerpts are optional.');}}catch(error){status(`Check your evidence: ${error.message}`,'error');return;}const question=$('question').value.trim();if(!question){status('Enter a research question.','error');return;}const mode=document.querySelector('input[name="mode"]:checked').value;$('run-button').disabled=true;$('research-form').setAttribute('aria-busy','true');status(mode==='live'?'The six-agent investigation is running. Live research may take several minutes; stage outputs will appear when the investigation completes.':'Running the six-role browser demonstration…','pending');try{const run=await request('/api/research',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question,mode,sources,retrieve:$('retrieve').checked,search_query:$('search-query').value.trim()})});showRun(run);status(run.storage_warning || (run.status==='failed'?'The investigation failed. Review the saved agent notebook for details.':run.status==='partial'?'A partial investigation was saved. Review incomplete stages in the agent notebook.':'Investigation saved to your research notebook.'),run.storage_warning || run.status==='failed'?'error':'');await history();}catch(error){status(error.message,'error');}finally{investigationController=null;$('stop-run').hidden=true;$('run-button').disabled=false;$('research-form').removeAttribute('aria-busy');}});
|
| 79 |
$('example').onclick=()=>{$('question').value='How might retrieval practice and sleep timing interact to improve long-term learning in university students?';$('search-query').value='retrieval practice sleep memory';$('question').focus();};
|
test-storage.cjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm');
|
| 2 |
+
const source=fs.readFileSync(__dirname+'/app.js','utf8').split('async function request')[0];
|
| 3 |
+
let raw='[]',fail=false;
|
| 4 |
+
const context=vm.createContext({localStorage:{getItem:()=>raw,setItem:(_,value)=>{if(fail)throw Error('quota');raw=value;}},document:{getElementById:()=>null}});
|
| 5 |
+
vm.runInContext(source,context);
|
| 6 |
+
const run=()=>vm.runInContext("({id:'new',question:'New question'})",context);
|
| 7 |
+
let result=run();context.result=result;vm.runInContext('saveRun(result,savedRuns())',context);assert.equal(JSON.parse(raw)[0].id,'new');
|
| 8 |
+
for(const invalid of ['{','null','{}','[null]','[{"id":7}]']){raw=invalid;assert.throws(()=>vm.runInContext('savedRuns()',context));result=run();context.result=result;vm.runInContext('saveRun(result,null)',context);assert.equal(raw,invalid);assert.ok(result.storage_warning);}
|
| 9 |
+
raw='[]';vm.runInContext('var previous=savedRuns()',context);raw='[{"id":"other","question":"Other tab"}]';context.result=run();vm.runInContext('saveRun(result,previous)',context);assert.equal(JSON.parse(raw)[0].id,'other');assert.ok(context.result.storage_warning);
|
| 10 |
+
raw='[]';fail=true;context.result=run();vm.runInContext('saveRun(result,[])',context);assert.ok(context.result.storage_warning);assert.equal(raw,'[]');
|
| 11 |
+
console.log('PASS: normal save, malformed histories preserved, stale-tab save blocked, quota failure exportable');
|