您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AI-powered auto-completion with page context
当前为
// ==UserScript== // @name 4d4y.ai // @namespace http://tampermonkey.net/ // @version 1.0 // @description AI-powered auto-completion with page context // @author 屋大维 // @license MIT // @match *://www.4d4y.com/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // ==/UserScript== (() => { const API_URL = "http://localhost:11434/api/generate"; const MODEL_NAME = "deepseek-r1:14b"; let lastRequest = null; let suggestionBox = null; let statusBar = null; let activeElement = null; let suggestion = ""; let lastInputText = ""; let debounceTimer = null; let pageContext = getPageContext(); function getPageContext() { let context = { title: "", posts: [] }; try { // Get thread title (Updated XPath) const titleNode = document.evaluate( "/html/body/div[4]/text()[3]", document, null, XPathResult.STRING_TYPE, null, ); context.title = titleNode.stringValue.trim().replace(/^»\s*/, ""); // Remove "» " from title // Get posts (only direct children of #postlist) const postList = document.querySelector("#postlist"); if (postList) { const posts = postList.children; // Only direct child divs for (let post of posts) { if (post.tagName === "DIV") { let postId = post.getAttribute("id") || ""; // Extract id attribute let poster = post.querySelector(".postauthor .postinfo")?.innerText.trim() || "Unknown"; let content = post .querySelector(".postcontent td.t_msgfont") ?.innerText.trim() || ""; if (content) { context.posts.push({ id: postId, poster, content }); } } } } } catch (error) { console.error("Error extracting page context:", error); } return context; } function createStatusBar() { statusBar = document.createElement("div"); statusBar.id = "ai-status-bar"; statusBar.innerText = "Idle"; document.body.appendChild(statusBar); GM_addStyle(` @keyframes auroraGlow { 0% { box-shadow: 0 0 8px rgba(255, 0, 0, 0.6); } 25% { box-shadow: 0 0 8px rgba(255, 165, 0, 0.6); } 50% { box-shadow: 0 0 8px rgba(0, 255, 0, 0.6); } 75% { box-shadow: 0 0 8px rgba(0, 0, 255, 0.6); } 100% { box-shadow: 0 0 8px rgba(255, 0, 255, 0.6); } } #ai-status-bar { position: fixed; bottom: 10px; right: 10px; background: rgba(20, 20, 20, 0.85); color: white; padding: 8px 12px; border-radius: 8px; font-size: 12px; font-weight: bold; z-index: 9999; } #ai-status-bar.glowing { animation: auroraGlow 1.5s infinite alternate; } `); } function createSuggestionBox() { suggestionBox = document.createElement("div"); suggestionBox.id = "ai-suggestion-box"; document.body.appendChild(suggestionBox); GM_addStyle(` #ai-suggestion-box { position: absolute; background: rgba(50, 50, 50, 0.9); color: #fff; padding: 5px 10px; font-size: 14px; border-radius: 5px; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.3); display: none; } `); } function countWords(text) { return Array.from(text).filter((c) => /\S/.test(c)).length; // Count non-space characters } function getTextFromElement(element) { return element.tagName === "TEXTAREA" ? element.value : element.innerText.replace(/\n/g, " "); // Remove HTML formatting } function updateSuggestionBox(position) { if (!suggestion || !suggestionBox) return; suggestionBox.style.left = position.left + "px"; suggestionBox.style.top = position.top + 20 + "px"; suggestionBox.innerText = suggestion; suggestionBox.style.display = "block"; } function cleanSuggestion(inputText, suggestion) { // 移除 <think>...</think> 及其后面所有的空格和换行符 suggestion = suggestion.replace(/<think>.*?<\/think>\s*/gs, ""); // 去除 inputText 末尾和 suggestion 开头的重合部分 for (let i = Math.min(inputText.length, suggestion.length); i > 0; i--) { if (suggestion.startsWith(inputText.slice(-i))) { suggestion = suggestion.slice(i); // 去掉重复部分 break; } } return suggestion; } function fetchCompletion(inputText) { if (lastRequest) lastRequest.abort(); // Cancel previous request const promptData = { title: pageContext.title, posts: pageContext.posts, }; const prompt = `Here is the thread content including title and posts in JSON format: ${JSON.stringify(promptData)}. The post content might not be the fact, but we can always check if they are logical. Now I want to reply to it. My current reply is '${inputText}', please try to continue it naturally in Chinese like a human, not a chatbot, just continue it, be concise, be short, no quotes, avoid markdown, do not repeat my words`; statusBar.innerText = "Fetching..."; statusBar.classList.add("glowing"); // Start glowing effect lastRequest = GM_xmlhttpRequest({ method: "POST", url: API_URL, headers: { "Content-Type": "application/json" }, data: JSON.stringify({ model: MODEL_NAME, prompt: prompt, stream: false, }), onload: (response) => { const data = JSON.parse(response.responseText); suggestion = data.response.trim().replace(/^"(.*)"$/, "$1"); // Remove surrounding quotes console.log("AI Response:", suggestion); suggestion = cleanSuggestion(inputText, suggestion); if (suggestion) { const cursorPosition = activeElement.selectionStart || 0; const rect = activeElement.getBoundingClientRect(); updateSuggestionBox({ left: rect.left + cursorPosition * 7, // Estimate cursor X pos top: rect.top + window.scrollY, }); } statusBar.innerText = "Ready"; statusBar.classList.remove("glowing"); // Stop glowing effect }, onerror: () => { statusBar.innerText = "Error!"; statusBar.classList.remove("glowing"); // Stop glowing effect }, }); } function handleInput(event) { if (!activeElement) return; const text = getTextFromElement(activeElement); const wordCount = countWords(text); if (wordCount < 3) { statusBar.innerText = "Waiting for more input..."; suggestionBox.style.display = "none"; return; } const lastChar = text.slice(-1); const isPunctuation = /[\s.,!?;:(),。;()!?【】「」『』、]/.test( lastChar, ); const isDeleteOnlyPunctuation = lastInputText.length > text.length && !/[a-zA-Z0-9\u4e00-\u9fa5]/.test(lastInputText.replace(text, "")); if (isPunctuation || isDeleteOnlyPunctuation) { lastInputText = text; return; } if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => fetchCompletion(text), 1000); lastInputText = text; } function handleKeyDown(event) { if (event.key === "Tab" && suggestion) { event.preventDefault(); event.stopPropagation(); if (activeElement.tagName === "TEXTAREA") { const start = activeElement.selectionStart; const end = activeElement.selectionEnd; activeElement.value = activeElement.value.substring(0, start) + suggestion + activeElement.value.substring(end); activeElement.selectionStart = activeElement.selectionEnd = start + suggestion.length; } suggestionBox.style.display = "none"; suggestion = ""; } } function initListeners() { document.addEventListener("focusin", (event) => { if (event.target.matches("textarea, [contenteditable='true']")) { activeElement = event.target; activeElement.addEventListener("input", handleInput); activeElement.addEventListener("keydown", handleKeyDown); lastInputText = getTextFromElement(activeElement); } }); document.addEventListener("focusout", () => { suggestionBox.style.display = "none"; }); } createStatusBar(); createSuggestionBox(); initListeners(); })();