UNPKG

gbdetector

Version:

GbDetector is an advanced text analysis module designed to identify gambling-related content through sophisticated pattern matching and text processing techniques.

1,341 lines (1,170 loc) 51.7 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>GbDetector | Comment Moderation System</title> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" /> <style> /* ========== Base Variables ========== */ :root { /* Color palette */ --primary: #6366f1; --primary-dark: #4f46e5; --primary-light: #a5b4fc; --secondary: #22d3ee; --danger: #ef4444; --danger-dark: #dc2626; --success: #22c55e; --dark: #1e293b; --light: #f8fafc; /* Gray scale */ --gray-100: #f3f4f6; --gray-200: #e5e7eb; --gray-300: #d1d5db; --gray-500: #6b7280; --gray-700: #374151; --gray-900: #111827; /* Elevation (shadows) */ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* Border radius */ --radius-sm: 0.375rem; /* 6px */ --radius: 0.625rem; /* 10px */ --radius-lg: 1rem; /* 16px */ /* Animation */ --transition-fast: all 0.2s ease; --transition: all 0.3s ease; --transition-slow: all 0.5s ease; /* Fonts */ --font-sans: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; --font-mono: "Consolas", "Monaco", monospace; /* Spacing (can be used with calc) */ --space-1: 0.25rem; /* 4px */ --space-2: 0.5rem; /* 8px */ --space-3: 0.75rem; /* 12px */ --space-4: 1rem; /* 16px */ --space-5: 1.35rem; /* 20px */ --space-6: 1.5rem; /* 24px */ --space-8: 2rem; /* 32px */ --space-10: 2.5rem; /* 40px */ } /* ========== Reset & Base Styles ========== */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } /* Improve text rendering */ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; scroll-behavior: smooth; } body { font-family: var(--font-sans); background: linear-gradient(135deg, #f6f8fb 0%, #e9edf5 100%); min-height: 100vh; padding: 0; margin: 0; color: var(--gray-700); line-height: 1.6; display: flex; flex-direction: column; align-items: center; justify-content: center; } /* Focus styles for accessibility */ :focus-visible { outline: 3px solid var(--primary-light); outline-offset: 2px; } /* ========== Layout Components ========== */ .container { width: clamp(300px, 95%, 800px); /* Responsive width with min/max constraints */ max-width: 100%; margin: clamp(1rem, 5vh, 2.5rem) auto; background: white; border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); overflow: hidden; position: relative; } .header { background: linear-gradient(to right, var(--primary), #8b5cf6); padding: var(--space-6) 0; text-align: center; color: white; position: relative; } /* Create curved edge at bottom of header */ .header::after { content: ""; position: absolute; width: 100%; height: 38px; bottom: -20px; left: 0; background: white; border-radius: 50% 50% 0 0; } .header .title { position: relative; z-index: 2; /* Ensure text is above curved edge */ } .header .title h1 { font-size: clamp(1.75rem, 5vw, 2.25rem); /* Responsive font size */ font-weight: 700; margin-bottom: calc(clamp(0.25rem, 0.5vw, 0.5rem) - 0.25rem); letter-spacing: -0.2px; } .header .title p { font-size: clamp(0.875rem, 3vw, 1rem); font-weight: 400; opacity: 0.9; } .content { padding: var(--space-4) var(--space-8) var(--space-5); } /* App logo styling */ .logo-container { position: absolute; top: -40px; left: 50%; transform: translateX(-50%); width: 80px; height: 80px; background: linear-gradient(to right, var(--primary), var(--secondary)); border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow); z-index: 10; } .logo { font-size: 2rem; color: white; } /* ========== Form Elements ========== */ .form-group { margin-bottom: 1.625rem; } label { display: block; font-weight: 500; margin-bottom: var(--space-2); font-size: 0.875rem; color: var(--gray-700); } select, input { width: 100%; padding: 0.875rem 1rem; font-size: 0.9375rem; border: 1px solid var(--gray-300); border-radius: var(--radius); background-color: var(--light); transition: var(--transition-fast); box-shadow: var(--shadow-sm); cursor: pointer; font-family: inherit; /* Ensure consistent font */ } /* Form control states */ select:hover, input:hover { border-color: var(--primary); } select:focus, input:focus { border-color: var(--primary); outline: none; background-color: white; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); } /* ========== Buttons ========== */ .button-group { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: var(--space-4); } button { padding: 0.875rem 1.25rem; font-size: 0.9375rem; font-weight: 600; border: none; border-radius: var(--radius); cursor: pointer; transition: var(--transition); display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow); flex: 1; min-width: 120px; font-family: inherit; /* Ensure consistent font */ } button i { margin-right: var(--space-2); } /* Button variants */ .start-button { background-color: var(--primary); color: white; } .start-button:hover { background-color: var(--primary-dark); transform: translateY(-2px); box-shadow: 0 6px 12px rgba(99, 102, 241, 0.2); } .reject-button { background-color: var(--danger); color: white; } .reject-button:hover { background-color: var(--danger-dark); transform: translateY(-2px); box-shadow: 0 6px 12px rgba(239, 68, 68, 0.2); } .history-button { background-color: var(--gray-500); color: white; } .history-button:hover { background-color: var(--gray-700); transform: translateY(-2px); box-shadow: 0 6px 12px rgba(82, 82, 93, 0.2); } .tutorial { background-color: var(--gray-100); color: var(--gray-700); } .tutorial:hover { background-color: var(--gray-200); transform: translateY(-2px); } /* Button states */ button:active { transform: translateY(0); } button:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; } .status { margin-top: var(--space-2); font-size: 0.875rem; color: var(--gray-500); } .status.success { color: var(--success); } .status.error { color: var(--danger); } /* ========== Log Container ========== */ #log-container { width: 100%; min-height: 190px; max-height: 195px; /* batas tinggi container, bisa kamu sesuaikan */ margin-top: var(--space-6); background-color: var(--dark); border-radius: var(--radius); overflow: hidden; /* penting agar tidak ada overflow luar */ display: flex; flex-direction: column; box-shadow: var(--shadow); } .log-header { padding: 0.75rem 1rem; background-color: var(--gray-900); color: var(--gray-300); font-size: 0.875rem; font-weight: 500; display: flex; justify-content: space-between; align-items: center; border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); } .log-content { flex: 1; /* isi ruang tersisa */ overflow-y: auto; /* scroll hanya di sini */ padding: 1rem; font-family: var(--font-mono); font-size: clamp(0.9rem, 0.4rem + 0.6vw, 1.4rem); color: #a5f3fc; line-height: 1.6; /* Scrollbar styling */ scrollbar-width: thin; scrollbar-color: var(--gray-700) var(--dark); } .log-content::-webkit-scrollbar { width: 8px; } .log-content::-webkit-scrollbar-track { background: var(--dark); } .log-content::-webkit-scrollbar-thumb { background-color: var(--gray-700); border-radius: 20px; border: 2px solid var(--dark); } .title-log, .menu-log{ display: flex; gap: 10px; } .menu-log span#clear-log{ cursor: pointer; transition: var(--transition); text-align: center; } .menu-log span#clear-log:hover, .menu-log span#clear-log:checked{ color: var(--gray-500); } /* ========== Notifications ========== */ #notification-container { position: fixed; top: 20px; right: 20px; z-index: 9999; } .notification { padding: 1rem 1.25rem; border-radius: var(--radius); font-weight: 500; margin-bottom: 20px; opacity: 0; transition: opacity 0.4s ease, transform 0.4s ease; transform: translateX(30px); box-shadow: var(--shadow-lg); max-width: 350px; display: flex; align-items: center; } .notification.show { opacity: 1; transform: translateX(0); } .notification i { margin-right: 0.75rem; font-size: 1.125rem; flex-shrink: 0; } /* Notification variants */ .notification.nothing { background-color: var(--gray-100); color: var(--gray-700); border-left: 4px solid var(--gray-500); } .notification.success { background-color: #dcfce7; color: #166534; border-left: 4px solid var(--success); } .notification.error { background-color: #fee2e2; color: #991b1b; border-left: 4px solid var(--danger); } /* ========== Loading Animation ========== */ .loading-screen { display: flex; position: absolute; top: 0; left: 0; color: white; font-size: 1.5em; z-index: 10; width: 100%; min-height: 100%; justify-content: center; align-items: center; background: var(--gray-900); } .loading-screen #typewriter-text { font-size: 1.2em; white-space: nowrap; overflow: hidden; border-right: 2px solid var(--gray-900); text-align: center; width: fit-content; line-height: 1.2em; min-height: 1.2em; animation: blink-caret 0.75s step-end infinite; } @keyframes blink-caret { from, to { border-color: transparent; } 50% { border-color: var(--gray-900); } } .loading-container { display: none; margin: 1.5rem 0; padding: 1.25rem; background-color: var(--gray-100); border-radius: var(--radius); text-align: center; box-shadow: var(--shadow-sm); } .loading-text { font-family: var(--font-mono); font-size: 0.875rem; color: var(--gray-700); font-weight: 500; height: 22px; margin-bottom: 0.75rem; } /* Text reveal animation elements */ .text-reveal { display: inline-block; opacity: 0; transform: translateY(8px); } .text-reveal.visible { opacity: 1; transform: translateY(0); transition: opacity 0.05s ease-in-out, transform 0.1s ease-in-out; } .pulse { animation: pulse 1.5s infinite; } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } } .loading-progress { width: 100%; height: 6px; background-color: var(--gray-200); border-radius: 3px; overflow: hidden; position: relative; } .loading-bar { height: 100%; width: 0%; background: linear-gradient(to right, var(--primary), var(--secondary)); transition: width 0.3s ease; position: relative; border-radius: 3px; } /* Loading bar shine animation */ .loading-bar::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0) 100%); animation: shine 1.5s infinite; } @keyframes shine { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } /* ========== Modal/Popup Components ========== */ .tutorial-popup, .history-popup { display: none; position: fixed; z-index: 999; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; } .tutorial-popup.show, .history-popup.show { opacity: 1; pointer-events: auto; } /* Popup content box */ .tutorial-popup-content, .history-popup-content { background-color: #fff; padding: 1.875rem; border-radius: var(--radius); width: 90%; max-width: 500px; box-shadow: var(--shadow-lg); animation: fadeInPopup 0.3s ease; position: relative; transform: scale(0.95); transition: transform 0.3s ease; } .tutorial-popup.show .tutorial-popup-content, .history-popup.show .history-popup-content { transform: scale(1); } /* Close button */ .close-btn { position: absolute; top: 15px; right: 20px; font-size: 22px; cursor: pointer; color: var(--gray-500); transition: var(--transition); width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } .close-btn:hover { color: var(--gray-900); background-color: var(--gray-100); } /* Popup content styling */ .tutorial-popup-content h3, .history-popup-content h3 { color: var(--primary); font-size: 1.25rem; margin-bottom: 1.25rem; display: flex; align-items: center; } .tutorial-popup-content h3 i, .history-popup-content h3 i { margin-right: 0.625rem; font-size: 1.375rem; } .tutorial-list { padding-left: 1.25rem; margin: 0; color: var(--gray-700); font-size: 0.9375rem; line-height: 1.7; } .tutorial-list li { margin-bottom: 0.625rem; } .tutorial-list code { background-color: var(--gray-100); padding: 0.188rem 0.375rem; border-radius: 4px; font-size: 0.8125rem; font-family: var(--font-mono); color: var(--primary); } /* Search input for history */ #history-search { margin-bottom: 1rem; padding: 0.625rem; width: 100%; border: 1px solid var(--gray-300); border-radius: var(--radius-sm); } #history-content { max-height: 300px; overflow-y: auto; } /* Animation keyframes */ @keyframes fadeInPopup { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } /* ========== Responsive Design ========== */ /* Responsive styles */ @media (max-width: 768px) { .container { width: 92%; margin: 20px auto; } .header { padding: 25px 0; } .title h1 { font-size: 28px; } .content { padding: 30px 20px 25px; } .button-group { flex-direction: column; } button { width: 100%; } #log-container { height: 100%; } .log-content { height: calc(200px - 40px); } .tutorial-popup-content { width: 95%; padding: 25px 20px; } } @media (max-width: 480px) { .container { width: 95%; margin: 15px auto; } .header { padding: 20px 0; } .title h1 { font-size: 24px; } .title p { font-size: 14px; } .content { padding: 25px 15px 20px; } label { font-size: 13px; } select, input { padding: 12px 14px; font-size: 14px; } button { padding: 12px 16px; font-size: 14px; } #log-container { height: 95%; } .log-content { height: calc(180px - 40px); } } /* ========== Print Styles ========== */ @media print { body { background: white; } .container { box-shadow: none; width: 100%; } .button-group, .notification, .loading-container { display: none !important; } } </style> </head> <body> <div id="notification-container"> <div id="notification" class="notification"> <i class="fas fa-check-circle"></i> <span id="notification-text"></span> </div> </div> <div class="container"> <!-- Loading screen Container --> <div class="loading-screen" id="loading-screen"> <div id="typewriter-text"></div> </div> <!-- End Loading screen Container --> <div class="header"> <!-- Title --> <div class="title"> <h1>GbDetector</h1> <p>Comment Moderation System</p> </div> <!-- End Title --> </div> <div class="content"> <!-- Form group --> <div class="form-group"> <label for="platform"><i class="fas fa-share-alt"></i> Select Platform</label> <select id="platform"> <option value="instagram">Instagram</option> <option value="facebook">Facebook</option> <option value="youtube">YouTube</option> <option value="twitter">Twitter</option> </select> <div id="credential-status" class="status"></div> </div> <div class="form-group"> <label for="mediaId"><i class="fas fa-id-card"></i> Media / Post / Video ID</label> <input type="text" id="mediaId" placeholder="Enter the ID here..." /> </div> <!-- End form group --> <!-- Button menu --> <div class="button-group"> <button id="startButton" class="start-button"><i class="fas fa-play"></i> Start Moderation</button> <button id="rejectButton" class="reject-button"><i class="fas fa-ban"></i> Cancel</button> <button id="historyButton" class="history-button"><i class="fas fa-history"></i> History</button> <button id="tutorial-btn" class="tutorial"><i class="fas fa-question-circle"></i> Tutorial</button> </div> <!-- End Button menu --> <!-- Loading Text Reveal Container --> <div id="loading-container" class="loading-container"> <div id="loading-text" class="loading-text"></div> <div class="loading-progress"> <div id="loading-bar" class="loading-bar"></div> </div> </div> <!-- End Loading Text Reveal Container --> <!-- Logging Container --> <div id="log-container"> <div class="log-header"> <div class="title-log"> <span><i class="fas fa-terminal"></i> System Log</span> </div> <div class="menu-log"> <span id="clear-log">Clear</span> <span id="log-status">Ready</span> </div> </div> <div id="log" class="log-content"></div> </div> <div class="hidden-log" id="hidden-log" style="display: none;"></div> <div class="historylog" id="historylog" style="display: none;"></div> <!-- End Logging Container --> </div> <div id="tutorial-container" class="tutorial-popup"> <!-- Popup menu Container --> <div class="tutorial-popup-content"> <span id="close-tutorial" class="close-btn"><i class="fas fa-times"></i></span> <h3><i class="fas fa-info-circle"></i> Usage Tutorial</h3> <ol class="tutorial-list"> <li>Make sure the <code>credential.json</code> file containing your API credentials is located in the current directory.</li> <li>Select a platform from the dropdown list (e.g., Instagram, Facebook, YouTube).</li> <li>Enter the ID of the media, post, or video you want to moderate.</li> <li>Click the <strong>Start Moderation</strong> button to begin the comment moderation process.</li> <li>The system log will display real-time progress and any detected issues.</li> <li>Click the <strong>Cancel</strong> button to stop the process at any time.</li> </ol> </div> <!-- End Popup menu Container --> </div> <div id="history-container" class="tutorial-popup"> <!-- Popup menu Container --> <div class="tutorial-popup-content"> <!-- Popup History Content --> <span id="close-history" class="close-btn"><i class="fas fa-times"></i></span> <h3><i class="fas fa-history"></i> Moderation History</h3> <input type="text" id="history-search" placeholder="Search ID or platform..." style="width: 100%; padding: 10px; margin-bottom: 10px; border: 1px solid var(--gray-300); border-radius: var(--radius);" /> <div id="history-content" style="max-height: 300px; overflow-y: auto; padding-top: 10px;"></div> </div> <!-- End Popup menu Container --> </div> </div> <script> // Cache DOM elements for better performance const elements = { notification: document.getElementById("notification"), startButton: document.getElementById("startButton"), rejectButton: document.getElementById("rejectButton"), logContainer: document.getElementById("log"), loadingContainer: document.getElementById("loading-container"), loadingText: document.getElementById("loading-text"), loadingBar: document.getElementById("loading-bar"), tutorialBtn: document.getElementById("tutorial-btn"), tutorialPopup: document.getElementById("tutorial-container"), tutorialCloseBtn: document.getElementById("close-tutorial"), logStatus: document.getElementById("log-status"), historyBtn: document.getElementById("historyButton"), historyPopup: document.getElementById("history-container"), closeHistory: document.getElementById("close-history"), historyContent: document.getElementById("history-content"), historyLog: document.getElementById("historylog"), historySearch: document.getElementById("history-search"), clearTerminalLog: document.getElementById("clear-log"), mediaIdInput: document.getElementById("mediaId"), platformSelect: document.getElementById("platform"), }; // State variables let isRunning = false; let textRevealInterval = null; // Collection of loading messages to display during processing const loadingTexts = [ "Initializing moderation system...", "Connecting to API services...", "Fetching comments data...", "Analyzing content patterns...", "Applying detection algorithms...", "Processing gambling indicators...", "Preparing moderation actions...", ]; /** * Animates text reveal character by character with progress bar update * @param {string} text - Text to reveal * @param {Function} callback - Function to call after text is revealed */ function startTextReveal(text, callback) { elements.loadingText.innerHTML = ""; const characters = text.split(""); let currentIndex = 0; // Clear any existing intervals to prevent overlaps if (textRevealInterval) clearInterval(textRevealInterval); textRevealInterval = setInterval(() => { if (currentIndex < characters.length) { // Create and add character with animation class const span = document.createElement("span"); span.className = "text-reveal"; span.textContent = characters[currentIndex]; elements.loadingText.appendChild(span); // Trigger reveal animation with slight delay setTimeout(() => span.classList.add("visible"), 10); currentIndex++; // Update progress bar proportionally const progress = (currentIndex / characters.length) * 100; elements.loadingBar.style.width = `${progress}%`; } else { // Animation complete clearInterval(textRevealInterval); // Add pulsing effect to indicate waiting state elements.loadingText.classList.add("pulse"); // Call next step after delay if (callback) { setTimeout(callback, 2000); } } }, 50); // Speed of character reveal } /** * Cycles through loading text messages while process is running */ function cycleLoadingTexts() { let textIndex = 0; const showNextText = () => { elements.loadingText.classList.remove("pulse"); startTextReveal(loadingTexts[textIndex], () => { textIndex = (textIndex + 1) % loadingTexts.length; if (isRunning) { showNextText(); // Continue cycle if still running } else { hideLoading(); // Stop and hide loading UI } }); }; showNextText(); // Start the cycle } /** * Creates typewriter text animation effect * @param {string} text - Text to animate * @param {string} elementId - ID of target element * @param {Object} options - Configuration options */ function typeWriterEffect(text, elementId, options = {}) { // Default options with destructuring const { typeSpeed = 100, eraseSpeed = 50, delay = 1000, mode = 1, // 0: type once, 1: type and erase loop callback = () => {}, } = options; const el = document.getElementById(elementId); if (!el) return; let i = 0; let isDeleting = false; function type() { const currentText = text.substring(0, i); if (mode === 1) { // Mode 1: Type and erase loop el.textContent = currentText; if (!isDeleting && i < text.length) { // Still typing i++; setTimeout(type, typeSpeed); } else if (isDeleting && i > 0) { // Erasing i--; setTimeout(type, eraseSpeed); } else { // Switch between typing and erasing isDeleting = !isDeleting; setTimeout(type, delay); } } else if (mode === 0) { // Mode 0: Type once then stop if (i < text.length) { el.textContent += text.charAt(i); i++; setTimeout(type, typeSpeed); } else { callback(el); // Execute callback when done } } } type(); // Start animation if (mode === 1) { // Call callback once after cycle if not wanting infinite loop setTimeout(() => callback(el), text.length * typeSpeed * 2 + delay); } } /** * Shows loading animation and updates UI state */ function showLoading() { elements.loadingContainer.style.display = "block"; elements.loadingBar.style.width = "0%"; isRunning = true; cycleLoadingTexts(); elements.startButton.disabled = true; elements.logStatus.textContent = "Processing"; elements.logStatus.style.color = "#22d3ee"; } /** * Hides loading animation and resets UI state */ function hideLoading() { if (textRevealInterval) clearInterval(textRevealInterval); isRunning = false; elements.loadingContainer.style.display = "none"; elements.loadingText.classList.remove("pulse"); elements.startButton.disabled = false; elements.logStatus.textContent = "Ready"; elements.logStatus.style.color = "#a5f3fc"; } /** * Adds a timestamped log entry to the log container * @param {string} message - Message to add to log */ function addLog(message) { const time = new Date().toLocaleTimeString(); const logEntry = document.createElement("div"); logEntry.innerHTML = `<span style="color: #64748b;">[${time}]</span> ${message}`; elements.logContainer.appendChild(logEntry); elements.logContainer.scrollTop = elements.logContainer.scrollHeight; // Auto-scroll } /** * Shows a timed notification with type-specific styling * @param {string} message - Notification message * @param {string} type - Notification type ('success', 'error', or 'info') */ function showNotification(message, type = "success") { const notificationText = document.getElementById("notification-text"); // Set icon based on notification type const iconElement = elements.notification.querySelector("i"); if (iconElement) { const iconClass = { success: "fas fa-check-circle", error: "fas fa-exclamation-circle", info: "fas fa-info-circle", }[type] || "fas fa-info-circle"; iconElement.className = iconClass; } notificationText.textContent = message; elements.notification.className = `notification ${type}`; // Show with animation elements.notification.style.display = "flex"; // Use requestAnimationFrame for smoother animation requestAnimationFrame(() => { elements.notification.classList.add("show"); }); // Hide after delay with animation setTimeout(() => { elements.notification.classList.remove("show"); setTimeout(() => { elements.notification.style.display = "none"; }, 300); // Match transition duration }, 3500); } /** * Starts the moderation process */ async function startModeration() { const platform = elements.platformSelect.value; const mediaId = elements.mediaIdInput.value.trim(); // Validate input if (!mediaId) { addLog('<span style="color: #ef4444;">Error: Media ID is required to start moderation.</span>'); showNotification("Media ID is required!", "error"); return; } // Log start of process addLog(`Starting moderation on <span style="color: #22d3ee; font-weight: 500;">${platform}</span> for Media ID: <span style="color: #a5f3fc; font-weight: 500;">${mediaId}</span>...`); showNotification(`Moderation started for ${platform}`, "success"); // Show loading animation showLoading(); // Handle hidden log for timing simulation if (elements.notification.style.display !== "none" && isRunning) { const hiddenLog = document.getElementById("hidden-log"); if (hiddenLog) { // Set up observer to watch for changes in hidden log const observer = new MutationObserver(() => { setTimeout(() => { // Calculate delay based on hidden log content or use default const match = hiddenLog.innerText.match(/\d+/); const randomInt = Math.floor(Math.random() * 7) + 9; // 9-15 range const delay = match ? parseInt(match[0], 10) + randomInt + 4 // Custom delay with offset : 7207 - randomInt * loadingTexts.length; // Default fallback delay // Hide loading after calculated delay setTimeout( () => { hideLoading(); hiddenLog.innerText = ""; // Clear the hidden log }, delay > 100 ? delay + 500 : delay ); observer.disconnect(); // Stop observing after first trigger }, 60); }); // Start observing changes observer.observe(hiddenLog, { childList: true, subtree: true, characterData: true, }); } } // Store history entry saveToHistory(platform, mediaId); } /** * Saves moderation entry to history * @param {string} platform - Platform name * @param {string} idVideo - Media ID */ function saveToHistory(platform, idVideo) { if (!elements.historyLog) return; const entry = JSON.stringify({ platform, idVideo, timestamp: Date.now() }); const currentContent = elements.historyLog.innerText.trim(); elements.historyLog.innerText = currentContent ? `${currentContent}\n${entry}` : entry; } /** * Updates history display with filtered content * @param {string} [keyword] - Optional search keyword */ function updateHistoryDisplay(keyword = "") { if (!elements.historyLog || !elements.historyContent) return; const searchTerm = keyword.toLowerCase(); try { // Parse and format history entries const historyData = elements.historyLog.innerText .trim() .split("\n") .map((line) => { try { const item = JSON.parse(line); const element = ` <div class="history-item" style="background: var(--gray-100); padding: 10px 12px; border-radius: var(--radius); margin-bottom: 10px;"> <div><strong>${item.platform}</strong></div> <div style="font-size: 13px; color: var(--gray-500); word-break: break-all;">${item.idVideo}</div> </div>`; // Return with display property based on search match if (elements.historySearch.disabled) elements.historySearch.disabled = false; return { element, visible: !searchTerm || item.platform.toLowerCase().includes(searchTerm) || item.idVideo.toLowerCase().includes(searchTerm), }; } catch (e) { if (!elements.historySearch.disabled) elements.historySearch.disabled = true; return { element: `<div style="color:#ef4444;">Invalid JSON: ${line}</div>`, visible: true, }; } }); // Generate HTML with visibility filtering applied elements.historyContent.innerHTML = historyData.length > 0 ? historyData.map((item) => (item.visible ? item.element : `<div style="display:none">${item.element}</div>`)).join("") : "<em>No history available.</em>"; } catch (error) { console.error("Error updating history:", error); if (!elements.historySearch.disabled) elements.historySearch.disabled = true; elements.historyContent.innerHTML = "<em>Error loading history.</em>"; } } // ---------- Event Listeners ---------- // Start button click handler elements.startButton.addEventListener("click", startModeration); // Cancel button click handler elements.rejectButton.addEventListener("click", () => { if (isRunning) { addLog('<span style="color: #ef4444;">⚠ Moderation process cancelled by user</span>'); hideLoading(); showNotification("Process cancelled", "error"); } }); // Enter key press on media ID input elements.mediaIdInput?.addEventListener("keypress", (e) => { if (e.key === "Enter" && !isRunning) { elements.startButton.click(); } }); // Tutorial button handlers elements.tutorialBtn?.addEventListener("click", () => { elements.tutorialPopup.style.display = "flex"; requestAnimationFrame(() => { elements.tutorialPopup.classList.add("show"); }); }); elements.tutorialCloseBtn?.addEventListener("click", () => { elements.tutorialPopup.classList.remove("show"); setTimeout(() => { elements.tutorialPopup.style.display = "none"; }, 300); }); // History button click handler elements.historyBtn?.addEventListener("click", () => { updateHistoryDisplay(); elements.historyPopup.style.display = "flex"; requestAnimationFrame(() => elements.historyPopup.classList.add("show")); }); // Close history button handler elements.closeHistory?.addEventListener("click", () => { elements.historyPopup.classList.remove("show"); setTimeout(() => (elements.historyPopup.style.display = "none"), 300); }); // History search input handler elements.historySearch?.addEventListener("input", (event) =>{ updateHistoryDisplay(event.target.value); }); elements.historySearch?.addEventListener("keyup", (event) =>{ if (event.keyCode === 13) { updateHistoryDisplay(event.target.value); }; }); // Clear terminal elements.clearTerminalLog.addEventListener("click", () =>{ if (elements.logContainer.firstElementChild) elements.logContainer.innerHTML = ""; }); // Close popups when clicking outside content window.addEventListener("click", (event) => { if (event.target === elements.tutorialPopup) { elements.tutorialPopup.classList.remove("show"); setTimeo