HoldFree Tax™
Instant IRS Answers. No Hold Music.
BYO API: Not connected

IRS Notice Decoder

Pick a notice and paste text (optional). Get steps + citations in seconds.

Client-Friendly Mode
Rewrite outputs for client emails
Result
No result yet
Ready
Choose a notice and click Decode Notice to preview a professional response.
/* ========================= 2) styles.css ========================= */ :root{ --bg:#0b1220; --panel:#0f1a30; --card:#101e38; --border:rgba(255,255,255,.10); --text:rgba(255,255,255,.92); --muted:rgba(255,255,255,.68); --muted2:rgba(255,255,255,.55); --accent:#6ea8ff; --accent2:#97ffda; --danger:#ff6e8a; --ok:#66e3a6; --shadow: 0 10px 30px rgba(0,0,0,.35); --radius:16px; --radius2:22px; --font: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; } *{box-sizing:border-box} body{ margin:0; font-family:var(--font); background: radial-gradient(1200px 800px at 20% 0%, rgba(110,168,255,.14), transparent 50%), radial-gradient(1000px 700px at 90% 30%, rgba(151,255,218,.10), transparent 50%), var(--bg); color:var(--text); } .topbar{ position:sticky; top:0; display:flex; align-items:center; justify-content:space-between; padding:16px 18px; background: rgba(11,18,32,.78); backdrop-filter: blur(10px); border-bottom:1px solid var(--border); z-index:10; } .brand{display:flex; gap:12px; align-items:center} .logo{ width:44px; height:44px; border-radius:14px; display:grid; place-items:center; background: linear-gradient(135deg, rgba(110,168,255,.35), rgba(151,255,218,.18)); border:1px solid var(--border); box-shadow: var(--shadow); font-weight:800; } .brand-name{font-weight:800; letter-spacing:.2px} .brand-tag{font-size:12px; color:var(--muted2); margin-top:2px} .top-actions{display:flex; gap:10px; align-items:center} .pill{ font-size:12px; padding:8px 10px; border-radius:999px; border:1px solid var(--border); background: rgba(255,255,255,.04); color:var(--muted); } .btn{ padding:10px 12px; border-radius:12px; border:1px solid rgba(110,168,255,.35); background: rgba(110,168,255,.18); color:var(--text); cursor:pointer; transition:.15s ease; font-weight:650; } .btn:hover{transform: translateY(-1px)} .btn:active{transform: translateY(0px)} .btn-ghost{ border:1px solid var(--border); background: rgba(255,255,255,.04); } .icon-btn{ border:1px solid var(--border); background: rgba(255,255,255,.04); color:var(--text); border-radius:12px; padding:8px 10px; cursor:pointer; } .layout{ display:grid; grid-template-columns: 280px 1fr; min-height: calc(100vh - 76px); } .sidebar{ padding:14px; border-right:1px solid var(--border); background: rgba(15,26,48,.35); } .nav-item{ width:100%; padding:12px 12px; border-radius:14px; border:1px solid var(--border); background: rgba(255,255,255,.03); cursor:pointer; text-align:left; color:var(--text); display:flex; flex-direction:column; gap:4px; margin-bottom:10px; } .nav-item.active{ border-color: rgba(110,168,255,.55); background: rgba(110,168,255,.10); } .nav-title{font-weight:800} .nav-sub{font-size:12px; color:var(--muted)} .sidebar-footer{margin-top:14px} .content{padding:18px} .page-title h1{margin:0; font-size:24px} .page-title p{margin:8px 0 18px; color:var(--muted)} .card{ background: rgba(16,30,56,.55); border:1px solid var(--border); border-radius: var(--radius2); box-shadow: var(--shadow); padding:16px; } .grid-2{ display:grid; grid-template-columns: 1fr 1fr; gap:14px; } .label{ display:block; font-size:12px; color:var(--muted); margin-bottom:6px; } .input{ width:100%; padding:11px 12px; border-radius:14px; border:1px solid var(--border); background: rgba(255,255,255,.04); color:var(--text); outline:none; } .input:focus{ border-color: rgba(110,168,255,.55); box-shadow: 0 0 0 3px rgba(110,168,255,.12); } .textarea{min-height:120px; resize:vertical} .row{display:flex; gap:10px; align-items:center} .space-between{justify-content:space-between} .mt{margin-top:10px} .divider{height:1px; background: var(--border); margin:14px 0} .result-head{ display:flex; align-items:flex-start; justify-content:space-between; gap:10px; margin-bottom:10px; } .result-title{font-weight:900} .result-sub{font-size:12px; color:var(--muted)} .badge{ font-size:12px; padding:6px 10px; border-radius:999px; border:1px solid var(--border); background: rgba(255,255,255,.04); color:var(--muted); } .result{ border-radius:16px; border:1px solid var(--border); background: rgba(0,0,0,.18); padding:12px; min-height:240px; overflow:auto; } .muted{color:var(--muted)} .switch{position:relative; width:42px; height:24px} .switch input{display:none} .slider{ position:absolute; inset:0; border-radius:999px; background: rgba(255,255,255,.10); border:1px solid var(--border); cursor:pointer; transition:.15s ease; } .slider::after{ content:""; position:absolute; width:18px; height:18px; border-radius:999px; background: rgba(255,255,255,.75); left:3px; top:2px; transition:.15s ease; } .switch input:checked + .slider{ background: rgba(110,168,255,.22); border-color: rgba(110,168,255,.55); } .switch input:checked + .slider::after{left:21px; background: rgba(151,255,218,.85)} .switch-title{font-weight:800} .switch-sub{font-size:12px; color:var(--muted)} .view.hidden{display:none} .saved-list{display:flex; flex-direction:column; gap:10px} .saved-item{ border:1px solid var(--border); background: rgba(255,255,255,.03); border-radius:16px; padding:12px; } .saved-meta{font-size:12px; color:var(--muted); margin-top:6px} .small-btn{ padding:8px 10px; border-radius:12px; border:1px solid var(--border); background: rgba(255,255,255,.04); color:var(--text); cursor:pointer; } .footer-note{margin-top:14px; font-size:12px; color:var(--muted2)} /* MODAL */ .modal{ position:fixed; inset:0; background: rgba(0,0,0,.55); display:grid; place-items:center; padding:18px; z-index:50; } .modal-card{ width:min(680px, 96vw); background: rgba(16,30,56,.92); border:1px solid var(--border); border-radius:22px; box-shadow: var(--shadow); padding:16px; } .modal-head{display:flex; justify-content:space-between; gap:12px; align-items:flex-start} .modal-title{font-weight:900; font-size:18px} .modal-sub{font-size:12px; color:var(--muted); margin-top:4px} .toast{ margin-top:12px; padding:10px 12px; border-radius:14px; border:1px solid var(--border); background: rgba(255,255,255,.04); color:var(--muted); } .toast.ok{border-color: rgba(102,227,166,.35); background: rgba(102,227,166,.12); color: rgba(255,255,255,.92)} .toast.bad{border-color: rgba(255,110,138,.35); background: rgba(255,110,138,.12); color: rgba(255,255,255,.92)} @media (max-width: 980px){ .layout{grid-template-columns: 1fr} .sidebar{border-right:none; border-bottom:1px solid var(--border)} .grid-2{grid-template-columns: 1fr} } /* ========================= 3) app.js ========================= */ // HoldFree Tax™ Demo — dummy data + localStorage const $ = (id) => document.getElementById(id); const STORAGE = { api: "hf_api_settings", saved: "hf_saved_answers", lastResult: "hf_last_result" }; const demoNotices = { CP2000: { title: "CP2000 — Proposed Changes to Income/Deductions", lastReviewed: "2025-12-13", citations: [ "IRS CP2000 notice guidance (IRS notices/letters overview)", "Pub 556 — Examination of Returns, Appeal Rights, and Claims for Refund", "IRC §6212 / §6213 (statutory notice & response timing concepts)" ], pro: ` What it means
CP2000 is a proposed change notice—often triggered by a mismatch between what the IRS received (W-2/1099, brokerage reporting, etc.) and what was reported on the return.

Why the IRS sent it
Most commonly: unreported income, basis not reported/recognized, missing Schedule D details, or mismatch on credits/deductions.

What NOT to do
• Don’t ignore the response deadline.
• Don’t sign/agree if figures are wrong or incomplete (especially cost basis).
• Don’t respond without supporting documents and a clear reconciliation.

Recommended next steps (practitioner workflow)
1) Compare CP2000 line items to the filed return and source docs.
2) Identify mismatch category: income omission vs basis/adjustment vs credit issue.
3) Prepare reconciliation schedule + attach supporting statements (brokerage, basis worksheets, corrected 1099, etc.).
4) Respond by mail/fax per notice instructions; keep proof of submission.
5) If correct and client agrees, sign and return with payment/arrangement plan info.

