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) {
|
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 = [];
|
const replacements = [];
|
||||||
let result = text;
|
let result = text;
|
||||||
|
|
||||||
|
|||||||
@@ -408,6 +408,48 @@ body {
|
|||||||
color: #059669;
|
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 */
|
/* Test tab */
|
||||||
.help-text {
|
.help-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
+26
-6
@@ -141,13 +141,33 @@
|
|||||||
|
|
||||||
<!-- Test Tab -->
|
<!-- Test Tab -->
|
||||||
<section class="tab-content" id="tab-test">
|
<section class="tab-content" id="tab-test">
|
||||||
<p class="help-text">Type text containing your real values to see what would be sent.</p>
|
<div class="test-mode-toggle">
|
||||||
<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>
|
<button class="test-mode-btn active" data-mode="strip">Strip (real → fake)</button>
|
||||||
<div class="diff-view" id="diffView">
|
<button class="test-mode-btn" data-mode="reveal">Reveal (fake → real)</button>
|
||||||
<div class="diff-label">What gets sent:</div>
|
|
||||||
<div class="diff-output" id="diffOutput"></div>
|
|
||||||
</div>
|
</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>
|
</section>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|||||||
+156
-30
@@ -34,6 +34,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
$(`#tab-${tab.dataset.tab}`).classList.add('active');
|
$(`#tab-${tab.dataset.tab}`).classList.add('active');
|
||||||
|
|
||||||
if (tab.dataset.tab === 'activity') renderActivity();
|
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();
|
renderActivity();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test tab - live diff
|
// Test tab - live diff + reveal
|
||||||
$('#testInput').addEventListener('input', renderTestDiff);
|
$('#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
|
// Options link
|
||||||
$('#btnOptions').addEventListener('click', (e) => {
|
$('#btnOptions').addEventListener('click', (e) => {
|
||||||
@@ -297,8 +317,7 @@ async function renderActivity() {
|
|||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Test Diff ---
|
// --- Test Diff (Strip: real → fake) ---
|
||||||
// Runs both smart patterns AND explicit mappings, shows combined result
|
|
||||||
function renderTestDiff() {
|
function renderTestDiff() {
|
||||||
const input = $('#testInput').value;
|
const input = $('#testInput').value;
|
||||||
const output = $('#diffOutput');
|
const output = $('#diffOutput');
|
||||||
@@ -310,39 +329,27 @@ function renderTestDiff() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smart patterns first (broader catches), then explicit mappings (specific overrides)
|
|
||||||
const smartResult = SmartPatterns.substitute(input, identity);
|
const smartResult = SmartPatterns.substitute(input, identity);
|
||||||
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
||||||
|
|
||||||
const allReplacements = [...smartResult.replacements, ...explicitResult.replacements];
|
const allReplacements = [...smartResult.replacements, ...explicitResult.replacements];
|
||||||
const finalText = explicitResult.text;
|
const finalText = explicitResult.text;
|
||||||
|
|
||||||
// Simple diff: highlight differences
|
|
||||||
if (finalText === input) {
|
if (finalText === input) {
|
||||||
output.textContent = input;
|
output.textContent = input;
|
||||||
stats.textContent = 'No substitutions detected';
|
stats.textContent = 'No substitutions detected';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a visual diff by running smart patterns on original to find positions
|
let html = escapeHtml(finalText);
|
||||||
// For display, we re-run on the original to get positions
|
for (const r of allReplacements) {
|
||||||
const smartPositions = findReplacementPositions(input, identity, mappings);
|
const escapedReplaced = escapeHtml(r.replaced);
|
||||||
|
html = html.replace(
|
||||||
if (smartPositions.length === 0) {
|
escapedReplaced,
|
||||||
output.textContent = finalText;
|
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
|
||||||
} 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;
|
|
||||||
}
|
}
|
||||||
|
output.innerHTML = html;
|
||||||
|
|
||||||
const smartCount = smartResult.replacements.length;
|
const smartCount = smartResult.replacements.length;
|
||||||
const explicitCount = explicitResult.replacements.length;
|
const explicitCount = explicitResult.replacements.length;
|
||||||
@@ -352,13 +359,132 @@ function renderTestDiff() {
|
|||||||
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findReplacementPositions(text, ident, maps) {
|
// --- Reveal Diff (fake → real) ---
|
||||||
const positions = [];
|
function renderRevealDiff() {
|
||||||
const r1 = SmartPatterns.substitute(text, ident);
|
const input = $('#revealInput').value;
|
||||||
positions.push(...r1.replacements);
|
const output = $('#revealOutput');
|
||||||
const r2 = SubstitutionEngine.substitute(r1.text, maps);
|
const stats = $('#revealStats');
|
||||||
positions.push(...r2.replacements);
|
|
||||||
return positions;
|
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 ---
|
// --- Status Dot ---
|
||||||
|
|||||||
Reference in New Issue
Block a user