// ==UserScript== // @name Perplexity Context Memory Injector // @namespace http://evandro.dev.br/ // @version 1.6.0 // @description Gerenciador de memórias com interface nativa da Perplexity (Lexical Append + Safe Declarative Context). // @author Senior Frontend Lead // @match https://www.perplexity.ai/* // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // ==/UserScript== (function () { "use strict"; // --- Logging --- const DEBUG = true; const log = (action, data = "") => { if (DEBUG) console.log( `%c[PPLX-Memory::${action}]`, "color: #22d3ee; font-weight: bold;", data, ); }; log( "Boot", "Script 1.6.0 initialized. Native UI/UX + English translation applied.", ); // --- Global State --- let memories = GM_getValue("pplx_memories", []); let isPanelOpen = false; let isSubmitting = false; // --- Panel Setup (Shadow DOM) --- const container = document.createElement("div"); container.id = "pplx-memory-panel-container"; container.style.position = "fixed"; container.style.bottom = "24px"; container.style.left = "84px"; container.style.zIndex = "999999"; document.body.appendChild(container); const shadow = container.attachShadow({ mode: "open" }); // Perplexity-inspired design system colors and styles const styles = ` *, *::before, *::after { box-sizing: border-box; } :host { --pplx-bg-primary: #1f1f1f; /* Main background */ --pplx-bg-secondary: #282828; /* Input fields, hover states */ --pplx-fg-primary: #e8e8e8; /* Main text */ --pplx-fg-secondary: #a0a0a0; /* Hints, placeholders, icons */ --pplx-border: #3a3a3a; /* Subtle borders */ --pplx-accent: #22d3ee; /* Cyan accent for focus/active states */ --pplx-danger: #ef4444; /* Red for delete actions */ --pplx-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --pplx-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: var(--pplx-font-sans); font-size: 14px; color: var(--pplx-fg-primary); } /* Light mode override - though prints are dark, good practice to have */ @media (prefers-color-scheme: light) { :host { --pplx-bg-primary: #ffffff; --pplx-bg-secondary: #f5f5f5; --pplx-fg-primary: #111111; --pplx-fg-secondary: #666666; --pplx-border: #e0e0e0; } } .panel { display: flex; flex-direction: column; width: 450px; max-height: 650px; background: var(--pplx-bg-primary); border: 1px solid var(--pplx-border); border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.15); transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s; transform-origin: bottom left; } .panel.hidden { opacity: 0; transform: scale(0.98) translateY(10px); pointer-events: none; visibility: hidden; } header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--pplx-border); } header h2 { margin: 0; font-size: 16px; font-weight: 600; color: var(--pplx-fg-primary); } header .count { margin-left: 8px; font-size: 13px; color: var(--pplx-fg-secondary); font-weight: normal; } .memory-list { flex: 1; overflow-y: auto; padding: 0; margin: 0; list-style: none; } .memory-item { display: flex; gap: 16px; padding: 16px 20px; border-bottom: 1px solid var(--pplx-border); align-items: flex-start; transition: background 0.15s ease; } .memory-item:hover { background: var(--pplx-bg-secondary); } .memory-content { flex: 1; font-family: var(--pplx-font-mono); font-size: 13px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; color: var(--pplx-fg-primary); } .controls { display: flex; flex-direction: column; gap: 12px; align-items: center; padding-top: 2px; } .input-group { padding: 20px; border-top: 1px solid var(--pplx-border); display: flex; flex-direction: column; gap: 12px; background: var(--pplx-bg-primary); } textarea { width: 100%; height: 140px; resize: vertical; background: var(--pplx-bg-secondary); border: 1px solid transparent; color: var(--pplx-fg-primary); padding: 12px; border-radius: 8px; font-family: var(--pplx-font-mono); font-size: 13px; line-height: 1.5; transition: border-color 0.15s ease, box-shadow 0.15s ease; } textarea:focus-visible { outline: none; border-color: var(--pplx-accent); box-shadow: 0 0 0 1px var(--pplx-accent); } textarea::placeholder { color: var(--pplx-fg-secondary); opacity: 0.7; } .hint { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--pplx-fg-secondary); line-height: 1.4; } .hint svg { flex-shrink: 0; color: var(--pplx-accent); } button { background: transparent; border: none; color: var(--pplx-fg-secondary); padding: 8px; border-radius: 6px; cursor: pointer; font-weight: 500; transition: all 0.15s ease; display: inline-flex; align-items: center; justify-content: center; } button:hover { background: var(--pplx-bg-secondary); color: var(--pplx-fg-primary); } button.icon-close { padding: 6px; margin-right: -6px; } button.icon-delete:hover { color: var(--pplx-danger); background: rgba(239, 68, 68, 0.1); } button.primary { background: var(--pplx-accent); color: #000; /* Black text on cyan accent for contrast */ padding: 10px 16px; border-radius: 8px; font-weight: 600; width: 100%; justify-content: center; } button.primary:hover { opacity: 0.9; background: var(--pplx-accent); color: #000; } button.primary:active { transform: translateY(1px); } /* Custom Checkbox styling */ .checkbox-wrapper { position: relative; width: 18px; height: 18px; } input[type="checkbox"] { opacity: 0; width: 100%; height: 100%; position: absolute; top: 0; left: 0; margin: 0; cursor: pointer; z-index: 1; } .checkbox-styled { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: var(--pplx-bg-secondary); border: 1px solid var(--pplx-border); border-radius: 4px; transition: all 0.15s ease; display: flex; align-items: center; justify-content: center; } input[type="checkbox"]:checked + .checkbox-styled { background: var(--pplx-accent); border-color: var(--pplx-accent); } input[type="checkbox"]:focus-visible + .checkbox-styled { box-shadow: 0 0 0 2px var(--pplx-bg-primary), 0 0 0 4px var(--pplx-accent); } .checkbox-styled svg { color: #000; opacity: 0; transform: scale(0.8); transition: all 0.15s ease; } input[type="checkbox"]:checked + .checkbox-styled svg { opacity: 1; transform: scale(1); } /* Scrollbar styling */ .memory-list::-webkit-scrollbar { width: 8px; } .memory-list::-webkit-scrollbar-track { background: transparent; } .memory-list::-webkit-scrollbar-thumb { background: var(--pplx-border); border-radius: 4px; border: 2px solid var(--pplx-bg-primary); } .memory-list::-webkit-scrollbar-thumb:hover { background: var(--pplx-fg-secondary); } `; const renderPanel = () => { shadow.innerHTML = ``; const panel = document.createElement("section"); panel.className = `panel ${isPanelOpen ? "" : "hidden"}`; // Use standard HTML hidden attribute for better accessibility/state panel.hidden = !isPanelOpen; // --- Header --- const header = document.createElement("header"); const activeCount = memories.filter((m) => m.active).length; header.innerHTML = `
[SYSTEM] blocks. Avoid ambiguous separators (e.g., ---) or imperative "ignore" commands to prevent injection detection.
`;
const textarea = document.createElement("textarea");
// Updated placeholder with the consolidated safe prompt structure in English
textarea.placeholder = `[SYSTEM]
You will act as a Precision Technical Assistant, Senior Software Engineer, and Technical Writer proficient in Markdown.
API Temperature: 0.2 (code & facts) / 0.3 (conceptual analysis).
[ABSOLUTE CONSTRAINTS]
- Never invent data or references; state "no official documentation confirmed" when applicable.
- Never use acronyms without defining them on first occurrence.
- Never deliver responses without inline source citations: [Source: Name/DOI/URL].
- Never use entertainment news sources as a technical basis.
- Never generate code without first describing the existing code context provided.
- Never produce destructive operations without issuing an explicit warning block first.
...`;
const addBtn = document.createElement("button");
addBtn.className = "primary";
addBtn.textContent = "Add Safe Memory Block";
addBtn.onclick = () => {
const text = textarea.value.trim();
if (text) {
memories.push({ id: Date.now(), text, active: true });
GM_setValue("pplx_memories", memories);
textarea.value = "";
renderPanel();
// Scroll to bottom of list to show new item
setTimeout(() => {
const list = shadow.querySelector(".memory-list");
if (list) list.scrollTop = list.scrollHeight;
}, 0);
}
};
inputGroup.appendChild(hint);
inputGroup.appendChild(textarea);
inputGroup.appendChild(addBtn);
panel.appendChild(header);
panel.appendChild(ul);
panel.appendChild(inputGroup);
shadow.appendChild(panel);
};
// --- Native Sidebar Button Injection ---
const injectSidebarButton = () => {
if (document.getElementById("pplx-memory-toggle-btn")) return;
const userProfileContainer = document.querySelector(
".gap-md.py-sm.mt-auto",
);
if (!userProfileContainer) return;
const btn = document.createElement("a");
btn.id = "pplx-memory-toggle-btn";
// Updated classes to match current Perplexity sidebar button styles more closely
btn.className =
"reset interactable-alt p-sm gap-two group flex w-full flex-col items-center justify-center rounded-lg cursor-pointer";
btn.innerHTML = `
[${safeTag}]
${escapedMemoriesHtml}
`; editor.focus(); // Ensure cursor is at the absolute end const sel = window.getSelection(); if (sel.rangeCount > 0) { const range = sel.getRangeAt(0); range.collapse(false); } // Create Multi-MIME DataTransfer const dt = new DataTransfer(); dt.setData("text/plain", plainPayload); dt.setData("text/html", htmlPayload); // Dispatch Paste Event const pasteEvent = new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData: dt, }); editor.dispatchEvent(pasteEvent); log( "Inject", "Memory block appended with safe semantic framing (English).", ); // Trigger native submit setTimeout(() => { const submitBtn = document.querySelector( 'button[aria-label="Submit"]', ); if (submitBtn && !submitBtn.disabled) { submitBtn.click(); log("Submit", "Native Submit button triggered."); } else { log("Error", "Submit button not found or disabled."); } // Release lock setTimeout(() => { isSubmitting = false; }, 1000); }, 100); } }, { capture: true }, ); // Initial Boot renderPanel(); })();