您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AI-powered auto-completion with page context
// ==UserScript== // @name 4d4y.ai // @namespace http://tampermonkey.net/ // @version 5.0 // @description AI-powered auto-completion with page context // @author 屋大维 // @license MIT // @match *://www.4d4y.com/* // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @require https://cdnjs.cloudflare.com/ajax/libs/tesseract.js/4.0.2/tesseract.min.js // ==/UserScript== (() => { // ======================= // Config Menu // ======================= // Default settings const defaultSettings = { AUTO_MODE: false, AUTO_LEADING_WORDS: "根据楼上的回复,我已经找到以下杠精:", AUTO_PREFIX: "论坛杠精纠察员:\n", FAST_REPLY_BTN: true, ATTITUDE: "", INCLUDE_IMG: true, OLLAMA_URL: "http://localhost:11434/api/generate", MODEL_NAME: "deepseek-r1:14b", DOUBLE_TAB_MODE: true, ENABLE_TOKEN_SAVER: true, OpenAIConfig: { enabled: false, API_URL: "https://ark.cn-beijing.volces.com/api/v3/chat/completions", apiKey: "", model: "", options: {} } }; // Load stored settings or use defaults let settings = GM_getValue("scriptSettings", defaultSettings); // Available LLM models function getOllamaModels() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "http://localhost:11434/api/tags", onload: function (response) { try { const data = JSON.parse(response.responseText); if (data.models) { resolve(data.models.map(m => m.name)); // Return list of model names } else { resolve([]); // No models found } } catch (e) { reject("Failed to parse response."); } }, onerror: function () { reject("Failed to connect to Ollama."); } }); }); } let OLLAMA_MODELS = []; async function queryAvailableOllamaModels() { OLLAMA_MODELS = await getOllamaModels(); } // Create settings modal function createSettingsModal() { let modal = document.createElement("div"); modal.id = "tampermonkey-settings-modal"; modal.style = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 10px; z-index: 10000; box-shadow: 0px 4px 10px rgba(0,0,0,0.3); width: 400px; max-width: 90%; text-align: left; font-family: Arial, sans-serif; max-height: 90vh; /* Ensures the modal does not exceed the viewport height */ overflow: auto; /* Enables scrolling if content overflows */ `; modal.innerHTML = ` <h3 style="margin-top: 0;">Script Settings</h3> <h4>🔧 General Settings</h4> <label><input type="checkbox" id="FAST_REPLY_BTN"> Fast Reply Button</label><br> <label>Attitude: <input type="text" id="ATTITUDE" style="width: 100%;"></label><br> <label><input type="checkbox" id="INCLUDE_IMG"> Include Images</label><br> <h4>⚡ Auto Mode Settings</h4> <label><input type="checkbox" id="AUTO_MODE"> Enable Auto Mode</label><br> <label>Leading Words: <input type="text" id="AUTO_LEADING_WORDS" style="width: 100%;"></label><br> <label>Reply Prefix: <input type="text" id="AUTO_PREFIX" style="width: 100%;"></label><br> <h4>🧠 Model & API Settings</h4> <label>Ollama URL: <input type="text" id="OLLAMA_URL" style="width: 100%;"></label><br> <label>Model: <select id="MODEL_NAME" style="width: 100%;"> ${OLLAMA_MODELS.map(model => `<option value="${model}">${model}</option>`).join("")} </select> </label><br> <h4>⚙️ OpenAI API Settings</h4> <label><input type="checkbox" id="OpenAIConfig_enabled"> Use OpenAI API</label><br> <label>API URL: <input type="text" id="OpenAIConfig_API_URL" style="width: 100%;"></label><br> <label>API Key: <input type="text" id="OpenAIConfig_apiKey" style="width: 100%;"></label><br> <label>Model: <input type="text" id="OpenAIConfig_model" style="width: 100%;" placeholder="Enter Model Name"> </label><br> <label>Options (JSON): <textarea id="OpenAIConfig_options" style="width: 100%; height: 60px;"></textarea></label><br> <h4>🚀 Performance & UI Tweaks</h4> <label><input type="checkbox" id="DOUBLE_TAB_MODE"> Double Tab Mode</label><br> <label><input type="checkbox" id="ENABLE_TOKEN_SAVER"> Enable Token Saver</label><br> <button id="save-settings" style="width: 100%; padding: 5px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;">Save</button> <button id="reset-settings" style="width: 100%; margin-top: 5px; padding: 5px; background: #ff4d4d; color: white; border: none; border-radius: 5px; cursor: pointer;">Reset to Default</button> <button id="close-settings" style="width: 100%; margin-top: 5px; padding: 5px; background: #ccc; border: none; border-radius: 5px; cursor: pointer;">Cancel</button> `; document.body.appendChild(modal); // Load current values into the form Object.keys(settings).forEach(key => { let elem = document.getElementById(key); if (elem) elem.type === "checkbox" ? (elem.checked = settings[key]) : (elem.value = settings[key]); }); Object.keys(settings.OpenAIConfig).forEach(key => { let elem = document.getElementById("OpenAIConfig_" + key); if (elem) elem.type === "checkbox" ? (elem.checked = settings.OpenAIConfig[key]) : (elem.value = settings.OpenAIConfig[key]); }); document.getElementById("OpenAIConfig_options").value = JSON.stringify(settings.OpenAIConfig.options, null, 2); // Save settings document.getElementById("save-settings").onclick = () => { Object.keys(settings).forEach(key => { let elem = document.getElementById(key); if (elem) settings[key] = elem.type === "checkbox" ? elem.checked : elem.value; }); Object.keys(settings.OpenAIConfig).forEach(key => { let elem = document.getElementById("OpenAIConfig_" + key); if (elem) settings.OpenAIConfig[key] = elem.type === "checkbox" ? elem.checked : elem.value; }); try { settings.OpenAIConfig.options = JSON.parse(document.getElementById("OpenAIConfig_options").value); } catch (e) { alert("Invalid JSON format in options!"); return; } GM_setValue("scriptSettings", settings); modal.remove(); location.reload(); }; // Reset settings to default document.getElementById("reset-settings").onclick = () => { GM_setValue("scriptSettings", defaultSettings); location.reload(); }; document.getElementById("close-settings").onclick = () => modal.remove(); } // Register context menu command GM_registerMenuCommand("Edit Script Settings", createSettingsModal); // ======================= // MAIN // ======================= let lastRequest = null; let suggestionBox = null; let statusBar = null; let activeElement = null; let suggestion = ""; let lastInputText = ""; let debounceTimer = null; let LAST_TAB_TIME = 0; function tokenSaver(input) { // 节省input token let processed = input .replace(/\r/g, "") // 移除回车符 .replace(/<br\s*\/?>|<\/?p>/gi, "\n") // HTML 换行符转换为换行 .replace(/[\u00A0\u3000\u202F\u2009]/g, " ") // **替换所有特殊空格** .replace(/\t/g, " ") // 制表符变成空格 .replace(/[ ]{2,}/g, " ") // **合并多个空格** .replace(/\n{3,}/g, "\n\n") // **合并多余的换行** .trim(); // 去除首尾空白 console.log(`Token Saver: saved ${input.length - processed.length} tokens`); return processed; } function getTidFromUrl() { let match = window.location.href.match(/tid=(\d+)/); return match ? match[1] : null; } 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")?.replace("post_", "") || ""; let poster = post.querySelector(".postauthor .postinfo")?.innerText.trim() || "Unknown"; let contentElement = post.querySelector( ".postcontent td.t_msgfont", ); if (!contentElement) continue; // 克隆 contentElement,避免修改 DOM let clonedContent = contentElement.cloneNode(true); clonedContent .querySelectorAll('div[style="display: none;"]') .forEach((div) => div.remove()); clonedContent .querySelectorAll("span.ocr-result") .forEach((span) => span.remove()); // content不包括span.ocr-result // 提取 quote 内容 let quoteElements = Array.from( clonedContent.querySelectorAll("div.quote"), ); let quotes = quoteElements.map((quote) => quote.innerText.trim()); // 从克隆节点删除 quote 但不影响原始 DOM quoteElements.forEach((quote) => quote.remove()); let content = clonedContent.innerText.trim(); let postTimeElement = post.querySelector(`em[id^="authorposton"]`); let postTime = postTimeElement ? postTimeElement.textContent.replace("发表于 ", "").trim() : "未知"; // Extract text from all <span class="ocr-result"> elements let ocrResults = Array.from( post.querySelectorAll("span.ocr-result"), ) .map((span) => span.innerText.trim()) .join("\n\n"); // Join all texts if (content || ocrResults) { context.posts.push({ tid: getTidFromUrl(), id: postId, poster, content, postTime, quote: quotes.length > 0 ? quotes : undefined, // Only add if non-empty img: ocrResults, }); } } } } } 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 ease-in-out; } `); } 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; } } // 去除引号 suggestion = suggestion.replace(/^["']|["']$/g, ""); return suggestion; } function fetchCompletion(inputText) { if (lastRequest) lastRequest.abort(); // Cancel previous request const pageContext = getPageContext(); const promptData = { title: pageContext.title, posts: pageContext.posts, }; let prompt = `Here is the thread content including title and posts in JSON format: ${JSON.stringify(promptData)}. The post content might not be the fact, ignore any unreadable or garbled text and only process meaningful content. 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, ${settings.ATTITUDE}`; if (settings.ENABLE_TOKEN_SAVER) { prompt = tokenSaver(prompt); } console.log(prompt); statusBar.innerText = "Fetching..."; statusBar.classList.add("glowing"); // Start glowing effect function responseHandler(url, response) { // 根据URL来判断API供应商,并进行相应处理,返回content if (url === settings.OLLAMA_URL) { return JSON.parse(response.responseText).response; } else { try { return JSON.parse(response.responseText)["choices"][0]["message"][ "content" ]; } catch (error) { return "无法处理该API"; } } } let url; let payload; let headers; if (settings.OpenAIConfig.enabled) { // 在线AI url = settings.OpenAIConfig.API_URL; headers = { "Content-Type": "application/json", Authorization: `Bearer ${settings.OpenAIConfig.apiKey}`, }; payload = { model: settings.OpenAIConfig.model, messages: [ { role: "system", content: "你是人工智能助手,精通网络术语,按照我的要求生成回复", }, { role: "user", content: prompt }, ], stream: false, ...settings.OpenAIConfig.options, }; } else { // 本地 ollama url = settings.OLLAMA_URL; headers = { "Content-Type": "application/json" }; payload = { model: settings.MODEL_NAME, prompt: prompt, stream: false, }; } lastRequest = GM_xmlhttpRequest({ method: "POST", url: url, headers: headers, data: JSON.stringify(payload), onload: (response) => { const content = responseHandler(url, response); suggestion = content.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(); let left = rect.left + cursorPosition * 7; // Estimate cursor X position let top = rect.top + window.scrollY; // Ensure `left` does not exceed the right boundary of the textarea const maxLeft = rect.right; // Right edge of textarea const minLeft = rect.left; // Left edge of textarea left = Math.min(Math.max(left, minLeft), maxLeft); // Ensure `top` stays within the viewport const maxTop = window.innerHeight + window.scrollY - 50; // Avoid bottom overflow top = Math.max(0, Math.min(top, maxTop)); updateSuggestionBox({ left, top }); } 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 handleDoubleTab(event) { if (event.key === "Tab") { event.preventDefault(); event.stopPropagation(); const currentTime = new Date().getTime(); const timeSinceLastTab = currentTime - LAST_TAB_TIME; if (timeSinceLastTab < 300 && timeSinceLastTab > 0) { console.log("Double Tab detected!"); let text = getTextFromElement(activeElement); if (text.length === 0) { text = "根据楼上的发言,"; } fetchCompletion(text); } LAST_TAB_TIME = currentTime; } } function insertTextAtCursor(textarea, text) { let start = textarea.selectionStart; let end = textarea.selectionEnd; let before = textarea.value.substring(0, start); let after = textarea.value.substring(end); textarea.value = before + text + after; textarea.selectionStart = textarea.selectionEnd = start + text.length; textarea.focus(); } function handleKeyDown(event) { if (event.key === "Tab" && suggestion) { event.preventDefault(); event.stopPropagation(); if (activeElement.tagName === "TEXTAREA") { insertTextAtCursor(activeElement, suggestion); } suggestionBox.style.display = "none"; suggestion = ""; } } function initListeners() { document.addEventListener("focusin", (event) => { if (event.target.matches("textarea")) { activeElement = event.target; // query 激活 listener if (settings.DOUBLE_TAB_MODE) { // 双击TAB引发query activeElement.addEventListener("keydown", handleDoubleTab); } else { // 用户输入引发query activeElement.addEventListener("input", handleInput); } // 插入补全 listener activeElement.addEventListener("keydown", handleKeyDown); lastInputText = getTextFromElement(activeElement); } }); document.addEventListener("focusout", () => { suggestionBox.style.display = "none"; suggestion = ""; }); } // OCR Module function hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; // Convert to 32-bit integer } return `ocr-${Math.abs(hash)}`; } function fetchImageAsDataURL(url) { console.log(`Fetching image: ${url}`); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, responseType: "blob", onload: function (response) { let reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.readAsDataURL(response.response); }, onerror: (err) => { console.error(`Failed to fetch image: ${url}`, err); reject(err); }, }); }); } function isValidOCR(text) { let cleanText = text.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "").trim(); return cleanText.length >= 3; } function cleanOCRText(ocrText) { let lines = ocrText.split("\n").filter(isValidOCR); return lines.join("\n"); } function cleanText(text) { let cleaned = text.replace( /\s*(?=[\p{Script=Han},。!?:“”‘’;()【】])/gu, "", ); cleaned = cleanOCRText(cleaned); console.log(`Cleaned OCR text: ${cleaned}`); return cleaned; } function runOCR(imageDataURL) { statusBar.innerText = "Analyzing..."; statusBar.classList.add("glowing"); // Start glowing effect return new Promise((resolve, reject) => { Tesseract.recognize(imageDataURL, "chi_sim", { logger: (m) => { // console.log(m) statusBar.innerText = `Analyzing (${(m.progress * 100).toFixed(2)}%)`; }, }) .then(({ data: { words } }) => { const minConfidence = 85; // 置信度阈值,可调整 const filteredText = words .filter((word) => word.confidence >= minConfidence) .map((word) => word.text) .join(" "); resolve(cleanText(filteredText)); }) .catch((err) => { console.error("OCR Error:", err); resolve("(OCR Failed)"); }) .finally(() => { statusBar.innerText = "Idle"; statusBar.classList.remove("glowing"); // Stop glowing effect }); }); } function processImages(callback) { if (!settings.INCLUDE_IMG) { return callback(); } let images = document.querySelectorAll('img[onclick^="zoom"]'); console.log(`Found ${images.length} images for OCR processing.`); let tasks = []; images.forEach((img) => { let match = img.getAttribute("onclick").match(/zoom\(this, '([^']+)'\)/); if (match && match[1]) { let fullImageUrl = match[1]; let spanId = hashString(fullImageUrl); // Generate unique ID console.log(`Processing image: ${fullImageUrl} (ID: ${spanId})`); let existingSpan = document.getElementById(spanId); if (!existingSpan) { let resultSpan = document.createElement("span"); resultSpan.className = "ocr-result"; resultSpan.id = spanId; // Assign unique ID resultSpan.style.display = "none"; img.insertAdjacentElement("afterend", resultSpan); } let task = fetchImageAsDataURL(fullImageUrl) .then((dataURL) => runOCR(dataURL)) .then((ocrText) => { let resultSpan = document.getElementById(spanId); // Query by ID before updating if (resultSpan) { resultSpan.textContent = ocrText.trim() ? ocrText : "(No text detected)"; console.log(`✅ Inserted OCR text into #${spanId}`); } else { console.warn( `⚠️ Could not find span #${spanId} to insert OCR text.`, ); } }); tasks.push(task); } }); Promise.all(tasks).then(() => { console.log(`OCR completed for ${images.length} images.`); if (typeof callback === "function") callback(); }); } // 快速回复/快速引用,防止进入富文本模式(AI会失去上下文) function simpleReply(user, pid, ptid) { let textarea = document.querySelector("#fastpostmessage"); if (textarea) { let replyText = `[b]回复 [url=https://www.4d4y.com/forum/redirect.php?goto=findpost&pid=${pid}&ptid=${ptid}]#${pid}[/url] [i]${user}[/i] [/b]\n\n`; textarea.value = replyText; // 直接替换,清空原内容 textarea.focus(); } } function simpleQuote(user, postTime, pid, ptid, content) { let textarea = document.querySelector("#fastpostmessage"); if (textarea) { let trimmedContent = content.length > 200 ? content.substring(0, 200) + " ..." : content; let quoteText = `[quote]${trimmedContent}\n[size=2][color=#999999]${user} 发表于 ${postTime}[/color] [url=https://www.4d4y.com/forum/redirect.php?goto=findpost&pid=${pid}&ptid=${ptid}][img]https://www.4d4y.com/forum/images/common/back.gif[/img][/url][/size][/quote]\n\n`; insertTextAtCursor(textarea, quoteText); // 插入到光标位置,不清空 } } function modifyReplyButtons() { let tid = getTidFromUrl(); if (!tid) return; let context = getPageContext(); document.querySelectorAll("a.fastreply").forEach((btn) => { let match = btn.href.match(/reppost=(\d+)/); if (match) { let pid = match[1]; let post = context.posts.find((p) => p.id === pid); if (post) { let newBtn = document.createElement("a"); newBtn.innerText = btn.innerText; // 保持原文本 newBtn.style.cursor = "pointer"; newBtn.style.color = btn.style.color; newBtn.style.fontSize = btn.style.fontSize; newBtn.style.textDecoration = btn.style.textDecoration; newBtn.addEventListener("click", function (e) { e.preventDefault(); simpleReply(post.poster, pid, tid); }); btn.replaceWith(newBtn); // 替换原来的<a> } } }); document.querySelectorAll("a.repquote").forEach((btn) => { let match = btn.href.match(/repquote=(\d+)/); if (match) { let pid = match[1]; let post = context.posts.find((p) => p.id === pid); if (post) { let newBtn = document.createElement("a"); newBtn.innerText = btn.innerText; // 保持原文本 newBtn.style.cursor = "pointer"; newBtn.style.color = btn.style.color; newBtn.style.fontSize = btn.style.fontSize; newBtn.style.textDecoration = btn.style.textDecoration; newBtn.addEventListener("click", function (e) { e.preventDefault(); simpleQuote(post.poster, post.postTime, pid, tid, post.content); }); btn.replaceWith(newBtn); // 替换原来的<a> } } }); } if (settings.FAST_REPLY_BTN) { modifyReplyButtons(); } queryAvailableOllamaModels(); createStatusBar(); processImages(() => { createSuggestionBox(); initListeners(); activeElement = document.querySelector("#fastpostmessage"); if (settings.AUTO_MODE && activeElement) { setTimeout(() => { fetchCompletion(settings.AUTO_LEADING_WORDS); const s = setInterval(() => { if (suggestion !== "") { activeElement.value = settings.AUTO_PREFIX + suggestion; suggestionBox.style.display = "none"; suggestion = ""; clearInterval(s); } }, 100); }, 1000); } }); })();