feat: add smart pattern detection for emails, names, usernames, phones
Instead of requiring explicit mappings for every variation, users now configure their identity once (Identity tab) and Silent Send auto-catches: - Emails: any address @gmail, @yahoo, @outlook, etc. - Names: first/last, full name, reversed, possessives, case variants - Usernames: user@host, ~user, /home/user, C:\Users\user - Phones: all common formats ((555) 123-4567, 555.123.4567, etc.) Smart patterns run before explicit mappings, so explicit rules can override smart catches when needed. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
@@ -242,6 +242,46 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Identity tab */
|
||||
.id-section {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.id-section:last-of-type {
|
||||
border-bottom: none;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.id-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.id-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.input-sm {
|
||||
padding: 5px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.id-hint {
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
margin-top: 2px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Mapping list */
|
||||
.add-mapping {
|
||||
padding-bottom: 12px;
|
||||
|
||||
+58
-2
@@ -29,13 +29,69 @@
|
||||
|
||||
<!-- Tab Bar -->
|
||||
<nav class="tabs">
|
||||
<button class="tab active" data-tab="mappings">Mappings</button>
|
||||
<button class="tab active" data-tab="identity">Identity</button>
|
||||
<button class="tab" data-tab="mappings">Mappings</button>
|
||||
<button class="tab" data-tab="activity">Activity</button>
|
||||
<button class="tab" data-tab="test">Test</button>
|
||||
</nav>
|
||||
|
||||
<!-- Identity Tab (Smart Patterns) -->
|
||||
<section class="tab-content active" id="tab-identity">
|
||||
<p class="help-text">Tell Silent Send who you are. It auto-catches all variations.</p>
|
||||
|
||||
<div class="id-section">
|
||||
<div class="id-label">Names</div>
|
||||
<div class="id-row">
|
||||
<input type="text" id="idFirstReal" placeholder="First name" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idFirstSub" placeholder="Fake first" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-row">
|
||||
<input type="text" id="idLastReal" placeholder="Last name" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idLastSub" placeholder="Fake last" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-hint">Catches: first last, last first, possessives, case variants</div>
|
||||
</div>
|
||||
|
||||
<div class="id-section">
|
||||
<div class="id-label">Email</div>
|
||||
<div class="id-row">
|
||||
<input type="email" id="idEmailReal" placeholder="you@gmail.com" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="email" id="idEmailSub" placeholder="fake@example.com" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-row">
|
||||
<input type="email" id="idCatchAllEmail" placeholder="Catch-all email (for unknown addresses)" class="input" style="flex:1">
|
||||
</div>
|
||||
<div class="id-hint">Catches: any *@gmail.com, *@yahoo.com, *@outlook.com, etc.</div>
|
||||
</div>
|
||||
|
||||
<div class="id-section">
|
||||
<div class="id-label">Username / Computer</div>
|
||||
<div class="id-row">
|
||||
<input type="text" id="idUserReal" placeholder="jsmith" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idUserSub" placeholder="ademo" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-hint">Catches: jsmith@hostname, /home/jsmith, ~jsmith, C:\Users\jsmith</div>
|
||||
</div>
|
||||
|
||||
<div class="id-section">
|
||||
<div class="id-label">Phone</div>
|
||||
<div class="id-row">
|
||||
<input type="text" id="idPhoneReal" placeholder="(555) 123-4567" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idPhoneSub" placeholder="(555) 000-0000" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-hint">Catches: all formats — (555) 123-4567, 555-123-4567, 555.123.4567</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" id="btnSaveIdentity" style="width:100%;margin-top:8px">Save Identity</button>
|
||||
</section>
|
||||
|
||||
<!-- Mappings Tab -->
|
||||
<section class="tab-content active" id="tab-mappings">
|
||||
<section class="tab-content" id="tab-mappings">
|
||||
<div class="add-mapping">
|
||||
<div class="input-row">
|
||||
<input type="text" id="inputReal" placeholder="Real value (e.g. John Smith)" class="input">
|
||||
|
||||
+142
-14
@@ -1,9 +1,11 @@
|
||||
import SubstitutionEngine from '../lib/substitution-engine.js';
|
||||
import SmartPatterns from '../lib/smart-patterns.js';
|
||||
import Storage from '../lib/storage.js';
|
||||
import api from '../lib/browser-polyfill.js';
|
||||
|
||||
// --- State ---
|
||||
let mappings = [];
|
||||
let identity = {};
|
||||
let settings = {};
|
||||
|
||||
// --- DOM refs ---
|
||||
@@ -13,10 +15,12 @@ const $$ = (sel) => document.querySelectorAll(sel);
|
||||
// --- Init ---
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
mappings = await Storage.getMappings();
|
||||
identity = await Storage.getIdentity();
|
||||
settings = await Storage.getSettings();
|
||||
|
||||
renderMappings();
|
||||
renderActivity();
|
||||
loadIdentityForm();
|
||||
updateStatusDot();
|
||||
|
||||
$('#enableToggle').checked = settings.enabled;
|
||||
@@ -57,6 +61,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
$('#btnReveal').classList.toggle('active', settings.revealMode);
|
||||
|
||||
// Save identity
|
||||
$('#btnSaveIdentity').addEventListener('click', saveIdentity);
|
||||
|
||||
// Add mapping
|
||||
$('#btnAdd').addEventListener('click', addMapping);
|
||||
$('#inputSub').addEventListener('keydown', (e) => {
|
||||
@@ -79,6 +86,93 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Identity ---
|
||||
function loadIdentityForm() {
|
||||
const first = (identity.names || []).find(n => n.type === 'first');
|
||||
const last = (identity.names || []).find(n => n.type === 'last');
|
||||
const email = (identity.emails || [])[0];
|
||||
const user = (identity.usernames || [])[0];
|
||||
const phone = (identity.phones || [])[0];
|
||||
|
||||
if (first) {
|
||||
$('#idFirstReal').value = first.real || '';
|
||||
$('#idFirstSub').value = first.substitute || '';
|
||||
}
|
||||
if (last) {
|
||||
$('#idLastReal').value = last.real || '';
|
||||
$('#idLastSub').value = last.substitute || '';
|
||||
}
|
||||
if (email) {
|
||||
$('#idEmailReal').value = email.real || '';
|
||||
$('#idEmailSub').value = email.substitute || '';
|
||||
}
|
||||
$('#idCatchAllEmail').value = identity.catchAllEmail || '';
|
||||
if (user) {
|
||||
$('#idUserReal').value = user.real || '';
|
||||
$('#idUserSub').value = user.substitute || '';
|
||||
}
|
||||
if (phone) {
|
||||
$('#idPhoneReal').value = phone.real || '';
|
||||
$('#idPhoneSub').value = phone.substitute || '';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveIdentity() {
|
||||
const names = [];
|
||||
const firstReal = $('#idFirstReal').value.trim();
|
||||
const firstSub = $('#idFirstSub').value.trim();
|
||||
if (firstReal && firstSub) {
|
||||
names.push({ real: firstReal, substitute: firstSub, type: 'first' });
|
||||
}
|
||||
const lastReal = $('#idLastReal').value.trim();
|
||||
const lastSub = $('#idLastSub').value.trim();
|
||||
if (lastReal && lastSub) {
|
||||
names.push({ real: lastReal, substitute: lastSub, type: 'last' });
|
||||
}
|
||||
|
||||
const emails = [];
|
||||
const emailReal = $('#idEmailReal').value.trim();
|
||||
const emailSub = $('#idEmailSub').value.trim();
|
||||
if (emailReal && emailSub) {
|
||||
emails.push({ real: emailReal, substitute: emailSub });
|
||||
}
|
||||
|
||||
const usernames = [];
|
||||
const userReal = $('#idUserReal').value.trim();
|
||||
const userSub = $('#idUserSub').value.trim();
|
||||
if (userReal && userSub) {
|
||||
usernames.push({ real: userReal, substitute: userSub });
|
||||
}
|
||||
|
||||
const phones = [];
|
||||
const phoneReal = $('#idPhoneReal').value.trim();
|
||||
const phoneSub = $('#idPhoneSub').value.trim();
|
||||
if (phoneReal && phoneSub) {
|
||||
phones.push({ real: phoneReal, substitute: phoneSub });
|
||||
}
|
||||
|
||||
identity = {
|
||||
names,
|
||||
emails,
|
||||
usernames,
|
||||
phones,
|
||||
catchAllEmail: $('#idCatchAllEmail').value.trim(),
|
||||
emailDomains: identity.emailDomains || [],
|
||||
enabled: identity.enabled || { emails: true, names: true, usernames: true, phones: true, paths: true },
|
||||
};
|
||||
|
||||
await Storage.saveIdentity(identity);
|
||||
|
||||
// Flash save button
|
||||
const btn = $('#btnSaveIdentity');
|
||||
btn.textContent = 'Saved!';
|
||||
btn.style.background = '#059669';
|
||||
setTimeout(() => {
|
||||
btn.textContent = 'Save Identity';
|
||||
btn.style.background = '';
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// --- Add Mapping ---
|
||||
async function addMapping() {
|
||||
const real = $('#inputReal').value.trim();
|
||||
@@ -191,6 +285,7 @@ async function renderActivity() {
|
||||
}
|
||||
|
||||
// --- Test Diff ---
|
||||
// Runs both smart patterns AND explicit mappings, shows combined result
|
||||
function renderTestDiff() {
|
||||
const input = $('#testInput').value;
|
||||
const output = $('#diffOutput');
|
||||
@@ -202,22 +297,55 @@ function renderTestDiff() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { text, replacements } = SubstitutionEngine.substitute(input, mappings);
|
||||
const chunks = SubstitutionEngine.diff(input, text, mappings);
|
||||
// Smart patterns first (broader catches), then explicit mappings (specific overrides)
|
||||
const smartResult = SmartPatterns.substitute(input, identity);
|
||||
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
||||
|
||||
output.innerHTML = chunks
|
||||
.map((chunk) => {
|
||||
if (chunk.type === 'substituted') {
|
||||
return `<span class="sub-highlight" title="Was: ${escapeHtml(chunk.original)}">${escapeHtml(chunk.replacement)}</span>`;
|
||||
}
|
||||
return escapeHtml(chunk.text);
|
||||
})
|
||||
.join('');
|
||||
const allReplacements = [...smartResult.replacements, ...explicitResult.replacements];
|
||||
const finalText = explicitResult.text;
|
||||
|
||||
stats.textContent =
|
||||
replacements.length > 0
|
||||
? `${replacements.length} substitution${replacements.length !== 1 ? 's' : ''} would be made`
|
||||
: 'No substitutions detected';
|
||||
// Simple diff: highlight differences
|
||||
if (finalText === input) {
|
||||
output.textContent = input;
|
||||
stats.textContent = 'No substitutions detected';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a visual diff by running smart patterns on original to find positions
|
||||
// For display, we re-run on the original to get positions
|
||||
const smartPositions = findReplacementPositions(input, identity, mappings);
|
||||
|
||||
if (smartPositions.length === 0) {
|
||||
output.textContent = finalText;
|
||||
} else {
|
||||
// Build highlighted output from the final text
|
||||
// Simpler approach: show the final text with replaced values highlighted
|
||||
let html = escapeHtml(finalText);
|
||||
for (const r of allReplacements) {
|
||||
const escapedReplaced = escapeHtml(r.replaced);
|
||||
html = html.replace(
|
||||
escapedReplaced,
|
||||
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
|
||||
);
|
||||
}
|
||||
output.innerHTML = html;
|
||||
}
|
||||
|
||||
const smartCount = smartResult.replacements.length;
|
||||
const explicitCount = explicitResult.replacements.length;
|
||||
const parts = [];
|
||||
if (smartCount > 0) parts.push(`${smartCount} smart`);
|
||||
if (explicitCount > 0) parts.push(`${explicitCount} explicit`);
|
||||
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
||||
}
|
||||
|
||||
function findReplacementPositions(text, ident, maps) {
|
||||
const positions = [];
|
||||
const r1 = SmartPatterns.substitute(text, ident);
|
||||
positions.push(...r1.replacements);
|
||||
const r2 = SubstitutionEngine.substitute(r1.text, maps);
|
||||
positions.push(...r2.replacements);
|
||||
return positions;
|
||||
}
|
||||
|
||||
// --- Status Dot ---
|
||||
|
||||
Reference in New Issue
Block a user