Typical timeline expectations
Responses can take several weeks to months to post/process depending on season volume.

Citations
${renderCitationsHtml("CP2000")} `, client: ` What it means
This letter is the IRS saying: “We received information that doesn’t match the tax return.” It’s not final yet—it's a proposed change.

What we’ll do
We’ll compare the letter to your tax return and your tax documents (W-2s/1099s/brokerage statements). If the IRS numbers are wrong (like missing cost basis), we’ll send them the proof and a clear explanation.

Important
We need to respond by the deadline on the letter so the IRS doesn’t finalize the changes automatically.

Sources
${renderCitationsHtml("CP2000")} ` }, CP14: { title: "CP14 — Balance Due Notice", lastReviewed: "2025-12-13", citations: [ "IRS notice CP14 explanation (balance due)", "Pub 594 — The IRS Collection Process", "IRS Installment Agreement options (payment plans)" ], pro: ` What it means
CP14 indicates the IRS assessed a balance due (tax, penalties, interest).

Recommended next steps
1) Verify assessment and tax year. Confirm whether return was processed correctly.
2) If correct: pay in full OR set up an installment agreement (IA).
3) If incorrect: identify the source (processing error, missing payment credit, amended return not applied) and respond with documentation.

What NOT to do
• Don’t delay if a payment plan is needed—interest/penalties accrue.

