Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion analyzer-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
// Prose context must load BEFORE analyzer.js runs an analysis: analyze() feature-detects
// these globals, so without them the worker would silently produce context-free scores while
// the main-thread fallback produced context-aware ones.
importScripts('prose-context.js?v=1', 'prose-norms.js?v=1', 'analyzer.js?v=25');
// Keep these versions in step with app.html: a stale analyzer here means the worker scores
// with old detectors while the page believes it is running the new ones.
importScripts('prose-context.js?v=1', 'prose-norms.js?v=2', 'analyzer.js?v=27');

// Note: _currentVersion tracking here is structurally inert — the Worker JS runtime
// is single-threaded, so _currentVersion cannot change while Analyzer.analyze() runs
Expand Down
329 changes: 233 additions & 96 deletions analyzer.js

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions app.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>&#127807;</text></svg>">
<title>AuthorScrolls - Author's Best Buddy</title>
<link rel="stylesheet" href="styles.css?v=5">
<link rel="stylesheet" href="styles.css?v=6">
<link rel="stylesheet" href="styles-mobile.css?v=2">
<link rel="stylesheet" href="workspace-header.css?v=2">
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;0,700;1,400&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
Expand Down Expand Up @@ -439,8 +439,8 @@ <h2 style="font-family:Lora,serif;color:var(--gold-l);margin-bottom:.3rem">Choos
</div>

<script src="prose-context.js?v=1"></script>
<script src="prose-norms.js?v=1"></script>
<script src="analyzer.js?v=25"></script>
<script src="prose-norms.js?v=2"></script>
<script src="analyzer.js?v=27"></script>
<script src="grammar-enhance.js?v=17"></script>
<script src="manuscript-parser.js?v=3"></script>
<script src="book-intelligence.js?v=3"></script>
Expand All @@ -456,12 +456,12 @@ <h2 style="font-family:Lora,serif;color:var(--gold-l);margin-bottom:.3rem">Choos
<script src="intelligence-pipeline.js?v=2"></script>
<script src="manuscript-retrieval.js?v=2"></script>
<script src="ai-engine.js?v=21"></script>
<script src="fixer.js?v=17"></script>
<script src="fixer.js?v=18"></script>
<!-- intelligence dependencies are loaded above AIEngine -->
<script data-intelligence-order="complete"></script>
<script src="storage.js?v=20"></script>
<script src="analysis-runner.js"></script>
<script src="app.js?v=22"></script>
<script src="app.js?v=24"></script>
<script src="workspace-header.js"></script>
<script src="mobile-panels.js?v=21"></script>

