diff --git a/src/content/content.css b/src/content/content.css index 077a022..be1beda 100644 --- a/src/content/content.css +++ b/src/content/content.css @@ -220,6 +220,24 @@ .ss-ps-add:hover { background: rgba(74, 222, 128, 0.15); } .ss-ps-add:disabled { border-color: #333; cursor: default; } +.ss-ps-ignore { + background: none; + border: 1px solid #6b7280; + border-radius: 4px; + color: #9ca3af; + font-size: 11px; + width: 24px; + height: 24px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; +} + +.ss-ps-ignore:hover { background: rgba(156, 163, 175, 0.15); color: #e5e7eb; } + /* Floating reveal mode indicator */ .ss-reveal-badge { position: fixed; diff --git a/src/content/content.js b/src/content/content.js index 5b4fc7b..d1df9e9 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -400,50 +400,44 @@ /** * Detect proper nouns (potential names, company names, project names) * that aren't configured in identity. Uses capitalization heuristics: - * - Capitalized words not at the start of a sentence - * - Multi-word capitalized sequences (e.g. "Acme Corp", "Project Atlas") + * - ONLY multi-word capitalized sequences (e.g. "Acme Corp", "Project Atlas") + * - Single capitalized words are too noisy — every sentence starts with one * - Filters out common English words and programming terms */ function detectProperNouns(text, configured) { const findings = []; - // Match capitalized words that aren't at the very start of the text - // and aren't after a period/newline (sentence start) - const re = /(?:^|[.!?\n]\s*)?([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*)/g; + // Only match TWO OR MORE consecutive capitalized words + // Single capitalized words cause too many false positives + const re = /\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})+)\b/g; let m; while ((m = re.exec(text)) !== null) { const fullMatch = m[1]; if (!fullMatch) continue; - // Check if this is at the start of a sentence - const before = text.slice(Math.max(0, m.index - 2), m.index); - const isSentenceStart = m.index === 0 || /[.!?\n]\s*$/.test(before); - - // Split into individual words and check each + // Split into individual words and filter common ones const words = fullMatch.split(/\s+/); const properWords = words.filter(w => w.length >= 3 && !COMMON_CAPITALIZED.has(w.toLowerCase()) && - !configured.has(w.toLowerCase()) + !configured.has(w.toLowerCase()) && + !ignoredValues.has(w.toLowerCase()) ); - if (properWords.length === 0) continue; + if (properWords.length < 2) continue; // need at least 2 proper words - // Single capitalized word at sentence start = likely not a proper noun - if (isSentenceStart && properWords.length === 1 && words.length === 1) continue; - - // Multi-word capitalized sequence is likely a proper noun - // Single capitalized word mid-sentence is likely a proper noun const value = properWords.join(' '); - if (value.length >= 3 && !configured.has(value.toLowerCase())) { + if (value.length >= 5 && !configured.has(value.toLowerCase()) && !ignoredValues.has(value.toLowerCase())) { findings.push({ name: 'Possible Name/Org', value, - hint: 'Capitalized word — could be a name, company, or project', + hint: 'Capitalized phrase — could be a name, company, or project', category: 'name', }); } } + } + } // Deduplicate const seen = new Set(); @@ -477,6 +471,7 @@ while ((m = pat.re.exec(text)) !== null) { const val = m[0]; if (configured.has(val.toLowerCase())) continue; + if (ignoredValues.has(val.toLowerCase())) continue; if (pat.skip && pat.skip.test(val)) continue; findings.push({ name: pat.name, value: val, hint: pat.hint, category: pat.cat }); } @@ -630,6 +625,30 @@ // ============================================================ const sessionSubstitutions = new Map(); + // Values the user has explicitly ignored via the "Ignore" button. + // Persisted to storage so they stay dismissed across page reloads. + const ignoredValues = new Set(); + + // Load ignored values from storage + (async () => { + const stored = await getStorageData('ss_ignored_ppi'); + if (Array.isArray(stored)) { + for (const v of stored) ignoredValues.add(v.toLowerCase()); + } + })(); + + function addIgnoredValue(value) { + ignoredValues.add(value.toLowerCase()); + // Persist + getStorageData('ss_ignored_ppi').then(stored => { + const list = Array.isArray(stored) ? stored : []; + if (!list.includes(value.toLowerCase())) { + list.push(value.toLowerCase()); + setStorageData('ss_ignored_ppi', list); + } + }); + } + // ============================================================ // Notify content script of substitutions (for badge + logging) // ============================================================ @@ -1632,6 +1651,7 @@ ${settings.autoAddDetected !== false ? `` : ''} + `; }).join(''); @@ -1694,6 +1714,24 @@ btn.disabled = true; }); }); + + // Ignore buttons + preSendWarningEl.querySelectorAll('.ss-ps-ignore').forEach(btn => { + btn.addEventListener('click', () => { + const value = decodeURIComponent(btn.dataset.value); + addIgnoredValue(value); + + // Remove this item's row + const row = btn.closest('.ss-ps-item'); + if (row) row.remove(); + + // Re-scan to update warning + if (inputEl) { + if (inputScanTimer) clearTimeout(inputScanTimer); + inputScanTimer = setTimeout(() => scanInputForPPI(inputEl), 150); + } + }); + }); } // Replace all occurrences of `real` with `fake` in an input or contenteditable element diff --git a/src/lib/auto-detect.js b/src/lib/auto-detect.js index e1af629..35a9eab 100644 --- a/src/lib/auto-detect.js +++ b/src/lib/auto-detect.js @@ -218,16 +218,15 @@ const AutoDetect = { */ _detectProperNouns(text, configured) { const findings = []; - const re = /(?:^|[.!?\n]\s*)?([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*)/g; + // Only match TWO OR MORE consecutive capitalized words + // Single capitalized words cause too many false positives (sentence starts) + const re = /\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})+)\b/g; let m; while ((m = re.exec(text)) !== null) { const fullMatch = m[1]; if (!fullMatch) continue; - const before = text.slice(Math.max(0, m.index - 2), m.index); - const isSentenceStart = m.index === 0 || /[.!?\n]\s*$/.test(before); - const words = fullMatch.split(/\s+/); const properWords = words.filter(w => w.length >= 3 && @@ -235,15 +234,14 @@ const AutoDetect = { !configured.has(w.toLowerCase()) ); - if (properWords.length === 0) continue; - if (isSentenceStart && properWords.length === 1 && words.length === 1) continue; + if (properWords.length < 2) continue; // need at least 2 proper words const value = properWords.join(' '); - if (value.length >= 3 && !configured.has(value.toLowerCase())) { + if (value.length >= 5 && !configured.has(value.toLowerCase())) { findings.push({ name: 'Possible Name/Org', value, - hint: 'Capitalized word — could be a name, company, or project', + hint: 'Capitalized phrase — could be a name, company, or project', category: 'name', }); }