Citations
${renderCitationsHtml("CP14")} `, client: ` What it means
This is a balance due letter. The IRS believes there is still tax owed for that year (plus penalties/interest).

What we’ll do
We’ll confirm the amount and why it happened. If it’s accurate, we can pay it or set up a payment plan. If it’s not accurate, we’ll show the IRS the proof (like a missing payment credit).

Sources
${renderCitationsHtml("CP14")} ` }, "12C": { title: "Letter 12C — Missing Information Request", lastReviewed: "2025-12-13", citations: [ "IRS correspondence audits overview (missing info requests)", "Pub 552 — Recordkeeping for Individuals", "Form 8962 / 1095-A guidance (if applicable)" ], pro: ` What it means
12C is commonly sent when the IRS needs missing forms/verification to finish processing. Often tied to credits/withholding verification, identity/documentation, or marketplace reconciliation (Form 8962 / 1095-A).

Recommended next steps
1) Identify exactly what the letter requests (forms, schedules, proof).
2) Send only what is requested + a brief cover letter referencing SSN/tax year and notice ID.
3) Keep copies and proof of submission.

Common gotchas
• Marketplace: missing 8962 or mismatched 1095-A figures will stall refunds.

Citations
${renderCitationsHtml("12C")} `, client: ` What it means
The IRS needs one or more documents before they can finish processing your return (this can delay refunds).

What we’ll do
We’ll send exactly what they asked for with a short letter so they can finish your return faster.

Sources
${renderCitationsHtml("12C")} ` }, "4883C": { title: "Letter 4883C — Identity Verification", lastReviewed: "2025-12-13", citations: [ "IRS Identity Verification process", "IRS letters 4883C/5071C overview" ], pro: ` What it means
4883C is an identity verification letter. The IRS needs to confirm the taxpayer’s identity before processing the return/refund.