Expand Down
59 changes: 39 additions & 20 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,11 @@ function _setPanelsHidden(hidden){

// REPLACE & FIX: select the highlight text and use execCommand to replace
// This works with native undo (Ctrl+Z) and properly updates the DOM
// Findings the engine set aside for their passage (see prose-norms.js) stay on the record
// with their reason, but they are not what the scores were built from and not what the
// author is asked to act on.
function liveIssues(r){return ((r&&r.issues)||[]).filter(i=>!i.contextSuppressed)}

function replaceAndFix(hlElement){
const type=hlElement.dataset.t;
const suggestion=hlElement.dataset.s||'';
Expand Down Expand Up @@ -1154,7 +1159,7 @@ function showDetail(cat){
const copyTypes=new Set(['passive','adverb','cliche','wordy','confused-word','repetition']);
let issueRows='';
if(cat==='sh_clarity'){
const ci=(r.issues||[]).filter(i=>copyTypes.has(i.type)).slice(0,5);
const ci=liveIssues(r).filter(i=>copyTypes.has(i.type)).slice(0,5);
if(ci.length>0)issueRows='<div style="margin-top:.5rem;font-size:.72rem;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em">Top copy issues</div>'+ci.map(i=>'<div class="rpd-issue"><div class="rpd-issue-head">'+esc(i.text.substring(0,50))+'</div><div class="rpd-desc">'+esc(i.suggestion)+'</div></div>').join('');
}
d.innerHTML='<div class="rpd-title"><span style="font-size:1.1rem">'+shTitles[cat]+'</span><span style="font-size:.7rem;color:var(--muted)">score: <b style="color:'+col+'">'+score+'/100</b></span></div>'+
Expand All @@ -1179,11 +1184,11 @@ function showDetail(cat){
}else if(cat==='plot'||cat==='style'){
issues=[];
}else if(cat==='copy'){
issues=r.issues.filter(i=>copyTypes.has(i.type));
issues=liveIssues(r).filter(i=>copyTypes.has(i.type));
}else{
const directMap={pacing:'sentence-length',showTell:'show-tell',grammar:'grammar'};
const t=directMap[cat];
issues=t?r.issues.filter(i=>i.type===t):[];
issues=t?liveIssues(r).filter(i=>i.type===t):[];
}

// Sort by severity: high first, then medium, then low
Expand All @@ -1194,7 +1199,9 @@ function showDetail(cat){

// Determine if suggestion has a concrete replacement
function hasConcreteFix(iss){
return !!iss.suggestion.match(/Replace with:\s*".+?"/)||!!iss.suggestion.match(/Try:\s*.+/i)||iss.type==='adverb'||iss.type==='wordy'||iss.type==='cliche';
// Mechanical fixes and AI-grounded replacements only; a synonym from a fixed list is
// offered for the author to weigh, never applied for them.
return !!iss.suggestion.match(/Replace with:\s*".+?"/)||iss.type==='adverb'||iss.type==='wordy'||iss.type==='cliche';
}

// Progress indicator
Expand Down Expand Up @@ -1322,7 +1329,7 @@ function renderDetailed(r){
const le=r.lineEditing||{};
const lineRows=['Stylistic editing: tone, flow, precision, pacing, POV, extraneous language'];
lineRows.push(sr('Tone / Register Evidence',(le.tone?.score??'N/A')+(le.tone?.score==null?'':'/100')));
lineRows.push(sr('Sentence Continuity',(le.flow?.score??'N/A')+(le.flow?.score==null?'':'/100')));
lineRows.push(sr('Sentence Continuity',le.flow?.score==null?(le.flow?.advisory?'N/A — not judged for nonfiction':'N/A'):le.flow.score+'/100'));
lineRows.push(sr('Word Precision',(le.precision?.score??'N/A')+(le.precision?.score==null?'':'/100')));
lineRows.push(sr('Sentence Rhythm',(le.pacing?.score??'N/A')+(le.pacing?.score==null?'':'/100')));
lineRows.push(sr('POV Review',le.pov?.score==null?'Advisory only':le.pov.score+'/100'));
Expand Down Expand Up @@ -1368,11 +1375,13 @@ function renderDetailed(r){
}
if(r.scores?.dialogue!=null)h+=sec('Dialogue',r.scores.dialogue,dlRows);
// Pacing heatmap
if(r.pacing){const cols={action:'#c0392b',dialogue:'#2980b9',description:'#27ae60',exposition:'#f39c12',reflection:'#8e44ad'};
if(r.pacing){const cols={action:'#c0392b',dialogue:'#2980b9',description:'#27ae60',exposition:'#f39c12',reflection:'#8e44ad',mixed:'#7f8c8d'};
const segs=r.pacing.segments||[];
h+='<div class="a-sec"><h3>Pacing Heatmap</h3><div style="font-size:.65rem;color:var(--dim);margin-bottom:.35rem">'+segs.length+' observed manuscript segments · '+(r.pacing.segmentSize||200)+' words per segment</div><div class="hm-wrap">'+segs.map((s,i)=>'<div class="hm-blk" style="background:'+cols[s.type]+'" title="Words '+(s.startWord||'?')+'–'+(s.endWord||'?')+': '+s.type+' · dialogue '+(s.dialogueDensity??0)+'% · action '+(s.actionDensity??0)+'%"></div>').join('')+'</div><div class="hm-leg"><span><span class="hm-dot" style="background:#c0392b"></span>Action</span><span><span class="hm-dot" style="background:#2980b9"></span>Dialogue</span><span><span class="hm-dot" style="background:#27ae60"></span>Description</span><span><span class="hm-dot" style="background:#f39c12"></span>Exposition</span><span><span class="hm-dot" style="background:#8e44ad"></span>Reflection</span></div></div>'}
// Characters
if(r.characters?.applicable!==false&&r.characters?.list?.length>0){const mx=Math.max(...r.characters.list.map(c=>c.mentions));h+='<div class="a-sec"><h3>Detected Characters</h3><div style="font-size:.65rem;color:var(--dim);margin-bottom:.35rem">Recurring person-name candidates with contextual evidence</div><div class="ch-grid">'+r.characters.list.map(c=>'<div class="ch-card"><div class="ch-name">'+esc(c.name)+'</div><div class="ch-cnt">'+c.mentions+' mentions · '+(c.evidenceCount||0)+' contextual signals</div><div class="ch-bar"><div class="ch-fill" style="width:'+Math.round(c.mentions/mx*100)+'%"></div></div></div>').join('')+'</div></div>'}
const hmNote=r.pacing.source==='passage-classifier'?'Each block is coloured by the kind of prose that covers most of it; grey is a stretch the classifier would not force into one mode.':'Keyword estimate (passage classifier unavailable).';
h+='<div class="a-sec"><h3>Pacing Heatmap</h3><div style="font-size:.65rem;color:var(--dim);margin-bottom:.35rem">'+segs.length+' observed manuscript segments · '+(r.pacing.segmentSize||200)+' words per segment</div><div class="hm-wrap">'+segs.map((s,i)=>'<div class="hm-blk" style="background:'+(cols[s.type]||cols.mixed)+'" title="Words '+(s.startWord||'?')+'–'+(s.endWord||'?')+': '+s.type+(s.share?' ('+s.share+'% of the block)':'')+'"></div>').join('')+'</div><div class="hm-leg"><span><span class="hm-dot" style="background:#c0392b"></span>Action</span><span><span class="hm-dot" style="background:#2980b9"></span>Dialogue</span><span><span class="hm-dot" style="background:#27ae60"></span>Description</span><span><span class="hm-dot" style="background:#f39c12"></span>Exposition</span><span><span class="hm-dot" style="background:#8e44ad"></span>Reflection</span><span><span class="hm-dot" style="background:#7f8c8d"></span>Mixed</span></div><p style="font-size:.7rem;color:var(--muted);margin-top:.3rem">'+hmNote+'</p></div>'}
// Names: only words capitalised where grammar did not force it (see Analyzer.analyzeCharacters).
// Expository nonfiction reports the dimension as not applicable and shows nothing.
if(r.characters?.applicable!==false&&r.characters?.list?.length>0){const mx=Math.max(...r.characters.list.map(c=>c.mentions));const chTitle=Analyzer.isNonfiction(r.genre)?'Names in the text':'Characters';h+='<div class="a-sec"><h3>'+chTitle+'</h3><div style="font-size:.65rem;color:var(--dim);margin-bottom:.35rem">Recurring names with evidence beyond a sentence-opening capital</div><div class="ch-grid">'+r.characters.list.map(c=>'<div class="ch-card" title="'+c.midSentence+' mid-sentence, '+c.dialogueCount+' speech attributions"><div class="ch-name">'+esc(c.name)+'</div><div class="ch-cnt">'+c.mentions+' mentions · '+(c.evidenceCount||0)+' signals</div><div class="ch-bar"><div class="ch-fill" style="width:'+Math.round(c.mentions/mx*100)+'%"></div></div></div>').join('')+'</div></div>'}
// Genre-Specific Elements Scanner
if(r.genreElements&&r.genreElements.applicable){
const ge=r.genreElements;
Expand Down Expand Up @@ -2067,7 +2076,7 @@ function renderAnnotatedAsPages(text,issues){
if(inlineIssues.length>MAX_INLINE_HL){
const sevRank={high:0,medium:1,low:2};
inlineIssues=[...inlineIssues]
.sort((a,b)=>((sevRank[a.severity]??3)-(sevRank[b.severity]??3))||((b.confidence||0)-(a.confidence||0)))
.sort((a,b)=>((a.contextSuppressed?1:0)-(b.contextSuppressed?1:0))||((sevRank[a.severity]??3)-(sevRank[b.severity]??3))||((b.confidence||0)-(a.confidence||0)))
.slice(0,MAX_INLINE_HL);
}
// Build non-overlapping issues sorted by position
Expand Down Expand Up @@ -2151,18 +2160,27 @@ function renderAnnotatedAsPages(text,issues){
const labels={passive:'Passive voice detected',adverb:'Adverb detected',cliche:'Cliche detected','weak-verb':'Weak verb detected',wordy:'Wordy phrase','show-tell':'Show vs Tell',repetition:'Word repetition','sentence-length':'Long sentence','confused-word':'Wrong word',grammar:'Grammar issue'};
const t=hl.dataset.t;
const sug=hl.dataset.s||'';
// Determine if this issue has an auto-replacement available
const msg=hl.dataset.m||'';
// A finding the engine set aside for this passage is shown quietly, with its reason,
// and never offered as something to fix.
const setAside=hl.classList.contains('hl-ctx')?(hl.dataset.r||'Set aside for this passage.'):'';
// Auto-fix only where the replacement is mechanical (remove an adverb, swap a wordy
// phrase, the mapped clich\u00E9) or came from the AI reader with the sentence in view.
// A verb or synonym from a fixed list is never applied for the author: it is shown
// for them to weigh, and the button takes them to the word instead.
const hasAI=!!sug.match(/Replace with:\s*".+?"/);
const hasTry=!!sug.match(/Try:\s*.+/i);
// Types with reliable auto-fix: wordy (has "Replace with"), weak-verb/repetition (has "Try:"), adverb (remove), passive (restructure), cliche (has map)
const canAutoFix=hasAI||hasTry||t==='adverb'||t==='wordy'||t==='cliche';
const fixLabel=canAutoFix?'Replace &amp; Fix':'Edit Here';
tip.innerHTML='<div class="tip-cat">'+esc(labels[t]||t)+'</div><div class="tip-sug">\u2192 Suggestion:</div><div class="tip-quote">\u201C'+esc(sug)+'\u201D</div><div class="tip-btns"><button class="tip-fix" id="tip-fix-btn">'+esc(fixLabel)+'</button><button class="tip-ign" id="tip-ign-btn">Ignore</button></div>';
const canAutoFix=!setAside&&(hasAI||t==='adverb'||t==='wordy'||t==='cliche');
const fixLabel=canAutoFix?'Replace & Fix':'Edit Here';
const body=setAside
?'<div class="tip-sug">Set aside for this passage:</div><div class="tip-quote">'+esc(setAside)+'</div>'
:(msg&&t==='weak-verb'?'<div class="tip-sug">'+esc(msg)+'</div>':'')+'<div class="tip-sug">\u2192 Suggestion:</div><div class="tip-quote">\u201C'+esc(sug)+'\u201D</div>';
tip.innerHTML='<div class="tip-cat">'+esc(labels[t]||t)+'</div>'+body+'<div class="tip-btns">'+(setAside?'':'<button class="tip-fix" id="tip-fix-btn">'+esc(fixLabel)+'</button>')+'<button class="tip-ign" id="tip-ign-btn">Ignore</button></div>';
tip.classList.add('on');
const rect=hl.getBoundingClientRect();
tip.style.top=(rect.bottom+8)+'px';
tip.style.left=Math.min(rect.left,window.innerWidth-360)+'px';
$('tip-fix-btn').onclick=()=>{
const fixBtn=$('tip-fix-btn');
if(fixBtn)fixBtn.onclick=()=>{
if(canAutoFix){replaceAndFix(activeHL)}
else{
// Focus the highlight text for manual editing
Expand Down Expand Up @@ -2193,7 +2211,8 @@ function getAnnotatedSlice(fullText,start,end,issues){
const iStart=Math.max(i.index,start);
const iEnd=Math.min(i.index+i.length,end);
if(iStart>pos)h+=esc(fullText.substring(pos,iStart));
h+='<span class="hl" data-t="'+i.type+'" data-m="'+escA(i.message)+'" data-s="'+escA(i.suggestion)+'" data-q="'+escA(i.text.substring(0,60))+'">'+esc(fullText.substring(iStart,iEnd))+'</span>';
const setAside=i.contextSuppressed&&i._context?' data-r="'+escA(i._context.reason||'')+'"':'';
h+='<span class="hl'+(i.contextSuppressed?' hl-ctx':'')+'" data-t="'+i.type+'" data-m="'+escA(i.message)+'" data-s="'+escA(i.suggestion)+'" data-q="'+escA(i.text.substring(0,60))+'"'+setAside+'>'+esc(fullText.substring(iStart,iEnd))+'</span>';
pos=iEnd;
}
if(pos<end)h+=esc(fullText.substring(pos,end));
Expand Down Expand Up @@ -2237,7 +2256,7 @@ document.querySelectorAll('.rtab').forEach(t=>{t.addEventListener('click',()=>{
const d=$('rp-detail');const r=analysisResult;
if(mode==='suggestions'){renderRight(r);return}
if(mode==='rewrite'){
const samples=r.issues.filter(i=>_REWRITE_TYPES.has(i.type)).slice(0,8);
const samples=liveIssues(r).filter(i=>_REWRITE_TYPES.has(i.type)).slice(0,8);
d.innerHTML='<div class="rpd-title">Rewrite Suggestions <span class="rpd-exp-badge" style="margin-left:.4rem">Experimental</span></div>'+(samples.length===0?'<p style="color:var(--muted);font-size:.78rem">No rewrite targets found.</p>':samples.map(i=>'<div class="rpd-issue" data-issue-text="'+escA(i.text)+'" data-issue-sug="'+escA(i.suggestion)+'" data-has-fix="'+(i.type==='adverb'||i.type==='passive'||i.type==='wordy'||i.type==='cliche'?'1':'0')+'" data-issue-type="'+escA(i.type)+'" data-issue-index="'+(i.index||0)+'"><div class="rpd-issue-head">'+esc(i.text.substring(0,40))+'</div><div class="rpd-desc">'+esc(i.suggestion)+'</div><div class="rpd-quote rpd-navigate" style="cursor:pointer" title="Click to jump to this text">\u2018'+esc(i.text.substring(0,60))+'\u2019</div><div class="rpd-btns"><button class="rpd-rewrite-btn">\u2736 Rewrite</button><button class="tip-ign rpd-ign-btn">Dismiss</button></div></div>').join(''));
d.querySelectorAll('.rpd-navigate').forEach(q=>{q.addEventListener('click',()=>{const card=q.closest('.rpd-issue');const issueText=card.dataset.issueText;document.querySelectorAll('.btab').forEach(b=>b.classList.remove('active'));document.querySelectorAll('.ms-page').forEach(p=>p.classList.remove('active'));const ab=document.querySelector('.btab[data-p="annotated"]');if(ab)ab.classList.add('active');$('ed-annotated')?.classList.add('active');const page=$('ed-annotated');const q2=issueText.substring(0,60).replace(/"/g,'&quot;');const hl=page?.querySelector('.hl[data-q="'+q2+'"]');if(hl){hl.scrollIntoView({behavior:'smooth',block:'center'});hl.style.outline='3px solid var(--gold)';hl.style.outlineOffset='3px';setTimeout(()=>{hl.style.outline=''},3000)}})});
d.querySelectorAll('.rpd-rewrite-btn').forEach(btn=>{btn.addEventListener('click',()=>{doRewrite(btn.closest('.rpd-issue'))})});
Expand Down Expand Up @@ -2443,7 +2462,7 @@ $('export-btn')?.addEventListener('click',()=>{
const issueLabels={passive:'Passive Voice','weak-verb':'Weak Verb',adverb:'Adverb',cliche:'Cliché',wordy:'Wordy','show-tell':'Show vs Tell',repetition:'Repetition','sentence-length':'Long Sentence'};

// Build annotated HTML
const sorted=[...r.issues].sort((a,b)=>a.index-b.index);
const sorted=[...liveIssues(r)].sort((a,b)=>a.index-b.index);
const noOverlap=[];let lastEnd=-1;
for(const i of sorted){if(i.index>=lastEnd){noOverlap.push(i);lastEnd=i.index+i.length}}

Expand Down
Loading
Loading