inkwell.wiki
The Tool

Most tools give you a score.
This one tells you where you were lied to.

They polished your sentences. Nobody mentioned they said nothing.

The Pipeline — Strict Order + Coherence Enforced
Director Architect (reorders for flow) Research Wordsmith Sentinel Designer Advocate Final Director (ruthless order check)
The Architect and Final Director will reorder your draft if sections break logical flow. No more books that are hard to follow.
Pricing
⭐ Founding 25
Free year
then $9.99/mo locked forever
First 25 only · 40% off standard
Spark
$9.99/mo
10 runs · single writer
Draft
$19.99/mo
30 runs · single writer
Forge
$39.99/mo
Unlimited · priority
Flesch-Kincaid 98. Zero people felt anything. Good score though.
⭐ Claim Founding 25 — 1 year free at launch →
✦ Inkwell GRAND EDITORIAL SUITE
0
words
0 min
read time
0
sentences
0
avg words/sent
grade level
Your Draft
Voice
Seed — topic, opening image, question, anything
Target Word Count
Prior Instructions (optional)
Editor

Finished Piece

Heads up — Inkwell is not a CPA, tax preparer, attorney, financial advisor, or licensed insurance agent. Numbers and recommendations on this site are estimates from data you logged. Prices, tip percentages, mileage deductions, ink valuations, gas prices, venue locations, and earnings are user-submitted or AI-generated and may be inaccurate. Verify with a qualified professional before relying. No CPA-client, attorney-client, or advisory relationship is created by use of this site. By using Inkwell you agree to the Terms. © 2026 G10be — Amarillo, Texas.
'; window.stop(); } })(); // ─── SUPABASE / BETA ─────────────────────── const SUPA_URL = 'https://zmrouoqututfndplboyc.supabase.co'; const SUPA_ANON = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inptcm91b3F1dHV0Zm5kcGxib3ljIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM0NDgyMDgsImV4cCI6MjA4OTAyNDIwOH0.LlFEregfgfs8wMuogLNCO9811wOF2XssZ0p-lyPds4E'; const WORD_LIMIT = 15000; let userEmail = ''; let wordsUsed = 0; function supaHeaders() { return { 'apikey': SUPA_ANON, 'Authorization': `Bearer ${SUPA_ANON}`, 'Content-Type': 'application/json' }; } async function submitEmail() { const raw = document.getElementById('emailInput').value.trim().toLowerCase(); if (!raw || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) { showGateErr('Please enter a valid email.'); return; } const btn = document.getElementById('gateBtn'); btn.disabled = true; btn.textContent = 'One moment...'; showGateErr(''); // Save email best-effort — don't block access on failure fetch(`${SUPA_URL}/functions/v1/email-save`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: raw, source: 'thetool' }) }).catch(() => {}); // Notify Lyle fetch('/notify-signup', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Thetool-Key': 'itk-gw-4f8a9c2e7b3d' }, body: JSON.stringify({ email: raw, source: 'thetool' }) }).catch(() => {}); userEmail = raw; wordsUsed = 0; document.getElementById('gate').classList.add('hidden'); document.getElementById('app').classList.add('show'); setTimeout(() => document.getElementById('draftText').focus(), 100); } function showGateErr(msg) { document.getElementById('gateErr').textContent = msg; } async function recordWords(count) { if (!userEmail || count <= 0) return; wordsUsed += count; } function checkWordBudget(incomingWords) { const remaining = WORD_LIMIT - wordsUsed; if (incomingWords > remaining) { const pct = Math.round((wordsUsed / WORD_LIMIT) * 100); toast(`Beta limit: ${wordsUsed.toLocaleString()} / ${WORD_LIMIT.toLocaleString()} words used (${pct}%). Visit inkwell.wiki/join for full access.`); return false; } return true; } // Focus email input on load setTimeout(() => { const el = document.getElementById('emailInput'); if (el) el.focus(); }, 200); // ─── MODE ────────────────────────────────── let currentMode = 'desk'; function setMode(m) { currentMode = m; document.getElementById('modeDesk').classList.toggle('active', m === 'desk'); document.getElementById('modeForge').classList.toggle('active', m === 'forge'); document.getElementById('draftPanel').classList.toggle('hidden', m === 'forge'); document.getElementById('seedMode').style.display = m === 'forge' ? 'block' : 'none'; } // ─── VOICE (FORGE) ───────────────────────── let forgeVoice = 'hst'; document.querySelectorAll('[data-v]').forEach(b => { b.addEventListener('click', () => { document.querySelectorAll('[data-v]').forEach(x => x.classList.remove('active')); b.classList.add('active'); forgeVoice = b.dataset.v; document.getElementById('customVoiceWrap').classList.toggle('hidden', forgeVoice !== 'custom'); }); }); // ─── PRIOR INSTRUCTIONS ──────────────────── function togglePrior() { const m = document.getElementById('priorModal'); m.style.display = m.style.display === 'flex' ? 'none' : 'flex'; } // ─── STATS ───────────────────────────────── let statsTimer = null; function onDraftInput() { clearTimeout(statsTimer); statsTimer = setTimeout(updateStats, 400); } function updateStats(text) { text = text || document.getElementById('draftText').value; const words = text.trim() ? text.trim().split(/\s+/).length : 0; const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 3).length; const readMin = Math.max(1, Math.round(words / 238)); const avg = sentences ? Math.round(words / sentences) : 0; // Flesch–Kincaid grade estimate (simplified) const syllables = text.split(/\s+/).reduce((n, w) => n + countSyllables(w), 0); const grade = sentences && words ? Math.max(1, Math.round(0.39 * (words / Math.max(1,sentences)) + 11.8 * (syllables / Math.max(1,words)) - 15.59)) : 0; flash('statWords', words.toLocaleString()); flash('statRead', readMin + ' min'); flash('statSent', sentences.toLocaleString()); flash('statAvg', avg); flash('statGrade', grade ? 'Grade ' + grade : '—'); } function flash(id, val) { const el = document.getElementById(id); const v = String(val); if (el.textContent !== v) { el.textContent = v; el.classList.add('changed'); setTimeout(() => el.classList.remove('changed'), 600); } } function countSyllables(word) { word = word.toLowerCase().replace(/[^a-z]/g, ''); if (!word) return 0; let count = word.match(/[aeiou]/g)?.length || 0; if (word.endsWith('e') && count > 1) count--; return Math.max(1, count); } function clearDraft() { document.getElementById('draftText').value = ''; updateStats(''); } // ─── PIPELINE DEFINITIONS ────────────────── const PERSONAS = [ { id:'director_brief', icon:'◈', name:'The Director', role:'Grand Literary Vision', color:'var(--director)' }, { id:'architect', icon:'⬡', name:'The Architect', role:'Master Structural Design', color:'var(--architect)' }, { id:'researcher', icon:'⬡', name:'The Researcher',role:'Epic Content & Truth Weaver', color:'var(--research)' }, { id:'advocate', icon:'⬡', name:'The Advocate', role:'Reader Soul & Stakes Guardian', color:'var(--advocate)' }, { id:'wordsmith', icon:'⬡', name:'The Wordsmith', role:'Voice Elevation & Line Majesty', color:'var(--wordsmith)' }, { id:'sentinel', icon:'⬡', name:'The Sentinel', role:'Precision Guardian & Polish', color:'var(--sentinel)' }, { id:'designer', icon:'⬡', name:'The Designer', role:'Grand Format & Presentation', color:'var(--designer)' }, { id:'director_final', icon:'◈', name:'The Director', role:'Final Grand Integrity & Elevation', color:'var(--director)' }, ]; const SYSTEMS = { director_brief: `You are The Director — the senior literary editor who reads a draft first, before any other editor touches it. Your job: identify the grand potential in this writing and craft a MASTER STYLE BRIEF that elevates it toward timeless, publishable literary art. Read cold. Identify the voice, tradition, signature moves, intentional rule-breaks. Envision the grand version: emotionally resonant, structurally masterful, voice unforgettable. Be surgical yet visionary. Be specific. OUTPUT FORMAT: ## STYLE IDENTIFICATION [One paragraph naming the tradition and architecture precisely. Name its grand potential.] ## SIGNATURE MOVES (PROTECT THESE) [Numbered. Quote each example. Explain the craft choice and how it can be made more magnificent.] ## INTENTIONAL RULE-BREAKS (PROTECT THESE) [Numbered. Name the broken convention and why it serves the piece — amplify for grandeur.] ## WATCH LIST [Things that look intentional but may be accidental. Flag for author confirmation. Note grand opportunities.] ## STYLE BRIEF FOR DOWNSTREAM EDITORS [Direct address to the editorial team. What to protect fiercely. What to push for epic impact. What the author is reaching for — and how to help them arrive grandly.] ## INITIAL READ [One honest, ambitious paragraph: what's alive and can be made legendary, what's dead and must be cut for the grand vision.] ## PREPARED DRAFT [Return the draft exactly as received. No edits.]`, architect: `You are The Architect — grand structural designer. You build a masterful, epic framework before the others polish it into a work of lasting power. You have the Style Brief. Work with the style, not against it. Envision and deliver a grand architecture: sweeping through-lines, unforgettable openings/closings, perfect pacing that builds to emotional and intellectual catharsis. CRITICAL — ENFORCE ORDER LIKE A MASTERPIECE DEMANDS IT: You MUST reorder the entire draft into perfect logical section sequence before anything else. Books with sections out of order or flow that lets the reader lose the thread are unacceptable failures. Scan for any break in narrative logic, any misplaced section, any place the reader would have to backtrack or guess the sequence. Reorder ruthlessly. Move sections, cut dead weight that breaks momentum, elevate openings and closings for maximum catharsis. The final structure must feel inevitable and grand. Never leave the reader confused about where they are in the story or argument. If order is wrong, FIX IT in the revised draft. Do not just flag — deliver the reordered masterpiece structure. Evaluate: through-line, opening, ending, section order, pacing, tonal consistency, overall grandeur. Does the structure serve the voice and elevate it to timeless status? ## STRUCTURAL FLAGS [Numbered. Cite the exact passage. Flag any out-of-order sections or flow breaks. Mark [STYLE CONFLICT] for anything that conflicts with the Style Brief. Be ruthless on ordering. Call out missed opportunities for grand scale.] ## RECOMMENDED MOVES [Specific: cut X, move Y before Z, expand W for epic impact. Prioritize reordering for flawless, grand coherence. Suggest architectural enhancements.] ## STRUCTURAL VERDICT [One paragraph. Explicitly call out if order or flow was fixed or still broken. Assess the grand potential of the new structure.] ## REVISED DRAFT [OUTPUT ONLY THE CLEAN REORDERED FULL DRAFT HERE. Put sections in strict correct logical sequence: Introduction/Setup first, then Buildup, Climax, then Conclusion/End. No extra commentary, no flags inside this section. Pure prose in the grand structure. If the input had out-of-order sections, this must be fully reordered so a reader can follow without confusion.]`, researcher: `You are The Researcher — content editor and fact-checker. You have the Style Brief. In Gonzo and experimental traditions, narrator unreliability IS content. Do not flag it as error. Evaluate: claims needing sources, gaps, unsupported assertions, unlabeled speculation. ## CONTENT FLAGS [Numbered list. For each: quote the exact phrase, then specify type: needs-source / needs-reporting / speculation-unlabeled / narrator-unreliability-check. Reference by paragraph if helpful. Keep all flags HERE in this section only.] ## GAPS IDENTIFIED [What's missing that the reader needs? Be specific.] ## CONTENT VERDICT [One paragraph.] ## REVISED DRAFT [Full clean draft — prose only. Do NOT insert inline flags, markers, or annotations into the draft text. The draft should read exactly as a reader would read it, with all content issues documented above in CONTENT FLAGS instead.]`, advocate: `You are The Advocate — you read for the stranger. Not the writer, not the editor. The person who picked this up cold, knows nothing about the subject, and has no obligation to keep reading. You have the Style Brief. Your job is not to smooth the voice — it's to ask whether the reader has any reason to stay. Evaluate three things only: **OPENING CONTRACT** — Does the first paragraph make a promise to the reader? Does it establish: something is at stake, something is unresolved, or something surprising is happening? If not, where is the first moment that does? **STAKES AUDIT** — What does the reader stand to lose by not finishing this? What question are they waiting to see answered? Is that question ever made explicit, or is it assumed? Mark every place where stakes are implied but never surfaced. **READER ENTRY POINTS** — Places where a cold reader would lose the thread. Unexplained references. Assumed context. Moments where insider knowledge is required. These are not content errors — they are entry failures. OUTPUT FORMAT: ## OPENING CONTRACT [Is the promise made? Where? Quote the moment. If not, what's the first candidate?] ## STAKES AUDIT [Numbered. What's at risk in this piece — for whom, why it matters. Mark each: SURFACED / IMPLIED / MISSING.] ## ENTRY FAILURES [Numbered. Quote the moment. Explain what a cold reader would think. Distinguish between intentional difficulty and accidental opacity.] ## ADVOCATE VERDICT [One honest paragraph: does a stranger have reason to stay?] ## REVISED DRAFT [Full clean draft with only the entry failures and stakes surfacing addressed — do not touch voice, structure, or content beyond this scope.]`, wordsmith: `You are The Wordsmith — line editor. Your job: make the voice MORE itself, not less. You have the Style Brief. Signature moves are protected. Do not flatten them. For Gonzo/HST, check the 10 mechanics: 1. Clinical observation of chaos 2. Declarative pivot 3. Confessional aside (once) 4. Slow zoom 5. Numbers exact (never approximate) 6. Ethical stance through observation 7. Inventory pile 8. Witnessed detail 9. Jazz rhythm 10. Room as cultural document Anti-patterns to kill: Homer voice, false neutrality, abstracted subject, compressed time skip, inspirational close, passive atrocity. ## LINE FLAGS [Numbered. Quote sentence. Name mechanic. Offer alternative. Mark [PROTECTED] if shielded by Style Brief.] ## MECHANICS SCORECARD [Each of 10: DEPLOYED / WEAK / MISSING] ## RHYTHM ANALYSIS [Where it sings. Where it drags.] ## LINE VERDICT [One paragraph.] ## REVISED DRAFT [Full draft. Protected choices remain. Weaknesses addressed.]`, sentinel: `You are The Sentinel — copy editor. Last gate before publication. You have the Style Brief. CRITICAL: fragments, run-ons, unconventional caps, profanity, interrupted thoughts — these are INTENTIONAL in Gonzo writing. Do NOT correct protected choices. Mark [STYLE CHOICE] instead. Check: genuine typos, spelling errors, punctuation consistency, unintentional tense shifts, pronoun agreement, number style consistency. ## COPY FLAGS [Numbered. Quote exact text. Show: "X" → "Y". Mark [STYLE CHOICE] for intentional choices.] ## CONSISTENCY NOTES [Style choices that need standardizing across the piece.] ## REVISED DRAFT [Full corrected draft. Style choices preserved.]`, designer: `You are The Designer — format editor. Format serves the voice. You have the Style Brief. A Gonzo dispatch has different format needs than an academic essay. Match the conventions to the identified tradition. Check: headers, section breaks, walls of text, dateline presence, artifact block formatting, paragraph length variation. ## FORMAT FLAGS [Numbered. Problem and fix.] ## STRUCTURE RECOMMENDATIONS [Specific changes to document architecture.] ## REVISED DRAFT [Full draft with formatting applied. Use markdown.]`, director_final: `You are The Director — you read this piece first and wrote the grand Style Brief. You will receive TWO drafts: the raw original and the final edited version. Compare them directly. Previous versions were too nice — they let sections slip out of order, flow broke, books became hard to follow, voice got sanded flat. This time be ruthless and visionary. NON-NEGOTIABLE: PERFECT LOGICAL ORDER AND UNBREAKABLE COHERENCE. The reader must never lose the thread for even one sentence. If any section is out of order, REORDER THE ENTIRE FINAL DRAFT into flawless sequence in your output. Scan for any place the reader would have to pause, backtrack, or guess. Fix it. Prioritize strict logical section order, narrative momentum, emotional power, and total reader immersion above all. Cut or reorder anything that breaks coherence or grandeur. Deliver the version a serious reader cannot put down. ## VOICE DELTA [Compare raw to final. For each Signature Move from your Style Brief: STRENGTHENED / INTACT / WEAKENED / LOST. Quote evidence from both versions. Note any grand elevations.] ## EDITORIAL DAMAGE REPORT [What did editors flatten, over-correct, or homogenize? Quote the raw original and the final version side by side. Specifically call out any remaining out-of-order sections, confusing flow, or lost grandeur. Be specific and merciless.] ## GENUINE IMPROVEMENTS [What did the pipeline actually fix? Quote before/after. Credit the wins. Note any structural or visionary fixes that elevated it.] ## WHAT'S STILL DEAD [What the pipeline didn't fix — and why it matters for the grand vision. These are the remaining problems. Flag any order or coherence issues that remain, and what would make it legendary.] ## FINAL VERDICT [One honest, ambitious paragraph: is this better or worse than what came in? Is it ready for publication as a grand work? Was structure and flow fixed and elevated? What's the single remaining thing it needs to achieve masterpiece status?] ## FINAL DRAFT [The finished piece — restore anything incorrectly flattened, reorder for perfect logical flow and grand coherence, lock in all genuine improvements, deliver the strongest possible version that a reader can actually follow and be moved by.]` }; const VOICE_DEFS = { hst: `You write in the pure Gonzo voice — Hunter S. Thompson architecture, fully deployed. CORE EQUATION: EVENT + ECONOMIC REALITY + SPECIFIC HUMAN DETAIL + EXACT NUMBER = TRUTH 5-LAYER STRUCTURE: L1 ARRIVAL: location + time + one sensory detail. No proper nouns except place. Reader smells/hears something by sentence three. L2 INTAKE: Institution named + annual revenue. One exact financial figure. One specific human: name, age, city, what they gave up. L3 FIELD REPORT: Present tense throughout. Clock visible. Wide/close alternation. One exact number per paragraph. Inventory pile once per 500 words. One witnessed detail. L4 ARTIFACT: Dateline + number + pull quote + verdict. Works standalone. L5 CONVERSATION: Name the structural cause. Do not resolve it. Stop. 10 MECHANICS: Clinical chaos observation. Declarative pivot. Confessional aside (once). Slow zoom. Numbers as credibility. Ethical stance via observation. Inventory pile. Witnessed detail. Jazz rhythm. Room as cultural document. ANTI-PATTERNS — KILL: Homer voice. False neutrality. Abstracted subject. Compressed time skip. Inspirational close. Passive atrocity. Profanity = punctuation. CAPS = screaming. Italics = emphasis. Fragments = where the thought crashes. DO NOT SANITIZE.`, uplift: `You write in the Amarillo Uplift voice — Jesse Welles storytelling meets gonzo prose mechanics on verified Panhandle research. - Sentences alternate: short declarative → long rolling → short. Jazz, not metronome. - Delayed subjects: "In the canyon, where the red walls catch the last hour of sun and hold it like a debt, the horses appeared." - Register mixing: academic precision next to working-class plainness. - Inventory lists: specific nouns piled. Not "supplies" but "a spool of barbed wire, a Bible cracked at Ecclesiastes, and a Winchester repeater." - Warm but not sentimental. Precise but not academic. Funny when earned, never forced. - Respects the land and the people who were here first. - BANNED: tapestry, rich history, nestled, boasts, vibrant, iconic, time-honored, legendary (unless earned), hidden gem, little-known, few people know.` }; // ─── PIPELINE STATE ──────────────────────── let pipelineStages = []; // ordered list of stage ids to run let currentStageIdx = -1; let currentDraft = ''; let rawDraft = ''; let styleBrief = ''; let allDrafts = {}; let chatHistory = {}; // stageId → [{role, content}] let stageResults = {}; // stageId → full text let pipelineRunning = false; // ─── PIPELINE BAR ────────────────────────── function buildPipeBar(stages) { const bar = document.getElementById('pipebar'); bar.innerHTML = ''; stages.forEach((sid, i) => { const p = PERSONAS.find(x => x.id === sid); if (!p) return; if (i > 0) { const conn = document.createElement('div'); conn.className = 'pipe-connector'; bar.appendChild(conn); } const el = document.createElement('div'); el.className = 'pipe-stage'; el.id = 'pipe-' + sid; el.innerHTML = `
${p.icon}
${p.name}
`; bar.appendChild(el); }); } function setPipeActive(sid) { document.querySelectorAll('.pipe-stage').forEach(el => { el.classList.remove('active'); }); const el = document.getElementById('pipe-' + sid); if (el) el.classList.add('active'); } function setPipeDone(sid) { const el = document.getElementById('pipe-' + sid); if (el) { el.classList.remove('active'); el.classList.add('done'); } } // ─── EDITOR PANEL ────────────────────────── function openEditorPanel(stageId) { const p = PERSONAS.find(x => x.id === stageId); if (!p) return; document.getElementById('epIcon').textContent = p.icon; document.getElementById('epIcon').style.color = p.color; document.getElementById('epIcon').style.borderColor = p.color; document.getElementById('epName').textContent = p.name; document.getElementById('epName').style.color = p.color; document.getElementById('epRole').textContent = p.role; // style chat input border to match editor document.getElementById('epPanel_color_target')?.style; document.querySelector('.ep-chat-input textarea').style.setProperty('--editor-color', p.color); document.getElementById('editorPanel').classList.add('open'); document.getElementById('draftPanel').classList.add('shifted'); // init chat history if (!chatHistory[stageId]) chatHistory[stageId] = []; renderChat(stageId); switchTab('report'); } function closeEditorPanel() { document.getElementById('editorPanel').classList.remove('open'); document.getElementById('draftPanel').classList.remove('shifted'); } function switchTab(tab) { document.getElementById('tabReport').classList.toggle('active', tab === 'report'); document.getElementById('tabChat').classList.toggle('active', tab === 'chat'); document.getElementById('paneReport').classList.toggle('active', tab === 'report'); document.getElementById('paneChat').classList.toggle('active', tab === 'chat'); if (tab === 'chat') document.getElementById('chatMessages').scrollTop = 9999; } // ─── CHAT ────────────────────────────────── function renderChat(stageId) { const msgs = chatHistory[stageId] || []; const container = document.getElementById('chatMessages'); container.innerHTML = ''; if (msgs.length === 0) { container.innerHTML = '
Ask questions, push back on flags,
defend your choices.
'; return; } msgs.forEach(m => { const isUser = m.role === 'user'; const el = document.createElement('div'); el.className = 'chat-msg ' + (isUser ? 'user' : 'editor'); const p = PERSONAS.find(x => x.id === currentStageId); el.innerHTML = `
${isUser ? 'You' : (p?.name || 'Editor')}
${escHtml(m.content)}
`; container.appendChild(el); }); container.scrollTop = container.scrollHeight; } function escHtml(s) { return s.replace(/&/g,'&').replace(//g,'>'); } function chatKeydown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } } let currentStageId = ''; async function sendChat() { const inp = document.getElementById('chatInput'); const msg = inp.value.trim(); if (!msg || !currentStageId) return; inp.value = ''; if (!chatHistory[currentStageId]) chatHistory[currentStageId] = []; chatHistory[currentStageId].push({role:'user', content: msg}); renderChat(currentStageId); const p = PERSONAS.find(x => x.id === currentStageId); const stageResult = stageResults[currentStageId] || ''; const brief = styleBrief; const sysPrompt = `You are ${p.name} — ${p.role}. You just reviewed a draft and produced an editorial report. The writer is now discussing your feedback with you. Your editorial report was: ${stageResult.slice(0, 3000)} ${brief ? `\nStyle Brief context:\n${brief.slice(0,1000)}` : ''} Engage directly with the writer's question or pushback. Be specific. If they're defending a choice, evaluate whether their argument holds. If they're asking why you flagged something, explain precisely. You are a collaborator, not an authority.`; const messages = chatHistory[currentStageId].map(m => ({role: m.role === 'user' ? 'user' : 'assistant', content: m.content})); try { const responseText = await streamToElement(sysPrompt, messages, 'paneChat', true); chatHistory[currentStageId].push({role:'assistant', content: responseText}); renderChat(currentStageId); } catch(e) { toast('Chat error: ' + e.message.slice(0,60)); } } // ─── CLAUDE API ──────────────────────────── async function callClaude(system, messages, onToken) { const body = { model: 'claude-opus-4-6', max_tokens: 8192, stream: true, system, messages }; const resp = await fetch('/thetool-api', { method: 'POST', headers: {'Content-Type': 'application/json', 'X-Thetool-Key': 'itk-gw-4f8a9c2e7b3d'}, body: JSON.stringify(body) }); if (!resp.ok) throw new Error('API ' + resp.status); let full = ''; const reader = resp.body.getReader(); const dec = new TextDecoder(); let buf = ''; while (true) { const {done, value} = await reader.read(); if (done) break; buf += dec.decode(value, {stream:true}); const lines = buf.split('\n'); buf = lines.pop(); for (const line of lines) { if (!line.startsWith('data: ')) continue; const d = line.slice(6).trim(); if (d === '[DONE]') continue; try { const ev = JSON.parse(d); if (ev.type === 'content_block_delta' && ev.delta?.type === 'text_delta') { full += ev.delta.text; onToken(ev.delta.text, full); } } catch {} } } return full; } async function streamToElement(system, messages, targetId, isChat) { if (isChat) { // stream to a temp element then render let acc = ''; const tmp = document.createElement('div'); tmp.className = 'chat-msg editor'; const from = document.createElement('div'); from.className = 'chat-from'; from.textContent = PERSONAS.find(x=>x.id===currentStageId)?.name || 'Editor'; const bubble = document.createElement('div'); bubble.className = 'chat-bubble typing'; tmp.appendChild(from); tmp.appendChild(bubble); document.getElementById('chatMessages').appendChild(tmp); document.getElementById('chatMessages').scrollTop = 9999; const result = await callClaude(system, messages, (tok, full) => { bubble.textContent = full; bubble.classList.add('typing'); document.getElementById('chatMessages').scrollTop = 9999; }); bubble.classList.remove('typing'); tmp.remove(); // will be re-rendered by renderChat return result; } } // ─── EXTRACT HELPERS ─────────────────────── function extractSection(text, ...markers) { for (const m of markers) { const idx = text.indexOf(m); if (idx === -1) continue; const after = text.slice(idx + m.length); const next = after.search(/\n## /); const section = next === -1 ? after : after.slice(0, next); if (section.trim().length > 200) return section.trim(); } // Fallback for known errors where LLM varies headers: take the largest trailing prose block const lines = text.split('\n'); let best = ''; let current = ''; for (let i = lines.length-1; i>=0; i--) { if (lines[i].trim().startsWith('## ')) { if (current.trim().length > best.length) best = current; current = ''; } else { current = lines[i] + '\n' + current; } } if (current.trim().length > best.length) best = current; if (best.trim().length > 200) return best.trim(); return null; } function extractStyleBrief(text) { return extractSection(text, '## STYLE BRIEF FOR DOWNSTREAM EDITORS\n', '## STYLE BRIEF FOR DOWNSTREAM EDITORS' ) || ''; } function extractDraft(text) { return extractSection(text, '## REVISED DRAFT\n', '## REVISED DRAFT:', '## FINAL DRAFT\n', '## FINAL DRAFT:', '## PREPARED DRAFT\n', '## PREPARED DRAFT:' ); } // Simple heuristic to catch known order/flow errors function validateLogicalOrder(draft) { if (!draft || draft.trim().length < 150) return { ok: true, issues: [] }; const lower = draft.toLowerCase(); const issues = []; // Conclusion or resolution before any real setup const conclusionEarly = /conclusion|resolution|the end|epilogue/.test(lower.split(/setup|beginning|introduction|chapter 1|the start/)[0] || ''); if (conclusionEarly) issues.push('Conclusion/resolution appears before setup'); // Climax before setup const climaxEarly = /climax|final battle|turning point/.test(lower.split(/setup|beginning|introduction|chapter 1/)[0] || ''); if (climaxEarly) issues.push('Climax appears before setup'); // Chapter numbers decreasing const chapterMatches = [...lower.matchAll(/chapter\s+(\d+)/g)]; if (chapterMatches.length > 1) { let last = 0; for (const m of chapterMatches) { const num = parseInt(m[1]); if (num < last) { issues.push('Chapters out of numeric sequence'); break; } last = num; } } // Abrupt topic jumps (heuristic) const sentences = draft.split(/[.!?]+/).filter(s => s.trim().length > 15); let jumps = 0; for (let i = 1; i < Math.min(sentences.length, 20); i++) { const p = sentences[i-1].toLowerCase(), c = sentences[i].toLowerCase(); if ((p.includes('battle') || p.includes('dragon')) && (c.includes('village') || c.includes('daily life'))) jumps++; if ((p.includes('king') || p.includes('throne')) && (c.includes('modern') || c.includes('today'))) jumps++; } if (jumps >= 2) issues.push('Multiple unexplained topic jumps that break the reader thread'); return { ok: issues.length === 0, issues }; } async function enforceCoherence(draft, brief, prior) { // Dedicated pass to catch order/flow errors that slip through const reorderSys = `You are a strict narrative flow enforcer. You receive a draft that may have sections out of logical order or broken reader thread. ${brief ? 'STYLE BRIEF: ' + brief.slice(0,800) : ''} ${prior ? 'PRIOR INSTRUCTIONS: ' + prior : ''} Your ONLY job: Reorder the content into PERFECT logical sequence so a first-time reader never loses the thread. - Identify natural beginning/setup first. - Then rising action/build. - Climax. - Resolution/conclusion last. - Preserve all content and voice exactly. - Output ONLY the clean full reordered draft. No commentary. Start immediately with prose.`; const reorderMsg = `Reorder this draft for perfect logical flow and coherence:\n\n${draft}`; let reordered = ''; try { reordered = await callClaude(reorderSys, [{role:'user', content: reorderMsg}], (tok, full) => { reordered = full; }); } catch(e) { return draft; // fallback } return reordered || draft; } // ─── FORMAT REPORT ───────────────────────── function formatReport(text) { // Add visual styling to section headers and flags return text .replace(/^## (.+)$/gm, '
$1
') .replace(/\[PROTECTED[^\]]*\]/g, '$&') .replace(/\[STYLE CHOICE[^\]]*\]/g, '$&') .replace(/\[CONTENT FLAG[^\]]*\]/g, '$&') .replace(/\[STYLE CONFLICT[^\]]*\]/g, '$&'); } // ─── RUN STAGE ───────────────────────────── async function runStage(stageId, draft, brief, prior) { currentStageId = stageId; setPipeActive(stageId); const p = PERSONAS.find(x => x.id === stageId); const sys = SYSTEMS[stageId]; const priorBlock = prior ? `\nWRITER'S PRIOR INSTRUCTIONS (protect these):\n${prior}\n` : ''; const briefBlock = brief ? `\nSTYLE BRIEF:\n${brief}\n` : ''; const fullSys = sys + priorBlock + briefBlock; let userMsg; if (stageId === 'director_brief') { userMsg = `Read this draft cold and produce your Style Brief:\n\n${draft}`; } else if (stageId === 'director_final') { userMsg = `RAW ORIGINAL DRAFT (what came in before editing):\n\n${rawDraft || draft}\n\n${'─'.repeat(60)}\n\nFINAL EDITED DRAFT (after all editors):\n\n${draft}\n\nYour Style Brief is attached. Perform your final integrity check now — compare both drafts directly.`; } else { userMsg = `Draft to edit:\n\n${draft}\n\nApply your full editorial pass now.`; } const reportEl = document.getElementById('reportText'); reportEl.innerHTML = ''; reportEl.classList.add('typing'); // open panel openEditorPanel(stageId); let fullText = ''; await callClaude(fullSys, [{role:'user', content: userMsg}], (tok, full) => { fullText = full; reportEl.innerHTML = formatReport(full); reportEl.scrollTop = reportEl.scrollHeight; }); reportEl.classList.remove('typing'); stageResults[stageId] = fullText; allDrafts[stageId] = fullText; // update stats if draft changed const newDraft = extractDraft(fullText); if (newDraft) currentDraft = newDraft; if (stageId === 'director_brief') { styleBrief = extractStyleBrief(fullText); } if (stageId === 'architect') { const priorVal = document.getElementById('priorText') ? document.getElementById('priorText').value.trim() : ''; currentDraft = await enforceCoherence(currentDraft || draft, brief, priorVal); allDrafts[stageId + '_enforced'] = currentDraft; // Catch known order errors that still slipped const orderCheck = validateLogicalOrder(currentDraft); if (!orderCheck.ok) { console.warn('[The Tool] Order issues after architect:', orderCheck.issues); // Force one more pass currentDraft = await enforceCoherence(currentDraft, brief, priorVal); } } setPipeDone(stageId); return fullText; } // ─── PROCEED TO NEXT ─────────────────────── async function proceedToNext() { // update draft from textarea before proceeding currentDraft = document.getElementById('draftText').value || currentDraft; // Re-validate on user edits (catches known errors re-introduced mid-pipeline) const midCheck = validateLogicalOrder(currentDraft); if (!midCheck.ok) { console.warn('[The Tool] Order issues after user edit:', midCheck.issues); const priorVal = document.getElementById('priorText').value.trim(); currentDraft = await enforceCoherence(currentDraft, styleBrief, priorVal); } closeEditorPanel(); currentStageIdx++; if (currentStageIdx >= pipelineStages.length) { // pipeline complete showFinal(); return; } const nextId = pipelineStages[currentStageIdx]; const prior = document.getElementById('priorText').value.trim(); try { if (nextId === 'director_final') { const priorVal = document.getElementById('priorText').value.trim(); currentDraft = await enforceCoherence(currentDraft, styleBrief, priorVal); // Extra check for known errors right before final const orderCheck = validateLogicalOrder(currentDraft); if (!orderCheck.ok) { console.warn('[The Tool] Order issues before final director:', orderCheck.issues); currentDraft = await enforceCoherence(currentDraft, styleBrief, priorVal); } } await runStage(nextId, currentDraft, styleBrief, prior); // update draft textarea with extracted draft if (currentDraft) document.getElementById('draftText').value = currentDraft; updateStats(currentDraft); } catch(e) { toast('Error: ' + e.message.slice(0,80)); pipelineRunning = false; } } // ─── START PIPELINE (DESK MODE) ──────────── async function startPipeline() { const draft = document.getElementById('draftText').value.trim(); if (!draft) { toast('Add your draft first.'); return; } if (pipelineRunning) return; const draftWordCount = draft.trim().split(/\s+/).length; if (!checkWordBudget(draftWordCount)) return; pipelineRunning = true; currentDraft = draft; rawDraft = draft; allDrafts = {}; stageResults = {}; chatHistory = {}; styleBrief = ''; recordWords(draftWordCount); pipelineStages = ['director_brief','architect','researcher','advocate','wordsmith','sentinel','designer','director_final']; buildPipeBar(pipelineStages); currentStageIdx = 0; const prior = document.getElementById('priorText').value.trim(); document.getElementById('startBtn').disabled = true; document.getElementById('nextEditorBtn').textContent = 'Next Editor →'; try { const sid = pipelineStages[0]; await runStage(sid, currentDraft, '', prior); if (currentDraft) document.getElementById('draftText').value = currentDraft; updateStats(currentDraft); } catch(e) { toast('Error: ' + e.message.slice(0,80)); pipelineRunning = false; } document.getElementById('startBtn').disabled = false; } // ─── THE FORGE (WRITE FROM SEED) ─────────── async function runForge() { const seed = document.getElementById('seedInput').value.trim(); if (!seed) { toast('Add a seed.'); return; } const wordTarget = parseInt(document.getElementById('wordTarget').value) || 2000; const voice = forgeVoice === 'custom' ? document.getElementById('customVoice').value : VOICE_DEFS[forgeVoice] || VOICE_DEFS.hst; const prior = document.getElementById('forgePrior').value.trim(); const priorBlock = prior ? `\nWRITER'S PRIOR INSTRUCTIONS:\n${prior}\n` : ''; if (!checkWordBudget(wordTarget)) return; document.getElementById('forgeRunBtn').disabled = true; setMode('desk'); allDrafts = {}; stageResults = {}; chatHistory = {}; styleBrief = ''; recordWords(wordTarget); pipelineStages = ['director_brief','architect','researcher','advocate','wordsmith','sentinel','designer','director_final']; buildPipeBar(['outline_gen','draft_write',...pipelineStages]); // show draft panel document.getElementById('draftTitle').textContent = 'Generating...'; document.getElementById('draftText').value = ''; document.getElementById('draftText').placeholder = ''; try { // Step 1: Outline setPipeActive('outline_gen'); const outlineSys = `You are a master narrative nonfiction outline architect.\n\n${voice}\n\nProduce a detailed scene-by-scene outline for approximately ${wordTarget} words. Working title, lede concept, section-by-section breakdown (heading, what happens, key facts/scenes/characters, approx word count per section), closing concept, total estimated word count. Be specific — name the exact scene, the exact quote, the exact fact.${priorBlock}`; let outline = ''; document.getElementById('draftText').value = 'Building outline...\n\n'; await callClaude(outlineSys, [{role:'user', content:`SEED: ${seed}\nTARGET: approximately ${wordTarget} words\n\nGenerate the full outline now.`}], (tok, full) => { outline = full; document.getElementById('draftText').value = '// OUTLINE\n\n' + full; }); allDrafts['outline'] = outline; setPipeDone('outline_gen'); // Step 2: Draft setPipeActive('draft_write'); const draftSys = `You are a master narrative nonfiction writer.\n\n${voice}\n\nWrite the FULL DRAFT from the outline. Do not summarize. Do not skip sections. Every scene, every paragraph, every sentence. Complete and fully realized.${priorBlock}`; let draft = ''; document.getElementById('draftText').value = 'Writing draft...\n\n'; await callClaude(draftSys, [{role:'user', content:`OUTLINE:\n${outline}\n\nWrite the complete draft now. All sections. Full prose. No placeholders.`}], (tok, full) => { draft = full; document.getElementById('draftText').value = full; document.getElementById('draftText').scrollTop = document.getElementById('draftText').scrollHeight; }); allDrafts['raw_draft'] = draft; currentDraft = draft; rawDraft = draft; setPipeDone('draft_write'); document.getElementById('draftTitle').textContent = 'Draft — ready for editorial pipeline'; updateStats(draft); // Now run full editorial pipeline pipelineRunning = true; currentStageIdx = 0; const sid = pipelineStages[0]; await runStage(sid, currentDraft, '', prior); updateStats(currentDraft); } catch(e) { toast('Error: ' + e.message.slice(0,80)); } document.getElementById('forgeRunBtn').disabled = false; } // ─── FINAL ───────────────────────────────── function showFinal() { pipelineRunning = false; const finalResult = stageResults['director_final'] || ''; const finalDraft = extractDraft(finalResult) || currentDraft; allDrafts['FINAL'] = finalDraft; document.getElementById('finalText').textContent = finalDraft; document.getElementById('finalOverlay').classList.add('show'); closeEditorPanel(); toast('Pipeline complete.'); } function copyFinal() { navigator.clipboard.writeText(document.getElementById('finalText').textContent) .then(() => toast('Copied to clipboard.')); } function downloadAll() { let bundle = '═'.repeat(60) + '\nINKWELL — ALL DRAFTS\n' + '═'.repeat(60) + '\n\n'; for (const [name, content] of Object.entries(allDrafts)) { bundle += '═'.repeat(60) + '\n' + name.toUpperCase().replace(/_/g,' ') + '\n' + '═'.repeat(60) + '\n\n' + content + '\n\n'; } const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([bundle], {type:'text/plain'})); a.download = 'inkwell_' + Date.now() + '.txt'; a.click(); } // ─── TOAST ───────────────────────────────── let toastTimer; function toast(msg) { const el = document.getElementById('toast'); el.textContent = msg; el.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => el.classList.remove('show'), 3000); } // ─── INIT ────────────────────────────────── buildPipeBar([]);
Legal