Recommended next steps
1) Follow the letter instructions to complete verification (phone or online, depending on letter).
2) Have prior-year return info available (AGI, filing status) and current-year return copy.
3) After verification, processing may still take several weeks.

What NOT to do
• Don’t file a second return unless instructed; it can create more delays/conflicts.

Citations
${renderCitationsHtml("4883C")} `, client: ` What it means
The IRS is verifying your identity to protect you from fraud. Once verification is completed, they can continue processing your return.

What we’ll do
We’ll help you complete the verification steps and then monitor for the return to finish processing.

Sources
${renderCitationsHtml("4883C")} ` }, CP504: { title: "CP504 — Notice of Intent to Levy (State Refund)", lastReviewed: "2025-12-13", citations: [ "Pub 594 — The IRS Collection Process", "IRS levy notices and options", "Taxpayer Bill of Rights (overview)" ], pro: ` What it means
CP504 is a serious collection notice. The IRS may seize (levy) a state tax refund and may file a Notice of Federal Tax Lien depending on the case.

Recommended next steps
1) Confirm the tax year and balance due accuracy.
2) If valid: pay, request an installment agreement, or explore resolution options quickly.
3) If invalid: respond with documentation and request account correction.

Citations
${renderCitationsHtml("CP504")} `, client: ` What it means
This is a more urgent letter about an unpaid balance. The IRS may take your state refund if the balance isn’t resolved.

What we’ll do
We’ll verify the balance and take the fastest path—pay, set a payment plan, or correct the account if the IRS is wrong.

Sources
${renderCitationsHtml("CP504")} ` } }; const demoQA = [ { keywords: ["12c", "1095", "8962", "ptc", "marketplace"], lastReviewed: "2025-12-13", citations: ["Form 8962 instructions", "Pub 974 — Premium Tax Credit", "IRS 1095-A / Marketplace reconciliation guidance"], pro: ` Conservative guidance
A 12C tied to marketplace coverage usually means the IRS cannot finalize the return without Form 8962 and the 1095-A figures. Prepare Form 8962 to reconcile APTC, verify the 1095-A (Part III), and send the requested items only, with a brief cover letter referencing the notice and tax year.

Practitioner steps
1) Obtain the 1095-A (ensure correct SLCSP amounts).
2) Complete Form 8962 reconciliation; confirm any excess APTC repayment limits where applicable.
3) Respond per 12C instructions with Form 8962 + copies of 1095-A (if requested) and cover letter.

Citations
${renderInlineCitations(["Form 8962 instructions","Pub 974 — Premium Tax Credit","IRS 1095-A / Marketplace reconciliation guidance"])} `, client: ` The IRS is asking for health marketplace paperwork so they can finish your return. We’ll complete the required reconciliation form and send the exact documents they requested, which should allow them to continue processing your return.

Sources
${renderInlineCitations(["Form 8962 instructions","Pub 974 — Premium Tax Credit","IRS 1095-A / Marketplace reconciliation guidance"])} ` }, { keywords: ["cp2000", "basis", "1099-b", "brokerage", "schedule d"], lastReviewed: "2025-12-13", citations: ["Pub 550 — Investment Income and Expenses", "Schedule D instructions", "CP2000 notice guidance"], pro: ` Conservative guidance
Many CP2000 brokerage mismatches are cost basis issues (IRS sees proceeds; basis may be missing/incorrect). Reconstruct basis, ensure Schedule D/Form 8949 matches broker reporting, and respond with a reconciliation schedule and supporting statements.

Citations
${renderInlineCitations(["Pub 550 — Investment Income and Expenses","Schedule D instructions","CP2000 notice guidance"])} `, client: ` The IRS letter often happens when they have the sale proceeds but not the purchase cost (basis). We’ll calculate the correct numbers and send the IRS proof so they don’t overcharge you.

