diff --git a/apps/browser-extension/entrypoints/content/grok.test.ts b/apps/browser-extension/entrypoints/content/grok.test.ts new file mode 100644 index 00000000..5625d99c --- /dev/null +++ b/apps/browser-extension/entrypoints/content/grok.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "bun:test" +import { removeGrokMemoryUiElements, sanitizeGrokMemoryText } from "./grok" + +describe("sanitizeGrokMemoryText", () => { + it("removes Grok interface text when it occupies a full line", () => { + const text = [ + "Memory from your chats", + "This summary is regenerated periodically from your conversations.", + "Save to supermemory", + "Close", + "Delete memory", + " Edit ", + "The user prefers TypeScript.", + ].join("\r\n") + + expect(sanitizeGrokMemoryText(text)).toBe("The user prefers TypeScript.") + }) + + it("preserves interface words when they are part of user memories", () => { + const text = [ + "Editor: VS Code", + "I use the Edit command every day.", + "Close friends know I prefer tea.", + "Delete memory leaks before deploying.", + "Memory from your chats can be useful.", + ].join("\n") + + expect(sanitizeGrokMemoryText(text)).toBe(text) + }) + + it("trims lines and removes empty whitespace", () => { + expect( + sanitizeGrokMemoryText("\n First memory \n\t\n Second memory \n"), + ).toBe("First memory\nSecond memory") + }) + + it("removes UI elements without deleting formatted memory text", () => { + const memory = createTextElement("Editor: VS Code") + const heading = createTextElement("Memory from your chats") + const description = createTextElement( + "This summary is regenerated periodically from your conversations.", + ) + const editButton = createTextElement("Edit") + const deleteButton = createTextElement("Delete memory") + const inlineEdit = createTextElement("Edit") + const elements = [ + memory, + heading, + description, + editButton, + deleteButton, + inlineEdit, + ] + + removeGrokMemoryUiElements({ + querySelectorAll: (selector) => { + if (selector.includes("div")) return [heading, description] + if (selector.startsWith("button")) return [editButton, deleteButton] + return [] + }, + }) + + const collapsedText = elements + .filter((element) => !element.removed) + .map((element) => element.textContent) + .join("") + expect(collapsedText).toBe("Editor: VS CodeEdit") + expect(memory.removed).toBe(false) + expect(inlineEdit.removed).toBe(false) + }) +}) + +function createTextElement(textContent: string) { + return { + textContent, + removed: false, + remove() { + this.removed = true + }, + } +} diff --git a/apps/browser-extension/entrypoints/content/grok.ts b/apps/browser-extension/entrypoints/content/grok.ts index 378ad750..ada14733 100644 --- a/apps/browser-extension/entrypoints/content/grok.ts +++ b/apps/browser-extension/entrypoints/content/grok.ts @@ -265,33 +265,55 @@ function getGrokMemoryDialog(): HTMLElement | null { return candidates[0] || null } -const GROK_MEMORY_UI_TEXT = [ +const GROK_MEMORY_UI_TEXT = new Set([ "Memory from your chats", "This summary is regenerated periodically from your conversations.", "Save to supermemory", "Close", "Delete memory", "Edit", +]) + +const GROK_MEMORY_UI_ELEMENT_RULES = [ + { + selector: + "h1, h2, h3, h4, h5, h6, p, div, section, [role='heading'], [role='note']", + text: new Set([ + "Memory from your chats", + "This summary is regenerated periodically from your conversations.", + ]), + }, + { + selector: + "button, [role='button'], [role='menuitem'], [role='tab'], [tabindex]", + text: new Set(["Save to supermemory", "Close", "Delete memory", "Edit"]), + }, ] as const -function escapeRegExp(text: string) { - return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +interface GrokMemoryTextElement { + textContent: string | null + remove(): void } -function sanitizeGrokMemoryText(text: string) { - let sanitizedText = text +interface GrokMemoryTextRoot { + querySelectorAll(selectors: string): Iterable +} - for (const uiText of GROK_MEMORY_UI_TEXT) { - sanitizedText = sanitizedText.replace( - new RegExp(escapeRegExp(uiText), "g"), - "\n", - ) +export function removeGrokMemoryUiElements(root: GrokMemoryTextRoot) { + for (const rule of GROK_MEMORY_UI_ELEMENT_RULES) { + for (const element of root.querySelectorAll(rule.selector)) { + if (rule.text.has(element.textContent?.trim() || "")) { + element.remove() + } + } } +} - return sanitizedText +export function sanitizeGrokMemoryText(text: string) { + return text .split("\n") .map((line) => line.trim()) - .filter((line) => line) + .filter((line) => line && !GROK_MEMORY_UI_TEXT.has(line)) .join("\n") .trim() } @@ -299,6 +321,7 @@ function sanitizeGrokMemoryText(text: string) { function getGrokMemoryText(dialog: HTMLElement): string { const clonedDialog = dialog.cloneNode(true) as HTMLElement clonedDialog.querySelector("#supermemory-save-button")?.remove() + removeGrokMemoryUiElements(clonedDialog) const possibleMemoryContainers = Array.from( clonedDialog.querySelectorAll(