feat: add reveal paste-back tool + fix smart detection bail
Test tab now has two modes: - Strip (real → fake): paste text with real data, see what gets sent - Reveal (fake → real): paste AI output with fake data, get back real data with a "Copy to Clipboard" button Also fixes: - content.js smartSubstitute bailing when identity.enabled was undefined (defaulted enabled to all-true instead of returning) - Test tab now reloads identity from storage on tab switch so changes saved in the Identity tab take effect immediately - Shows yellow warning when identity fields are missing https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
@@ -95,7 +95,9 @@
|
||||
]);
|
||||
|
||||
function smartSubstitute(text, id) {
|
||||
if (!id || !id.enabled) return { text, replacements: [] };
|
||||
if (!id) return { text, replacements: [] };
|
||||
// Default enabled to all-true if not set
|
||||
if (!id.enabled) id.enabled = { emails: true, names: true, usernames: true, phones: true, paths: true };
|
||||
const replacements = [];
|
||||
let result = text;
|
||||
|
||||
|
||||
@@ -408,6 +408,48 @@ body {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Test tab mode toggle */
|
||||
.test-mode-toggle {
|
||||
display: flex;
|
||||
background: #f3f4f6;
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.test-mode-btn {
|
||||
flex: 1;
|
||||
padding: 6px 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.test-mode-btn.active {
|
||||
background: #fff;
|
||||
color: #111;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.test-identity-status {
|
||||
margin-top: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #fef3c7;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
color: #92400e;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.test-identity-status.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Test tab */
|
||||
.help-text {
|
||||
font-size: 12px;
|
||||
|
||||
+26
-6
@@ -141,13 +141,33 @@
|
||||
|
||||
<!-- Test Tab -->
|
||||
<section class="tab-content" id="tab-test">
|
||||
<p class="help-text">Type text containing your real values to see what would be sent.</p>
|
||||
<textarea id="testInput" class="textarea" placeholder="Try: My name is John Smith, email john@gmail.com, logged in as jsmith@macbook-pro" rows="4"></textarea>
|
||||
<div class="diff-view" id="diffView">
|
||||
<div class="diff-label">What gets sent:</div>
|
||||
<div class="diff-output" id="diffOutput"></div>
|
||||
<div class="test-mode-toggle">
|
||||
<button class="test-mode-btn active" data-mode="strip">Strip (real → fake)</button>
|
||||
<button class="test-mode-btn" data-mode="reveal">Reveal (fake → real)</button>
|
||||
</div>
|
||||
<div class="diff-stats" id="diffStats"></div>
|
||||
|
||||
<div id="stripMode">
|
||||
<p class="help-text">Paste text with your real info. See what gets sent.</p>
|
||||
<textarea id="testInput" class="textarea" placeholder="Try: My name is John Smith, email john@gmail.com, logged in as jsmith@macbook-pro" rows="4"></textarea>
|
||||
<div class="diff-view" id="diffView">
|
||||
<div class="diff-label">What gets sent:</div>
|
||||
<div class="diff-output" id="diffOutput"></div>
|
||||
</div>
|
||||
<div class="diff-stats" id="diffStats"></div>
|
||||
</div>
|
||||
|
||||
<div id="revealMode" style="display:none">
|
||||
<p class="help-text">Paste text from AI responses (with fake data). Get back real data to copy.</p>
|
||||
<textarea id="revealInput" class="textarea" placeholder="Paste AI output here, e.g.: Hello Alex Demo, I see your project at /home/ademo/..." rows="4"></textarea>
|
||||
<div class="diff-view" id="revealView">
|
||||
<div class="diff-label">Your real data:</div>
|
||||
<div class="diff-output" id="revealOutput"></div>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="btnCopyRevealed" style="width:100%;margin-top:8px">Copy to Clipboard</button>
|
||||
<div class="diff-stats" id="revealStats"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-identity-status" id="identityStatus"></div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
+156
-30
@@ -34,6 +34,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
$(`#tab-${tab.dataset.tab}`).classList.add('active');
|
||||
|
||||
if (tab.dataset.tab === 'activity') renderActivity();
|
||||
if (tab.dataset.tab === 'test') {
|
||||
// Reload identity from storage in case it was just saved
|
||||
Storage.getIdentity().then((id) => {
|
||||
identity = id;
|
||||
updateIdentityStatus();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,8 +83,21 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
renderActivity();
|
||||
});
|
||||
|
||||
// Test tab - live diff
|
||||
// Test tab - live diff + reveal
|
||||
$('#testInput').addEventListener('input', renderTestDiff);
|
||||
$('#revealInput').addEventListener('input', renderRevealDiff);
|
||||
$('#btnCopyRevealed').addEventListener('click', copyRevealedText);
|
||||
|
||||
// Test mode toggle (strip vs reveal)
|
||||
$$('.test-mode-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
$$('.test-mode-btn').forEach((b) => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const mode = btn.dataset.mode;
|
||||
$('#stripMode').style.display = mode === 'strip' ? 'block' : 'none';
|
||||
$('#revealMode').style.display = mode === 'reveal' ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Options link
|
||||
$('#btnOptions').addEventListener('click', (e) => {
|
||||
@@ -297,8 +317,7 @@ async function renderActivity() {
|
||||
.join('');
|
||||
}
|
||||
|
||||
// --- Test Diff ---
|
||||
// Runs both smart patterns AND explicit mappings, shows combined result
|
||||
// --- Test Diff (Strip: real → fake) ---
|
||||
function renderTestDiff() {
|
||||
const input = $('#testInput').value;
|
||||
const output = $('#diffOutput');
|
||||
@@ -310,39 +329,27 @@ function renderTestDiff() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Smart patterns first (broader catches), then explicit mappings (specific overrides)
|
||||
const smartResult = SmartPatterns.substitute(input, identity);
|
||||
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
||||
|
||||
const allReplacements = [...smartResult.replacements, ...explicitResult.replacements];
|
||||
const finalText = explicitResult.text;
|
||||
|
||||
// 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;
|
||||
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;
|
||||
@@ -352,13 +359,132 @@ function renderTestDiff() {
|
||||
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;
|
||||
// --- Reveal Diff (fake → real) ---
|
||||
function renderRevealDiff() {
|
||||
const input = $('#revealInput').value;
|
||||
const output = $('#revealOutput');
|
||||
const stats = $('#revealStats');
|
||||
|
||||
if (!input) {
|
||||
output.innerHTML = '';
|
||||
stats.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Reverse: substitute → real using SmartPatterns reveal + explicit reveal
|
||||
let result = input;
|
||||
let totalCount = 0;
|
||||
|
||||
// Reverse explicit mappings (substitute → real)
|
||||
const explicitRevealed = SubstitutionEngine.reveal(result, mappings);
|
||||
// Count explicit reveals
|
||||
for (const m of mappings) {
|
||||
if (!m.enabled || !m.substitute) continue;
|
||||
const regex = new RegExp(escapeRegex(m.substitute), m.caseSensitive ? 'g' : 'gi');
|
||||
const matches = result.match(regex);
|
||||
if (matches) totalCount += matches.length;
|
||||
}
|
||||
result = explicitRevealed;
|
||||
|
||||
// Reverse smart patterns (all identity substitutes → real)
|
||||
const allSubs = gatherSmartSubstitutePairs(identity);
|
||||
for (const pair of allSubs) {
|
||||
const regex = new RegExp(escapeRegex(pair.substitute), 'gi');
|
||||
const matches = result.match(regex);
|
||||
if (matches) totalCount += matches.length;
|
||||
result = result.replace(regex, pair.real);
|
||||
}
|
||||
|
||||
if (result === input) {
|
||||
output.textContent = input;
|
||||
stats.textContent = 'No substituted values found to reveal';
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight revealed values
|
||||
let html = escapeHtml(result);
|
||||
for (const pair of [...allSubs, ...mappings.filter(m => m.enabled)]) {
|
||||
const real = pair.real;
|
||||
if (!real) continue;
|
||||
const escapedReal = escapeHtml(real);
|
||||
html = html.replace(
|
||||
new RegExp(escapeRegex(escapedReal), 'gi'),
|
||||
`<span class="sub-highlight" title="Was: ${escapeHtml(pair.substitute)}" style="background:#dbeafe;color:#1d4ed8">${escapedReal}</span>`
|
||||
);
|
||||
}
|
||||
output.innerHTML = html;
|
||||
stats.textContent = `${totalCount} value${totalCount !== 1 ? 's' : ''} revealed`;
|
||||
}
|
||||
|
||||
// Gather all substitute → real pairs from identity for reveal
|
||||
function gatherSmartSubstitutePairs(id) {
|
||||
const pairs = [];
|
||||
for (const e of (id.emails || [])) {
|
||||
if (e.substitute && e.real) pairs.push(e);
|
||||
}
|
||||
if (id.catchAllEmail) {
|
||||
pairs.push({ substitute: id.catchAllEmail, real: '[catch-all]' });
|
||||
}
|
||||
for (const n of (id.names || [])) {
|
||||
if (n.substitute && n.real) pairs.push(n);
|
||||
}
|
||||
for (const u of (id.usernames || [])) {
|
||||
if (u.substitute && u.real) pairs.push(u);
|
||||
}
|
||||
for (const h of (id.hostnames || [])) {
|
||||
if (h.substitute && h.real) pairs.push(h);
|
||||
}
|
||||
for (const p of (id.phones || [])) {
|
||||
if (p.substitute && p.real) pairs.push(p);
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
async function copyRevealedText() {
|
||||
const output = $('#revealOutput');
|
||||
const text = output.textContent;
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
const btn = $('#btnCopyRevealed');
|
||||
btn.textContent = 'Copied!';
|
||||
btn.style.background = '#059669';
|
||||
setTimeout(() => {
|
||||
btn.textContent = 'Copy to Clipboard';
|
||||
btn.style.background = '';
|
||||
}, 1500);
|
||||
} catch (e) {
|
||||
// Fallback
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Identity Status ---
|
||||
function updateIdentityStatus() {
|
||||
const el = $('#identityStatus');
|
||||
const missing = [];
|
||||
|
||||
if (!(identity.names || []).some(n => n.type === 'first')) missing.push('first name');
|
||||
if (!(identity.names || []).some(n => n.type === 'last')) missing.push('last name');
|
||||
if ((identity.emails || []).length === 0 && !identity.catchAllEmail) missing.push('email');
|
||||
if ((identity.usernames || []).length === 0) missing.push('username');
|
||||
|
||||
if (missing.length > 0) {
|
||||
el.textContent = `Identity missing: ${missing.join(', ')}. Go to the Identity tab to set up.`;
|
||||
el.classList.add('visible');
|
||||
} else {
|
||||
el.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegex(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
// --- Status Dot ---
|
||||
|
||||
Reference in New Issue
Block a user