Sources
${renderInlineCitations(["Pub 550 — Investment Income and Expenses","Schedule D instructions","CP2000 notice guidance"])} ` }, { keywords: ["installment", "payment plan", "cp14", "balance due"], lastReviewed: "2025-12-13", citations: ["Pub 594 — The IRS Collection Process", "IRS Installment Agreement options"], pro: ` Conservative guidance
If CP14 is accurate and the taxpayer cannot pay in full, an installment agreement is typically the fastest route to avoid escalations. Confirm the assessment is correct first (payments applied, amended return status).

Citations
${renderInlineCitations(["Pub 594 — The IRS Collection Process","IRS Installment Agreement options"])} `, client: ` If the amount is correct, we can set up a payment plan so you don’t have to pay it all at once. First we’ll confirm the IRS balance is accurate.

Sources
${renderInlineCitations(["Pub 594 — The IRS Collection Process","IRS Installment Agreement options"])} ` } ]; let currentView = "notice"; let currentResult = null; // {type, title, contentHtml, meta} function init() { wireNav(); wireNotice(); wireQA(); wireSettings(); wireSave(); renderSaved(); refreshApiStatusPill(); restoreSettingsFields(); } function wireNav() { document.querySelectorAll(".nav-item").forEach(btn => { btn.addEventListener("click", () => { document.querySelectorAll(".nav-item").forEach(b => b.classList.remove("active")); btn.classList.add("active"); currentView = btn.dataset.view; showView(currentView); if (currentView === "saved") renderSaved(); }); }); } function showView(view) { ["notice","qa","saved"].forEach(v => { $("view-" + v).classList.toggle("hidden", v !== view); }); } function wireNotice() { $("btnLoadSampleNotice").addEventListener("click", () => { const code = $("noticeSelect").value; $("noticeText").value = getSampleNoticeText(code); }); $("btnDecode").addEventListener("click", () => { const code = $("noticeSelect").value; const clientMode = $("clientModeNotice").checked; const data = demoNotices[code]; const meta = `${code} • Last reviewed ${data.lastReviewed}`; $("resultMetaNotice").textContent = meta; $("resultBadgeNotice").textContent = "Generated"; $("resultBadgeNotice").style.borderColor = "rgba(102,227,166,.35)"; $("resultBadgeNotice").style.background = "rgba(102,227,166,.12)"; const html = clientMode ? data.client : data.pro; $("noticeResult").innerHTML = wrapResultHtml(data.title, html, data.lastReviewed); currentResult = { type: "Notice Decode", title: `${code} — ${data.title}`, contentHtml: $("noticeResult").innerHTML, meta }; persistLastResult(); }); } function wireQA() { $("btnLoadSampleQA").addEventListener("click", () => { $("qaInput").value = "Client received Letter 12C asking for 1095-A / Form 8962. They had Marketplace coverage and APTC. What should we send and what risks should we watch?"; }); $("btnAsk").addEventListener("click", () => { const q = ($("qaInput").value || "").trim(); const clientMode = $("clientModeQA").checked; const strict = $("strictCitations").checked; if (!q) { $("qaResult").innerHTML = `
Type a question first.
`; return; } const match = findBestQA(q); const meta = `Demo answer • Last reviewed ${match?.lastReviewed || "2025-12-13"}`; $("resultMetaQA").textContent = meta; if (strict && !match) { $("resultBadgeQA").textContent = "Refused"; $("resultBadgeQA").style.borderColor = "rgba(255,110,138,.35)"; $("resultBadgeQA").style.background = "rgba(255,110,138,.12)"; $("qaResult").innerHTML = ` Strict mode: I can’t answer confidently without citations in this demo.

