// ==UserScript==
// @name Grok Prompt Manager
// @namespace grok.prompt.manager
// @version 1.0
// @description 4 prompt slots for Grok's Custom Instructions.
// @author Mr005K
// @match https://grok.com/*
// @match https://*.grok.com/*
// @match https://x.ai/*
// @match https://*.x.ai/*
// @license MIT
// @run-at document-idle
// @grant none
// ==/UserScript==
(function () {
"use strict";
const STORAGE_KEY = "grok-prompt-manager-v1";
const UI_ATTR = "data-grok-prompt-manager";
const DIALOG_ATTR = "data-gpm-installed";
const TA_ATTR = "data-gpm-bound";
const DEBUG = false;
const log = (...a) => DEBUG && console.log("[GrokPM]", ...a);
const DEFAULT_SLOTS = [
{ name: "Slot 1", text: "" },
{ name: "Slot 2", text: "" },
{ name: "Slot 3", text: "" },
{ name: "Slot 4", text: "" },
];
function loadSlots() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [...DEFAULT_SLOTS];
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length !== 4) return [...DEFAULT_SLOTS];
return parsed.map((s, i) => ({
name: typeof s?.name === "string" && s.name.trim() ? s.name : `Slot ${i + 1}`,
text: typeof s?.text === "string" ? s.text : "",
}));
} catch {
return [...DEFAULT_SLOTS];
}
}
function saveSlots(slots) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(slots));
} catch {
alert("Could not save slots (localStorage is blocked).");
}
}
function findByText(root, selector, text) {
const els = root.querySelectorAll(selector);
text = (text || "").trim().toLowerCase();
for (const el of els) {
const t = (el.textContent || "").trim().toLowerCase();
if (t.includes(text)) return el;
}
return null;
}
function getCustomTextarea(root = document) {
// Primary: the known placeholder
let ta = root.querySelector('textarea[placeholder="How should Grok behave?"]');
if (ta) return ta;
// Fallback: under "Custom Instructions"
const label = findByText(root, "label, div, p", "custom instructions");
if (label) {
const container = label.closest("div")?.parentElement || label.parentElement;
if (container) {
ta = container.querySelector("textarea");
if (ta) return ta;
}
}
// Last resort: first visible textarea in the dialog
const dialog = root.closest('[role="dialog"]') || root.querySelector('[role="dialog"]');
if (dialog) {
const candidates = [...dialog.querySelectorAll("textarea")].filter(isVisible);
if (candidates.length) return candidates[0];
}
return null;
}
function isCustomizeDialog(root) {
const header = findByText(root, "p, h2, h3, div", "customize grok's response");
const label = findByText(root, "label, div, p", "custom instructions");
return !!(header && label);
}
function isVisible(el) {
if (!el) return false;
const cs = getComputedStyle(el);
const r = el.getBoundingClientRect();
return cs.display !== "none" && cs.visibility !== "hidden" && r.width > 0 && r.height > 0;
}
function setTextareaValue(textarea, value) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value")?.set;
if (setter) setter.call(textarea, value);
else textarea.value = value;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
textarea.dispatchEvent(new Event("change", { bubbles: true }));
}
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
function buildUI(textarea) {
const slots = loadSlots();
const ui = document.createElement("div");
ui.setAttribute(UI_ATTR, "1");
ui.className =
"flex flex-col gap-2 p-2 rounded-2xl ring-1 ring-inset ring-toggle-border hover:ring-card-border-focus bg-button-ghost-hover";
ui.style.marginBottom = "8px";
ui.innerHTML = `
<div class="flex items-center justify-between gap-2">
<div class="text-sm font-semibold">Prompt Manager</div>
<div class="text-xs text-secondary">Insert into the box below. Use Grok’s own <b>Save</b> after.</div>
</div>
<div class="flex flex-wrap gap-2" role="tablist" aria-label="Prompt Slots">
${slots
.map(
(s, i) => `
<button data-slot="${i}" data-role="slot-btn"
class="inline-flex items-center justify-start gap-2 text-sm rounded-2xl px-3 py-2 ring-1 ring-inset ring-toggle-border hover:bg-card-hover text-primary">
<span class="truncate max-w-[9rem]">${escapeHtml(s.name || `Slot ${i + 1}`)}</span>
</button>`
)
.join("")}
</div>
<div data-role="editor" class="flex flex-col gap-2 hidden mt-2 rounded-xl border border-border-l2 p-2">
<div class="flex items-center gap-2">
<label class="text-xs text-secondary">Name</label>
<input data-role="name" class="flex-1 text-sm bg-transparent rounded-xl border border-border-l2 px-2 py-1" placeholder="Slot name">
</div>
<textarea data-role="text" class="w-full text-sm bg-transparent rounded-xl border border-border-l2 p-2" style="min-height: 120px;" placeholder="Saved prompt here..."></textarea>
<div class="flex flex-wrap gap-2 justify-end">
<button data-action="import" class="text-xs rounded-xl px-3 py-2 ring-1 ring-inset ring-toggle-border hover:bg-card-hover">Import Current</button>
<button data-action="clear" class="text-xs rounded-xl px-3 py-2 ring-1 ring-inset ring-toggle-border hover:bg-card-hover">Clear</button>
<div class="flex-1"></div>
<button data-action="save" class="text-xs rounded-xl px-3 py-2 bg-button-filled text-fg-invert hover:bg-button-filled-hover">Save Slot</button>
<button data-action="insert" class="text-xs rounded-xl px-3 py-2 bg-button-filled text-fg-invert hover:bg-button-filled-hover">Insert into Custom</button>
</div>
</div>
`;
// Wire up editor
const editor = ui.querySelector('[data-role="editor"]');
const nameInput = editor.querySelector('[data-role="name"]');
const textInput = editor.querySelector('[data-role="text"]');
let currentSlots = slots;
let openIndex = null;
ui.addEventListener("click", (e) => {
const slotBtn = e.target.closest("[data-role='slot-btn']");
if (slotBtn) {
openIndex = Number(slotBtn.getAttribute("data-slot"));
const s = currentSlots[openIndex] || {};
nameInput.value = s.name || `Slot ${openIndex + 1}`;
textInput.value = s.text || "";
editor.classList.remove("hidden");
ui.querySelectorAll("[data-role='slot-btn']").forEach((b) => {
const active = Number(b.getAttribute("data-slot")) === openIndex;
b.classList.toggle("bg-button-ghost-hover", active);
b.classList.toggle("ring-card-border-focus", active);
});
return;
}
const act = e.target.closest("[data-action]");
if (!act) return;
const action = act.getAttribute("data-action");
if (action === "import") {
textInput.value = textarea.value || "";
} else if (action === "clear") {
nameInput.value = currentSlots[openIndex]?.name || `Slot ${openIndex + 1}`;
textInput.value = "";
} else if (action === "save") {
if (openIndex == null) return;
const n = (nameInput.value || "").trim() || `Slot ${openIndex + 1}`;
currentSlots[openIndex] = { name: n, text: textInput.value || "" };
saveSlots(currentSlots);
const labelSpan = ui.querySelector(`[data-role='slot-btn'][data-slot='${openIndex}'] span`);
if (labelSpan) labelSpan.textContent = n;
flash(act);
} else if (action === "insert") {
if (openIndex == null) return;
setTextareaValue(textarea, textInput.value || "");
flash(act);
}
});
function flash(btn) {
btn.animate([{ opacity: 0.4 }, { opacity: 1 }], { duration: 180, easing: "ease-out" });
}
return ui;
}
// Debounced attach routine
let rafToken = null;
function scheduleAttach() {
if (rafToken) cancelAnimationFrame(rafToken);
rafToken = requestAnimationFrame(tryAttach);
}
let pollTimer = setInterval(scheduleAttach, 1200);
const observer = new MutationObserver(scheduleAttach);
observer.observe(document.documentElement, { childList: true, subtree: true });
function tryAttach() {
rafToken = null;
// Find visible dialogs
const dialogs = [...document.querySelectorAll('[role="dialog"]')].filter(isVisible);
let installedAny = false;
for (const dlg of dialogs) {
if (!isCustomizeDialog(dlg)) continue;
// If already installed in this dialog, skip
if (dlg.hasAttribute(DIALOG_ATTR) || dlg.querySelector(`[${UI_ATTR}]`)) {
continue;
}
const ta = getCustomTextarea(dlg);
if (!ta || !isVisible(ta)) continue;
// Guard on the textarea as well
if (ta.hasAttribute(TA_ATTR)) continue;
// Build & insert UI directly before the textarea (lowest-risk spot).
const ui = buildUI(ta);
const parent = ta.parentElement || dlg;
parent.insertBefore(ui, ta);
// Mark installed
dlg.setAttribute(DIALOG_ATTR, "1");
ta.setAttribute(TA_ATTR, "1");
installedAny = true;
log("Installed Prompt Manager.");
}
// If we successfully installed at least once, stop the poller (observer remains).
if (installedAny && pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
// First run
scheduleAttach();
})();