feat: underline revealed text in AI responses
Revealed values are now wrapped in <span class="ss-revealed"> with a blue underline and subtle blue background, making it obvious which parts are your real data vs what the AI said. Hover shows tooltip "Substituted value: [fake]" so you can see what the AI actually received. Un-reveal properly removes the spans and normalizes text nodes. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
@@ -16,9 +16,10 @@
|
|||||||
/* Revealed text styling (when reveal mode shows real values in responses) */
|
/* Revealed text styling (when reveal mode shows real values in responses) */
|
||||||
.ss-revealed {
|
.ss-revealed {
|
||||||
background: rgba(59, 130, 246, 0.1);
|
background: rgba(59, 130, 246, 0.1);
|
||||||
border-bottom: 1.5px dashed rgba(59, 130, 246, 0.5);
|
border-bottom: 2px solid rgba(59, 130, 246, 0.6);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0 1px;
|
padding: 0 2px;
|
||||||
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Floating reveal mode indicator */
|
/* Floating reveal mode indicator */
|
||||||
|
|||||||
+82
-7
@@ -630,37 +630,109 @@
|
|||||||
|
|
||||||
function revealInElement(el) {
|
function revealInElement(el) {
|
||||||
if (SKIP_REVEAL_TAGS.has(el.tagName)) return;
|
if (SKIP_REVEAL_TAGS.has(el.tagName)) return;
|
||||||
// Skip our own badge
|
|
||||||
if (el.classList?.contains('ss-reveal-badge')) return;
|
if (el.classList?.contains('ss-reveal-badge')) return;
|
||||||
|
if (el.classList?.contains('ss-revealed')) return; // skip our own spans
|
||||||
|
|
||||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
||||||
acceptNode(node) {
|
acceptNode(node) {
|
||||||
const parent = node.parentElement;
|
const parent = node.parentElement;
|
||||||
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
||||||
if (parent?.classList?.contains('ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
|
if (parent?.classList?.contains('ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
|
||||||
|
if (parent?.classList?.contains('ss-revealed')) return NodeFilter.FILTER_REJECT;
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Collect nodes first (modifying DOM during walk breaks the walker)
|
||||||
|
const nodes = [];
|
||||||
let textNode;
|
let textNode;
|
||||||
while ((textNode = walker.nextNode())) {
|
while ((textNode = walker.nextNode())) {
|
||||||
const text = textNode.textContent;
|
nodes.push(textNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_revealPairsCache) _revealPairsCache = buildRevealPairs();
|
||||||
|
const pairs = _revealPairsCache;
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
const text = node.textContent;
|
||||||
if (!text || text.length < MIN_STRING_LENGTH) continue;
|
if (!text || text.length < MIN_STRING_LENGTH) continue;
|
||||||
|
|
||||||
if (!originalTexts.has(textNode)) {
|
// Save original
|
||||||
originalTexts.set(textNode, text);
|
if (!originalTexts.has(node)) {
|
||||||
|
originalTexts.set(node, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
const revealed = revealText(text);
|
// Find all substituted values and their positions
|
||||||
if (revealed !== text) {
|
const segments = [];
|
||||||
textNode.textContent = revealed;
|
let remaining = text;
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (const p of pairs) {
|
||||||
|
const escaped = esc(p.from);
|
||||||
|
const regex = new RegExp(escaped, p.caseSensitive ? 'gi' : 'gi');
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
segments.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + match[0].length,
|
||||||
|
original: match[0],
|
||||||
|
revealed: p.to,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (segments.length === 0) continue;
|
||||||
|
|
||||||
|
// Sort by position, deduplicate overlaps
|
||||||
|
segments.sort((a, b) => a.start - b.start);
|
||||||
|
const deduped = [];
|
||||||
|
let lastEnd = -1;
|
||||||
|
for (const s of segments) {
|
||||||
|
if (s.start >= lastEnd) {
|
||||||
|
deduped.push(s);
|
||||||
|
lastEnd = s.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build fragment: text + highlighted spans
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
for (const s of deduped) {
|
||||||
|
// Text before this match
|
||||||
|
if (s.start > pos) {
|
||||||
|
frag.appendChild(document.createTextNode(text.slice(pos, s.start)));
|
||||||
|
}
|
||||||
|
// Highlighted revealed value
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = 'ss-revealed';
|
||||||
|
span.textContent = s.revealed;
|
||||||
|
span.title = 'Substituted value: ' + s.original;
|
||||||
|
frag.appendChild(span);
|
||||||
|
pos = s.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remaining text after last match
|
||||||
|
if (pos < text.length) {
|
||||||
|
frag.appendChild(document.createTextNode(text.slice(pos)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the text node with the fragment
|
||||||
|
node.parentNode.replaceChild(frag, node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unrevealInElement(el) {
|
function unrevealInElement(el) {
|
||||||
if (SKIP_REVEAL_TAGS.has(el.tagName)) return;
|
if (SKIP_REVEAL_TAGS.has(el.tagName)) return;
|
||||||
|
|
||||||
|
// Remove all ss-revealed spans, restoring original text
|
||||||
|
const spans = el.querySelectorAll('.ss-revealed');
|
||||||
|
for (const span of spans) {
|
||||||
|
const text = document.createTextNode(span.title?.replace('Substituted value: ', '') || span.textContent);
|
||||||
|
span.parentNode.replaceChild(text, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also restore any text nodes that were modified without spans
|
||||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
||||||
let textNode;
|
let textNode;
|
||||||
while ((textNode = walker.nextNode())) {
|
while ((textNode = walker.nextNode())) {
|
||||||
@@ -669,6 +741,9 @@
|
|||||||
textNode.textContent = original;
|
textNode.textContent = original;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize adjacent text nodes
|
||||||
|
el.normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Elements to skip when revealing (inputs, scripts, styles, extension UI)
|
// Elements to skip when revealing (inputs, scripts, styles, extension UI)
|
||||||
|
|||||||
Reference in New Issue
Block a user