Try asking about: CP2000 basis, CP14 payment plan, or 12C/1095-A. `; currentResult = { type: "Tax Q&A", title: `Strict mode refusal`, contentHtml: $("qaResult").innerHTML, meta }; persistLastResult(); return; } $("resultBadgeQA").textContent = "Generated"; $("resultBadgeQA").style.borderColor = "rgba(102,227,166,.35)"; $("resultBadgeQA").style.background = "rgba(102,227,166,.12)"; const answerHtml = match ? (clientMode ? match.client : match.pro) : (clientMode ? genericClientAnswer(q) : genericProAnswer(q)); $("qaResult").innerHTML = wrapResultHtml("Tax Q&A", answerHtml, match?.lastReviewed || "2025-12-13"); currentResult = { type: "Tax Q&A", title: `Q: ${q.slice(0, 80)}${q.length > 80 ? "…" : ""}`, contentHtml: $("qaResult").innerHTML, meta }; persistLastResult(); }); } function findBestQA(question) { const q = question.toLowerCase(); let best = null; let bestScore = 0; for (const item of demoQA) { let score = 0; for (const kw of item.keywords) if (q.includes(kw)) score += 1; if (score > bestScore) { bestScore = score; best = item; } } return bestScore > 0 ? best : null; } function wrapResultHtml(title, bodyHtml, lastReviewed) { return `
${escapeHtml(title)}
Last reviewed: ${escapeHtml(lastReviewed)}
Demo
${bodyHtml}
`; } function renderCitationsHtml(code) { const cits = demoNotices[code]?.citations || []; return renderInlineCitations(cits); } function renderInlineCitations(citations) { return ``; } function getSampleNoticeText(code) { switch(code) { case "CP2000": return "We propose changes to your 2023 tax return based on information we received from third parties. If you agree, sign and return. If you disagree, provide supporting documentation."; case "CP14": return "We assessed tax, penalty, and interest for the tax period shown. Pay by the due date to avoid additional charges."; case "12C": return "We need additional information to process your return. Send the requested forms and supporting documents within 20 days."; case "4883C": return "We need to verify your identity before we can process your tax return. Call the number provided and have your prior-year return available."; case "CP504": return "We intend to levy your state tax refund or other property. Contact us immediately to resolve your balance due."; default: return ""; } } function genericProAnswer() { return ` Conservative guidance (demo)
This appears to require a fact pattern review (tax year, filing status, income documents, credits, and notices involved). In production, the assistant would ask clarifying questions, provide a source-cited response, and output a client-friendly summary.

Next steps
• Confirm tax year + relevant forms (W-2/1099/1095-A, etc.)
• Identify if this involves a notice, mismatch, or processing delay
• Respond with documentation per IRS instructions

Citations
`; } function genericClientAnswer() { return ` This question needs a quick review of the tax documents and the year involved. Once we confirm details, we’ll follow the IRS’s documented steps and respond with the right forms so the issue can be resolved as quickly as possible.

