公开
版本 1.0
最新pplx提示词脚本
描述
youhou
提示词内容
// ==UserScript==
// @name Gemini / Genspark / Perplexity 提示词管理器
// @namespace http://tampermonkey.net/
// @version 2.2.0
// @description 为 Gemini、Genspark 与 Perplexity 添加提示词管理功能,支持增删改查和快速插入
// @author Your Name
// @match https://gemini.google.com/*
// @match https://www.genspark.ai/agents*
// @match https://genspark.ai/agents*
// @match https://www.perplexity.ai/*
// @match https://perplexity.ai/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
// 防止重复初始化
if (window.promptManagerInitialized) return;
window.promptManagerInitialized = true;
// 站点识别
const isGemini = location.hostname.includes("gemini.google.com");
const isGenspark = location.hostname.includes("genspark.ai");
const isPerplexity = location.hostname.includes("perplexity.ai");
// 主题
const THEME = isGemini
? { g1: "#4285f4", g2: "#34a853", name: "Gemini" }
: isGenspark
? { g1: "#667eea", g2: "#764ba2", name: "Genspark" }
: isPerplexity
? { g1: "#0ea5e9", g2: "#8b5cf6", name: "Perplexity" }
: { g1: "#3b82f6", g2: "#9333ea", name: "Site" };
// 默认提示词
const DEFAULT_PROMPTS = [
{
id: "default_1",
title: "代码优化",
content: "请帮我优化以下代码,提高性能和可读性:",
category: "编程",
},
{
id: "default_2",
title: "翻译助手",
content: "请将以下内容翻译成中文,保持专业术语的准确性:",
category: "翻译",
},
{
id: "default_3",
title: "文章总结",
content: "请帮我总结以下文章的要点,用简洁的语言概括主要内容:",
category: "写作",
},
{
id: "default_4",
title: "创意写作",
content: "请基于以下主题进行创意写作,要求生动有趣:",
category: "写作",
},
{
id: "default_5",
title: "问题解答",
content: "请详细解答以下问题,提供准确和全面的信息:",
category: "学习",
},
];
// 安全创建元素
function createElementSafely(tag, props = {}, text = "") {
const el = document.createElement(tag);
Object.keys(props).forEach((k) => {
if (k === "className") el.className = props[k];
else if (k === "style") el.setAttribute("style", props[k]);
else el.setAttribute(k, props[k]);
});
if (text) el.textContent = text;
return el;
}
function clearElementSafely(el) {
while (el.firstChild) el.removeChild(el.firstChild);
}
class UniversalPromptManager {
// 获取仍然挂在 DOM 上的当前输入框;若失效则重找
getLiveEditorEl() {
if (this.textarea && document.body.contains(this.textarea))
return this.textarea;
// 优先 Perplexity 的 #ask-input,其次兜底选择器
const el =
document.querySelector('#ask-input[contenteditable="true"]') ||
document.querySelector(
'div#ask-input[contenteditable="true"][data-lexical-editor="true"]'
) ||
document.querySelector('div[contenteditable="true"][role="textbox"]') ||
document.querySelector("textarea");
if (el) this.textarea = el;
return el;
}
constructor() {
this.prompts = this.loadPrompts();
this.selectedPrompt = null;
this.textarea = null; // textarea 或 contenteditable
this.isCollapsed = false;
this.site = isGemini
? "gemini"
: isGenspark
? "genspark"
: isPerplexity
? "perplexity"
: "unknown";
this._pmObserver = null;
this._pmRouteHooked = false;
this._pmFocusHooked = false;
this.init();
}
// 加载/保存
loadPrompts() {
const saved = GM_getValue("universal_prompts", null);
if (!saved) {
GM_setValue("universal_prompts", DEFAULT_PROMPTS);
return DEFAULT_PROMPTS;
}
return saved;
}
savePrompts() {
GM_setValue("universal_prompts", this.prompts);
}
// CRUD
addPrompt(p) {
p.id = "custom_" + Date.now();
this.prompts.push(p);
this.savePrompts();
this.refreshPromptList();
}
updatePrompt(id, u) {
const i = this.prompts.findIndex((p) => p.id === id);
if (i !== -1) {
this.prompts[i] = { ...this.prompts[i], ...u };
this.savePrompts();
this.refreshPromptList();
}
}
deletePrompt(id) {
this.prompts = this.prompts.filter((p) => p.id !== id);
this.savePrompts();
this.refreshPromptList();
}
getCategories() {
return Array.from(
new Set(this.prompts.map((p) => p.category).filter(Boolean))
);
}
// 初始化
init() {
this.createStyles();
this.createUI();
this.bindEvents();
this.findTextarea();
}
// 样式
createStyles() {
const old = document.getElementById("universal-prompt-manager-styles");
if (old) old.remove();
const style = document.createElement("style");
style.id = "universal-prompt-manager-styles";
style.textContent = `
#universal-prompt-panel{position:fixed;top:50%;right:20px;transform:translateY(-50%);width:320px;max-height:70vh;background:#fff;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.15);z-index:999999;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;transition:.3s;border:1px solid #e0e0e0}
#universal-prompt-panel.collapsed{display:none}
.prompt-panel-header{padding:16px;background:linear-gradient(135deg,${
THEME.g1
} 0%,${
THEME.g2
} 100%);color:#fff;border-radius:12px 12px 0 0;display:flex;justify-content:space-between;align-items:center;cursor:move}
.prompt-panel-title{font-size:16px;font-weight:600;display:flex;align-items:center;gap:8px}
.site-indicator{font-size:12px;padding:2px 6px;background:rgba(255,255,255,.2);border-radius:4px;margin-left:8px}
.prompt-panel-controls{display:flex;gap:8px}
.prompt-panel-btn{background:rgba(255,255,255,.2);border:none;color:#fff;width:28px;height:28px;border-radius:6px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.2s;font-size:14px}
.prompt-panel-btn:hover{background:rgba(255,255,255,.3);transform:scale(1.1)}
.prompt-search-bar{padding:12px;border-bottom:1px solid #e5e7eb;background:#f9fafb}
.prompt-search-input{width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;transition:.2s;box-sizing:border-box}
.prompt-search-input:focus{outline:0;border-color:${
THEME.g1
};box-shadow:0 0 0 3px rgba(16,185,129,.1)}
.prompt-categories{padding:8px 12px;display:flex;gap:6px;flex-wrap:wrap;background:#fff;border-bottom:1px solid #e5e7eb}
.category-tag{padding:4px 10px;background:#f3f4f6;border-radius:12px;font-size:12px;color:#4b5563;cursor:pointer;transition:.2s;border:1px solid transparent}
.category-tag:hover{background:#e5e7eb}
.category-tag.active{background:${THEME.g1};color:#fff;border-color:${
THEME.g1
}}
.prompt-list{flex:1;overflow-y:auto;padding:8px}
.prompt-item{background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:12px;margin-bottom:8px;cursor:pointer;transition:.2s;position:relative}
.prompt-item:hover{border-color:${
THEME.g1
};box-shadow:0 4px 12px rgba(0,0,0,.08);transform:translateY(-2px)}
.prompt-item.selected{background:linear-gradient(135deg,rgba(232,240,254,.6) 0%,rgba(241,248,233,.6) 100%);border-color:${
THEME.g1
}}
.prompt-item-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px}
.prompt-item-title{font-weight:600;font-size:14px;color:#1f2937;flex:1}
.prompt-item-category{font-size:11px;padding:2px 6px;background:#f3f4f6;border-radius:4px;color:#6b7280}
.prompt-item-content{font-size:13px;color:#6b7280;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.prompt-item-actions{position:absolute;top:8px;right:8px;display:none;gap:4px}
.prompt-item:hover .prompt-item-actions{display:flex}
.prompt-action-btn{width:24px;height:24px;border:none;background:#fff;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.2s;box-shadow:0 1px 3px rgba(0,0,0,.1);font-size:12px}
.prompt-action-btn:hover{background:#f3f4f6;transform:scale(1.1)}
.add-prompt-btn{margin:12px;padding:10px;background:linear-gradient(135deg,${
THEME.g1
} 0%,${
THEME.g2
} 100%);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:.2s;display:flex;align-items:center;justify-content:center;gap:6px}
.add-prompt-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.15)}
.prompt-modal{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:1000000;animation:fadeIn .2s}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
.prompt-modal-content{background:#fff;border-radius:12px;width:90%;max-width:500px;padding:24px;animation:slideUp .3s}
@keyframes slideUp{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
.prompt-modal-header{font-size:18px;font-weight:600;margin-bottom:20px;color:#1f2937}
.prompt-form-group{margin-bottom:16px}
.prompt-form-label{display:block;font-size:14px;font-weight:500;color:#374151;margin-bottom:6px}
.prompt-form-input,.prompt-form-textarea{width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:14px;transition:.2s;box-sizing:border-box}
.prompt-form-textarea{min-height:100px;resize:vertical;font-family:inherit}
.prompt-form-input:focus,.prompt-form-textarea:focus{outline:0;border-color:${
THEME.g1
};box-shadow:0 0 0 3px rgba(66,133,244,.1)}
.prompt-modal-actions{display:flex;gap:12px;justify-content:flex-end;margin-top:24px}
.prompt-modal-btn{padding:8px 16px;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;transition:.2s;border:none}
.prompt-modal-btn.primary{background:linear-gradient(135deg,${
THEME.g1
} 0%,${THEME.g2} 100%);color:#fff}
.prompt-modal-btn.secondary{background:#f3f4f6;color:#4b5563}
.prompt-modal-btn:hover{transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,.15)}
.selected-prompt-bar{position:fixed;bottom:120px;left:50%;transform:translateX(-50%);background:linear-gradient(135deg,${
THEME.g1
} 0%,${
THEME.g2
} 100%);color:#fff;padding:8px 16px;border-radius:20px;font-size:13px;display:none;align-items:center;gap:8px;box-shadow:0 4px 12px rgba(0,0,0,.15);z-index:999998;animation:slideInUp .3s}
@keyframes slideInUp{from{transform:translate(-50%,20px);opacity:0}to{transform:translate(-50%,0);opacity:1}}
.selected-prompt-bar.show{display:flex}
.selected-prompt-text{max-width:300px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.clear-prompt-btn{background:rgba(255,255,255,.2);border:none;color:#fff;width:20px;height:20px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.2s;font-size:14px}
.clear-prompt-btn:hover{background:rgba(255,255,255,.3);transform:scale(1.1)}
.prompt-list::-webkit-scrollbar{width:6px}.prompt-list::-webkit-scrollbar-track{background:#f3f4f6;border-radius:3px}.prompt-list::-webkit-scrollbar-thumb{background:#9ca3af;border-radius:3px}.prompt-list::-webkit-scrollbar-thumb:hover{background:#6b7280}
.quick-prompt-btn{position:fixed;bottom:180px;right:30px;width:48px;height:48px;background:linear-gradient(135deg,${
THEME.g1
} 0%,${
THEME.g2
} 100%);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:20px;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,.15);transition:.3s;z-index:999997;border:none}
.quick-prompt-btn:hover{transform:scale(1.1);box-shadow:0 6px 20px rgba(0,0,0,.2)}
.quick-prompt-btn.hidden{display:none}
${
isGemini
? `
.ql-editor{min-height:80px!important;max-height:200px!important;line-height:1.5!important;padding:12px!important;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif!important;font-size:14px!important}
`
: ""
}
${
isGenspark || isPerplexity
? `
textarea{min-height:80px!important;max-height:200px!important;resize:vertical!important;line-height:1.5!important;padding:12px!important;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif!important;font-size:14px!important}
`
: ""
}
`;
document.head.appendChild(style);
}
// UI
createUI() {
const ep = document.getElementById("universal-prompt-panel");
const eb = document.querySelector(".selected-prompt-bar");
const qb = document.querySelector(".quick-prompt-btn");
if (ep) ep.remove();
if (eb) eb.remove();
if (qb) qb.remove();
const panel = createElementSafely("div", {
id: "universal-prompt-panel",
});
const header = createElementSafely("div", {
className: "prompt-panel-header",
});
const title = createElementSafely("div", {
className: "prompt-panel-title",
});
title.appendChild(createElementSafely("span", {}, "📝"));
title.appendChild(createElementSafely("span", {}, "提示词管理"));
title.appendChild(
createElementSafely("span", { className: "site-indicator" }, THEME.name)
);
const controls = createElementSafely("div", {
className: "prompt-panel-controls",
});
const refreshBtn = createElementSafely(
"button",
{ className: "prompt-panel-btn", id: "refresh-prompts", title: "刷新" },
"⟳"
);
const toggleBtn = createElementSafely(
"button",
{ className: "prompt-panel-btn", id: "toggle-panel", title: "收起" },
"−"
);
controls.appendChild(refreshBtn);
controls.appendChild(toggleBtn);
header.appendChild(title);
header.appendChild(controls);
const searchBar = createElementSafely("div", {
className: "prompt-search-bar",
});
const searchInput = createElementSafely("input", {
className: "prompt-search-input",
id: "prompt-search",
type: "text",
placeholder: "搜索提示词...",
});
searchBar.appendChild(searchInput);
const categories = createElementSafely("div", {
className: "prompt-categories",
id: "prompt-categories",
});
const list = createElementSafely("div", {
className: "prompt-list",
id: "prompt-list",
});
const addBtn = createElementSafely("button", {
className: "add-prompt-btn",
id: "add-prompt",
});
addBtn.appendChild(createElementSafely("span", {}, "+"));
addBtn.appendChild(createElementSafely("span", {}, "添加新提示词"));
panel.appendChild(header);
panel.appendChild(searchBar);
panel.appendChild(categories);
panel.appendChild(list);
panel.appendChild(addBtn);
document.body.appendChild(panel);
const selectedBar = createElementSafely("div", {
className: "selected-prompt-bar",
});
selectedBar.appendChild(createElementSafely("span", {}, "当前提示词:"));
selectedBar.appendChild(
createElementSafely("span", {
className: "selected-prompt-text",
id: "selected-prompt-text",
})
);
const clearBtn = createElementSafely(
"button",
{ className: "clear-prompt-btn", id: "clear-prompt" },
"×"
);
selectedBar.appendChild(clearBtn);
document.body.appendChild(selectedBar);
const quickBtn = createElementSafely(
"button",
{
className: "quick-prompt-btn",
title: `打开提示词管理器 (${THEME.name})`,
},
"📝"
);
quickBtn.addEventListener("click", () => this.togglePanel());
document.body.appendChild(quickBtn);
this.refreshCategories();
this.refreshPromptList();
}
togglePanel() {
const panel = document.getElementById("universal-prompt-panel");
const quickBtn = document.querySelector(".quick-prompt-btn");
const toggleBtn = document.getElementById("toggle-panel");
this.isCollapsed = !this.isCollapsed;
if (this.isCollapsed) {
panel.classList.add("collapsed");
quickBtn.classList.remove("hidden");
if (toggleBtn) toggleBtn.textContent = "+";
} else {
panel.classList.remove("collapsed");
quickBtn.classList.add("hidden");
if (toggleBtn) toggleBtn.textContent = "−";
}
}
refreshCategories() {
const container = document.getElementById("prompt-categories");
if (!container) return;
clearElementSafely(container);
const allTag = createElementSafely(
"span",
{ className: "category-tag active", "data-category": "all" },
"全部"
);
container.appendChild(allTag);
this.getCategories().forEach((cat) =>
container.appendChild(
createElementSafely(
"span",
{ className: "category-tag", "data-category": cat },
cat
)
)
);
}
refreshPromptList(filter = "") {
const container = document.getElementById("prompt-list");
if (!container) return;
const activeCategory =
document.querySelector(".category-tag.active")?.dataset.category ||
"all";
let arr = this.prompts.slice();
if (activeCategory !== "all")
arr = arr.filter((p) => p.category === activeCategory);
if (filter)
arr = arr.filter(
(p) =>
p.title.toLowerCase().includes(filter.toLowerCase()) ||
p.content.toLowerCase().includes(filter.toLowerCase())
);
clearElementSafely(container);
if (!arr.length) {
container.appendChild(
createElementSafely(
"div",
{ style: "text-align:center;padding:20px;color:#9ca3af;" },
"暂无提示词"
)
);
return;
}
arr.forEach((prompt) => {
const item = createElementSafely("div", { className: "prompt-item" });
if (this.selectedPrompt?.id === prompt.id)
item.classList.add("selected");
const head = createElementSafely("div", {
className: "prompt-item-header",
});
head.appendChild(
createElementSafely(
"div",
{ className: "prompt-item-title" },
prompt.title
)
);
head.appendChild(
createElementSafely(
"span",
{ className: "prompt-item-category" },
prompt.category || "未分类"
)
);
const content = createElementSafely(
"div",
{ className: "prompt-item-content" },
prompt.content
);
const actions = createElementSafely("div", {
className: "prompt-item-actions",
});
const editBtn = createElementSafely(
"button",
{
className: "prompt-action-btn edit-prompt",
"data-id": prompt.id,
title: "编辑",
},
"✏"
);
const delBtn = createElementSafely(
"button",
{
className: "prompt-action-btn delete-prompt",
"data-id": prompt.id,
title: "删除",
},
"🗑"
);
actions.appendChild(editBtn);
actions.appendChild(delBtn);
item.appendChild(head);
item.appendChild(content);
item.appendChild(actions);
item.addEventListener("click", (e) => {
if (!e.target.closest(".prompt-item-actions"))
this.selectPrompt(prompt, item);
});
container.appendChild(item);
});
}
selectPrompt(prompt, el) {
this.selectedPrompt = prompt;
document
.querySelectorAll(".prompt-item")
.forEach((i) => i.classList.remove("selected"));
el.classList.add("selected");
const bar = document.querySelector(".selected-prompt-bar");
const txt = document.getElementById("selected-prompt-text");
if (bar && txt) {
txt.textContent = prompt.title;
bar.classList.add("show");
}
this.insertPromptToTextarea(prompt.content);
this.showToast(`已选择提示词: ${prompt.title}`);
}
insertPromptToTextarea(text) {
if (!this.textarea) this.findTextarea();
const el = this.textarea;
if (!el) return this.showToast("未找到输入框,请刷新页面重试");
if (isGemini) return this.insertToGemini(text);
if (isGenspark) return this.insertAsTextarea(text);
if (isPerplexity) return this.insertToPerplexity(text);
// 兜底
if (el.tagName && el.tagName.toLowerCase() === "textarea")
return this.insertAsTextarea(text);
return this.insertAsContentEditable(text);
}
// Gemini(contenteditable)
insertToGemini(promptContent) {
const editor = this.textarea;
const current = (editor.textContent || "").trim();
const newContent = !current
? promptContent + "\n\n"
: promptContent + "\n\n" + current;
editor.textContent = "";
editor.focus();
setTimeout(() => {
editor.textContent = newContent;
editor.dispatchEvent(new Event("input", { bubbles: true }));
try {
const range = document.createRange(),
sel = getSelection();
if (editor.firstChild) {
range.setStart(editor.firstChild, promptContent.length + 2);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
} catch {}
editor.focus();
}, 50);
}
// Perplexity(contenteditable: #ask-input)——像用户输入一样插入
insertToPerplexity(promptContent) {
const el = this.getLiveEditorEl();
if (!el) {
this.showToast("未找到输入框");
return;
}
const text = promptContent + "\n\n";
// textarea 直接用通用逻辑
if (el.tagName && el.tagName.toLowerCase() === "textarea")
return this.insertAsTextarea(promptContent);
try {
el.focus();
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(true); // 开头插入;如需结尾,改为 false
sel.removeAllRanges();
sel.addRange(range);
let ok = false;
if (document.execCommand)
ok = document.execCommand("insertText", false, text);
if (!ok) el.textContent = text + (el.textContent || "");
// 通知框架
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
} catch (e) {
// 最终兜底
el.textContent = text + (el.textContent || "");
el.dispatchEvent(new Event("input", { bubbles: true }));
}
}
// 通用:任意 contenteditable 兜底
insertAsContentEditable(promptContent) {
const el = this.textarea;
if (!el) return;
const text = promptContent + "\n\n";
try {
el.focus();
const sel = getSelection(),
range = document.createRange();
range.selectNodeContents(el);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
let ok = false;
if (document.execCommand)
ok = document.execCommand("insertText", false, text);
if (!ok) el.textContent = text + (el.textContent || "");
el.dispatchEvent(new Event("input", { bubbles: true }));
} catch {
el.textContent = text + (el.textContent || "");
el.dispatchEvent(new Event("input", { bubbles: true }));
}
}
// 通用:textarea
insertAsTextarea(promptContent) {
const ta = this.textarea;
const current = (ta.value || "").trim();
ta.value = !current
? promptContent + "\n\n"
: promptContent + "\n\n" + current;
this.adjustTextareaHeight();
ta.dispatchEvent(new Event("input", { bubbles: true }));
const pos = promptContent.length + 2;
try {
ta.setSelectionRange(pos, pos);
} catch {}
ta.focus();
}
adjustTextareaHeight() {
const el = this.textarea;
if (!el) return;
const isTA = el.tagName && el.tagName.toLowerCase() === "textarea";
if (isTA) {
el.style.height = "auto";
el.style.height = Math.min(el.scrollHeight, 200) + "px";
}
}
clearSelectedPrompt() {
this.selectedPrompt = null;
document.querySelector(".selected-prompt-bar")?.classList.remove("show");
document
.querySelectorAll(".prompt-item")
.forEach((i) => i.classList.remove("selected"));
// 关键:拿到“仍然在页面上的”输入框
const el = this.getLiveEditorEl();
if (!el) {
this.showToast("未找到输入框");
return;
}
const isTA = el.tagName && el.tagName.toLowerCase() === "textarea";
if (isTA) {
el.value = "";
this.adjustTextareaHeight();
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
return;
}
// contenteditable(Perplexity / Lexical)
const hardReset = () => {
// Lexical 的空内容占位:一个空段落
el.innerHTML = '<p dir="auto"><br></p>';
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
// 放回光标到开头
try {
el.focus();
const sel = getSelection(),
r = document.createRange();
r.selectNodeContents(el);
r.collapse(true);
sel.removeAllRanges();
sel.addRange(r);
} catch {}
};
try {
el.focus();
// 像用户一样:全选 → 删除
const sel = getSelection();
const r = document.createRange();
r.selectNodeContents(el);
sel.removeAllRanges();
sel.addRange(r);
// 先尝试 beforeinput/input(很多富文本靠这个驱动)
const fire = (type, inputType) =>
el.dispatchEvent(new InputEvent(type, { bubbles: true, inputType }));
let acted =
fire("beforeinput", "deleteContent") | fire("input", "deleteContent");
// 再尝试 execCommand(很多编辑器仍兼容)
if (document.execCommand) {
acted = document.execCommand("delete", false, null) || acted;
}
// 若都没效果,做硬重置(最稳)
if (!acted) hardReset();
// 有些框架会立刻“回写”旧内容,这里再保险一次(50ms 后再清一遍)
setTimeout(() => {
if (el.textContent && el.textContent.trim() !== "") hardReset();
}, 50);
} catch {
hardReset();
}
}
showEditModal(prompt = null) {
const isEdit = !!prompt;
const modal = createElementSafely("div", { className: "prompt-modal" });
const mc = createElementSafely("div", {
className: "prompt-modal-content",
});
const mh = createElementSafely(
"div",
{ className: "prompt-modal-header" },
isEdit ? "编辑提示词" : "添加新提示词"
);
const g1 = createElementSafely("div", { className: "prompt-form-group" });
g1.appendChild(
createElementSafely("label", { className: "prompt-form-label" }, "标题")
);
const ti = createElementSafely("input", {
className: "prompt-form-input",
id: "prompt-title",
type: "text",
value: isEdit ? prompt.title : "",
});
g1.appendChild(ti);
const g2 = createElementSafely("div", { className: "prompt-form-group" });
g2.appendChild(
createElementSafely("label", { className: "prompt-form-label" }, "分类")
);
const ci = createElementSafely("input", {
className: "prompt-form-input",
id: "prompt-category",
type: "text",
value: isEdit ? prompt.category || "" : "",
placeholder: "例如:编程、翻译、写作",
});
g2.appendChild(ci);
const g3 = createElementSafely("div", { className: "prompt-form-group" });
g3.appendChild(
createElementSafely(
"label",
{ className: "prompt-form-label" },
"提示词内容"
)
);
const ta = createElementSafely("textarea", {
className: "prompt-form-textarea",
id: "prompt-content",
});
ta.value = isEdit ? prompt.content : "";
g3.appendChild(ta);
const acts = createElementSafely("div", {
className: "prompt-modal-actions",
});
const cancel = createElementSafely(
"button",
{ className: "prompt-modal-btn secondary", id: "cancel-modal" },
"取消"
);
const save = createElementSafely(
"button",
{ className: "prompt-modal-btn primary", id: "save-prompt" },
isEdit ? "保存" : "添加"
);
acts.appendChild(cancel);
acts.appendChild(save);
mc.appendChild(mh);
mc.appendChild(g1);
mc.appendChild(g2);
mc.appendChild(g3);
mc.appendChild(acts);
modal.appendChild(mc);
document.body.appendChild(modal);
cancel.addEventListener("click", () => modal.remove());
save.addEventListener("click", () => {
const title = ti.value.trim();
const category = ci.value.trim();
const content = ta.value.trim();
if (!title || !content) return alert("请填写标题和内容");
if (isEdit) {
this.updatePrompt(prompt.id, { title, category, content });
this.showToast("提示词已更新");
} else {
this.addPrompt({ title, category, content });
this.showToast("提示词已添加");
}
this.refreshCategories();
modal.remove();
});
modal.addEventListener("click", (e) => {
if (e.target === modal) modal.remove();
});
const esc = (e) => {
if (e.key === "Escape") {
modal.remove();
document.removeEventListener("keydown", esc);
}
};
document.addEventListener("keydown", esc);
}
showToast(msg) {
const t = createElementSafely("div", { className: "prompt-toast" }, msg);
t.style.position = "fixed";
t.style.top = "20px";
t.style.left = "50%";
t.style.transform = "translateX(-50%)";
t.style.background = "#10b981";
t.style.color = "#fff";
t.style.padding = "12px 20px";
t.style.borderRadius = "8px";
t.style.fontSize = "14px";
t.style.boxShadow = "0 4px 12px rgba(0,0,0,.15)";
t.style.zIndex = 1000001;
document.body.appendChild(t);
setTimeout(() => {
t.style.opacity = "0";
setTimeout(() => t.remove(), 300);
}, 2000);
}
// ——★ 关键:更稳健的输入框捕获(含 Perplexity:#ask-input)★——
findTextarea() {
const isInputLike = (el) => {
if (!el) return false;
const cs = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
const visible =
el.offsetParent !== null &&
cs.visibility !== "hidden" &&
cs.display !== "none" &&
rect.width > 0 &&
rect.height > 0;
const isTA = el.tagName && el.tagName.toLowerCase() === "textarea";
const isCE =
el.getAttribute && el.getAttribute("contenteditable") === "true";
return visible && (isTA || isCE);
};
const selectorGroups = [
// Perplexity 特异选择器(你页上就是它)
'#ask-input[contenteditable="true"]',
'div#ask-input[contenteditable="true"][data-lexical-editor="true"]',
'div[contenteditable="true"][id="ask-input"][role="textbox"]',
// 常规优先
"form textarea",
'textarea[placeholder*="Ask"]',
'textarea[placeholder*="Message"]',
'textarea[aria-label*="Ask"]',
'textarea[aria-label*="Message"]',
'textarea[data-testid*="search"]',
"textarea",
// ContentEditable 兜底
'.ql-editor[contenteditable="true"]',
"rich-textarea .ql-editor",
".text-input-field_textarea .ql-editor",
'div[contenteditable="true"][data-lexical-editor="true"]',
'div[contenteditable="true"][role="textbox"]',
'div[contenteditable="true"]',
];
const pickBest = (nodes) => {
const viewH =
window.innerHeight || document.documentElement.clientHeight;
let best = null,
score = -Infinity;
nodes.forEach((el) => {
if (!isInputLike(el)) return;
const rect = el.getBoundingClientRect();
const s = viewH - rect.top + rect.width * 0.001; // 越靠近底部越大
if (s > score) {
score = s;
best = el;
}
});
return best;
};
let candidates = [];
for (const sel of selectorGroups)
candidates = candidates.concat(
Array.from(document.querySelectorAll(sel))
);
const target = pickBest(candidates);
if (target) {
this.textarea = target;
console.log(`找到 ${this.site} 文本框:`, target);
if (
this.textarea.tagName &&
this.textarea.tagName.toLowerCase() === "textarea"
) {
this.textarea.addEventListener("input", () =>
this.adjustTextareaHeight()
);
this.adjustTextareaHeight();
}
if (!this._pmObserver) {
this._pmObserver = new MutationObserver(() => {
if (!this.textarea || !document.body.contains(this.textarea)) {
this.textarea = null;
setTimeout(() => this.findTextarea(), 200);
}
});
this._pmObserver.observe(document.body, {
childList: true,
subtree: true,
});
}
return true;
}
// 路由变化重试
if (!this._pmRouteHooked) {
this._pmRouteHooked = true;
const _push = history.pushState;
history.pushState = function () {
const r = _push.apply(this, arguments);
setTimeout(() => dispatchEvent(new Event("pm-route-changed")), 100);
return r;
};
addEventListener("popstate", () =>
setTimeout(() => dispatchEvent(new Event("pm-route-changed")), 100)
);
addEventListener("pm-route-changed", () =>
setTimeout(() => this.findTextarea(), 200)
);
}
// 手动聚焦捕获
if (!this._pmFocusHooked) {
this._pmFocusHooked = true;
document.addEventListener(
"focusin",
(e) => {
const el = e.target;
if (isInputLike(el)) {
this.textarea = el;
console.log("已通过用户聚焦捕获到输入框:", el);
this.showToast("已捕获到输入框");
}
},
true
);
}
setTimeout(() => this.findTextarea(), 800);
return false;
}
// 事件绑定
bindEvents() {
const searchInput = document.getElementById("prompt-search");
if (searchInput)
searchInput.addEventListener("input", (e) =>
this.refreshPromptList(e.target.value)
);
const categories = document.getElementById("prompt-categories");
if (categories)
categories.addEventListener("click", (e) => {
if (e.target.classList.contains("category-tag")) {
document
.querySelectorAll(".category-tag")
.forEach((t) => t.classList.remove("active"));
e.target.classList.add("active");
const kw = document.getElementById("prompt-search")?.value || "";
this.refreshPromptList(kw);
}
});
const addBtn = document.getElementById("add-prompt");
if (addBtn) addBtn.addEventListener("click", () => this.showEditModal());
const list = document.getElementById("prompt-list");
if (list)
list.addEventListener("click", (e) => {
if (e.target.classList.contains("edit-prompt")) {
const id = e.target.dataset.id;
const p = this.prompts.find((x) => x.id === id);
if (p) this.showEditModal(p);
} else if (e.target.classList.contains("delete-prompt")) {
if (confirm("确定要删除这个提示词吗?")) {
const id = e.target.dataset.id;
this.deletePrompt(id);
this.refreshCategories();
this.showToast("提示词已删除");
}
}
});
const clearBtn = document.getElementById("clear-prompt");
if (clearBtn)
clearBtn.addEventListener("click", () => {
this.clearSelectedPrompt();
this.showToast("已清除选中的提示词");
});
const refreshBtn = document.getElementById("refresh-prompts");
if (refreshBtn)
refreshBtn.addEventListener("click", () => {
this.refreshPromptList();
this.findTextarea();
this.showToast("列表已刷新");
});
const toggleBtn = document.getElementById("toggle-panel");
if (toggleBtn)
toggleBtn.addEventListener("click", () => this.togglePanel());
this.makeDraggable();
}
// 可拖动
makeDraggable() {
const panel = document.getElementById("universal-prompt-panel");
const header = panel?.querySelector(".prompt-panel-header");
if (!panel || !header) return;
let isDragging = false,
currentX,
currentY,
initialX,
initialY,
xOffset = 0,
yOffset = 0;
const dragStart = (e) => {
if (e.target.closest(".prompt-panel-controls")) return;
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === header || e.target.closest(".prompt-panel-title"))
isDragging = true;
};
const drag = (e) => {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
panel.style.transform = `translate(${currentX}px, ${currentY}px)`;
};
const dragEnd = () => {
initialX = currentX;
initialY = currentY;
isDragging = false;
};
header.addEventListener("mousedown", dragStart);
document.addEventListener("mousemove", drag);
document.addEventListener("mouseup", dragEnd);
}
}
// 初始化:等待关键容器出现
function init() {
const check = setInterval(() => {
let containers = [];
if (isGemini)
containers = [
".ql-editor",
"rich-textarea",
".text-input-field_textarea",
];
else if (isGenspark)
containers = [
".input-wrapper",
".prompt-input-wrapper",
".textarea-wrapper",
'textarea[name="query"]',
];
else if (isPerplexity)
containers = [
"#ask-input",
"form textarea",
'div[contenteditable="true"][role="textbox"]',
];
else containers = ["textarea", 'div[contenteditable="true"]'];
const found = containers.some((sel) => document.querySelector(sel));
if (found) {
clearInterval(check);
console.log(`${THEME.name} 提示词管理器: 页面元素已加载,开始初始化`);
try {
new UniversalPromptManager();
} catch (e) {
console.error("提示词管理器: 初始化失败", e);
}
}
}, 1000);
setTimeout(() => clearInterval(check), 30000);
}
if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", init);
else init();
})();