Sources
`; } /* SETTINGS */ function wireSettings() { $("btnOpenSettings").addEventListener("click", openSettings); $("btnCloseSettings").addEventListener("click", closeSettings); $("settingsModal").addEventListener("click", (e) => { if (e.target.id === "settingsModal") closeSettings(); }); $("btnTestKey").addEventListener("click", () => { const key = ($("apiKeyInput").value || "").trim(); const provider = $("providerSelect").value; // Demo test: "passes" if length >= 10 if (key.length >= 10) { showToast(`Connected to ${provider.toUpperCase()} (demo).`, "ok"); } else { showToast("Key looks invalid (demo). Paste a longer key string.", "bad"); } }); $("btnSaveKey").addEventListener("click", () => { const settings = { provider: $("providerSelect").value, apiKey: ($("apiKeyInput").value || "").trim() }; localStorage.setItem(STORAGE.api, JSON.stringify(settings)); refreshApiStatusPill(); showToast("Saved locally (demo).", "ok"); }); } function openSettings() { restoreSettingsFields(); $("settingsModal").classList.remove("hidden"); } function closeSettings() { $("settingsModal").classList.add("hidden"); hideToast(); } function restoreSettingsFields() { const raw = localStorage.getItem(STORAGE.api); if (!raw) return; try { const s = JSON.parse(raw); if (s.provider) $("providerSelect").value = s.provider; if (s.apiKey) $("apiKeyInput").value = s.apiKey; } catch {} } function refreshApiStatusPill() { const pill = $("apiStatusPill"); const raw = localStorage.getItem(STORAGE.api); if (!raw) { pill.textContent = "BYO API: Not connected"; pill.style.borderColor = "rgba(255,255,255,.10)"; pill.style.background = "rgba(255,255,255,.04)"; pill.style.color = "rgba(255,255,255,.72)"; return; } try { const s = JSON.parse(raw); const ok = s.apiKey && s.apiKey.length >= 10; pill.textContent = ok ? `BYO API: Connected (${(s.provider || "provider").toUpperCase()})` : "BYO API: Saved (not tested)"; pill.style.borderColor = ok ? "rgba(102,227,166,.35)" : "rgba(110,168,255,.35)"; pill.style.background = ok ? "rgba(102,227,166,.12)" : "rgba(110,168,255,.10)"; pill.style.color = "rgba(255,255,255,.88)"; } catch { pill.textContent = "BYO API: Not connected"; } } function showToast(msg, kind) { const t = $("settingsToast"); t.textContent = msg; t.classList.remove("hidden", "ok", "bad"); t.classList.add(kind === "ok" ? "ok" : "bad"); } function hideToast() { $("settingsToast").classList.add("hidden"); } /* SAVE CURRENT RESULT */ function wireSave() { $("btnSaveCurrent").addEventListener("click", () => { if (!currentResult) { alert("Nothing to save yet. Generate a result first."); return; } const saved = loadSaved(); const item = { id: crypto.randomUUID(), ts: new Date().toISOString(), type: currentResult.type, title: currentResult.title, meta: currentResult.meta, html: currentResult.contentHtml }; saved.unshift(item); localStorage.setItem(STORAGE.saved, JSON.stringify(saved)); alert("Saved (local demo). Open Saved Answers to view."); }); $("btnClearSaved").addEventListener("click", () => { if (!confirm("Clear all saved answers?")) return; localStorage.removeItem(STORAGE.saved); renderSaved(); }); } function renderSaved() { const list = $("savedList"); const saved = loadSaved(); if (!saved.length) { list.innerHTML = `
No saved answers yet.
`; return; } list.innerHTML = saved.map(item => `
${escapeHtml(item.type)}
${escapeHtml(item.title)}
${escapeHtml(formatTs(item.ts))} • ${escapeHtml(item.meta || "")}
`).join(""); list.querySelectorAll("button[data-action]").forEach(btn => { btn.addEventListener("click", () => { const id = btn.dataset.id; const action = btn.dataset.action; if (action === "delete") deleteSaved(id); if (action === "open") openSaved(id); }); }); } function openSaved(id) { const saved = loadSaved(); const item = saved.find(x => x.id === id); if (!item) return; if (item.type === "Notice Decode") { activateView("notice"); $("noticeResult").innerHTML = item.html; $("resultMetaNotice").textContent = item.meta || "Saved item"; $("resultBadgeNotice").textContent = "Saved"; } else { activateView("qa"); $("qaResult").innerHTML = item.html; $("resultMetaQA").textContent = item.meta || "Saved item"; $("resultBadgeQA").textContent = "Saved"; } currentResult = { type: item.type, title: item.title, contentHtml: item.html, meta: item.meta }; persistLastResult(); } function deleteSaved(id) { const saved = loadSaved().filter(x => x.id !== id); localStorage.setItem(STORAGE.saved, JSON.stringify(saved)); renderSaved(); } function loadSaved() { const raw = localStorage.getItem(STORAGE.saved); if (!raw) return []; try { return JSON.parse(raw) || []; } catch { return []; } } function activateView(view) { document.querySelectorAll(".nav-item").forEach(b => b.classList.remove("active")); document.querySelector(`.nav-item[data-view="${view}"]`)?.classList.add("active"); currentView = view; showView(view); } function persistLastResult() { try { localStorage.setItem(STORAGE.lastResult, JSON.stringify(currentResult)); } catch {} } function formatTs(ts) { try { const d = new Date(ts); return d.toLocaleString(); } catch { return ts; } } function escapeHtml(str) { return String(str ?? "") .replaceAll("&","&") .replaceAll("<","<") .replaceAll(">",">") .replaceAll('"',""") .replaceAll("'","'"); } init();