Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>KW Manager</title> | |
| <style> | |
| * { box-sizing: border-box; } | |
| body { | |
| font: 14px sans-serif; | |
| padding: 20px; | |
| background: #f5f7fa; | |
| margin: 0; | |
| } | |
| .hdr { | |
| background: #fff; | |
| padding: 20px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| } | |
| .hdr h2 { | |
| margin: 0 0 15px 0; | |
| color: #1a73e8; | |
| } | |
| .stats { | |
| display: flex; | |
| gap: 20px; | |
| margin: 15px 0; | |
| flex-wrap: wrap; | |
| } | |
| .stat { | |
| background: #f8f9fa; | |
| padding: 15px; | |
| border-radius: 6px; | |
| min-width: 120px; | |
| } | |
| .stat strong { | |
| display: block; | |
| font-size: 28px; | |
| color: #1a73e8; | |
| font-weight: 600; | |
| } | |
| .stat span { | |
| font-size: 12px; | |
| color: #666; | |
| text-transform: uppercase; | |
| } | |
| .run-selector { | |
| margin: 15px 0; | |
| padding: 15px; | |
| background: #e8f5e9; | |
| border-radius: 6px; | |
| border-left: 4px solid #0f9d58; | |
| } | |
| .run-selector label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| .run-selector select { | |
| width: 100%; | |
| max-width: 400px; | |
| padding: 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| font-size: 14px; | |
| background: #fff; | |
| } | |
| .auto-search { | |
| margin: 20px 0; | |
| padding: 20px; | |
| background: #fff; | |
| border-radius: 8px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| border-left: 4px solid #f9ab00; | |
| } | |
| .auto-search h3 { | |
| margin: 0 0 10px 0; | |
| color: #f9ab00; | |
| font-size: 18px; | |
| } | |
| .auto-search p { | |
| margin: 0 0 15px 0; | |
| font-size: 12px; | |
| color: #666; | |
| } | |
| .search-input-group { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .search-input-group textarea { | |
| flex: 1; | |
| padding: 12px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| font-size: 14px; | |
| min-height: 80px; | |
| font-family: inherit; | |
| resize: vertical; | |
| } | |
| .search-input-group button { | |
| padding: 12px 24px; | |
| white-space: nowrap; | |
| align-self: flex-start; | |
| } | |
| .queue-list { | |
| margin-top: 15px; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| } | |
| .queue-item { | |
| background: #f8f9fa; | |
| padding: 12px 15px; | |
| margin: 5px 0; | |
| border-radius: 4px; | |
| font-size: 13px; | |
| } | |
| .queue-item.processing { | |
| background: #fff3cd; | |
| border-left: 3px solid #f9ab00; | |
| } | |
| .queue-item.completed { | |
| background: #d4edda; | |
| border-left: 3px solid #28a745; | |
| } | |
| .queue-item.error { | |
| background: #f8d7da; | |
| border-left: 3px solid #dc3545; | |
| } | |
| .queue-main { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 5px; | |
| } | |
| .queue-detail { | |
| font-size: 11px; | |
| color: #666; | |
| margin-top: 4px; | |
| } | |
| .run-info { | |
| background: #e3f2fd; | |
| padding: 8px 12px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| margin-top: 10px; | |
| color: #1565c0; | |
| } | |
| .btn { | |
| padding: 10px 20px; | |
| margin: 5px; | |
| border: none; | |
| background: #1a73e8; | |
| color: #fff; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| .btn:hover { background: #1557b0; } | |
| .btn:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .bg { background: #0f9d58; } | |
| .bg:hover { background: #0d8547; } | |
| .by { background: #f9ab00; } | |
| .by:hover { background: #e69500; } | |
| .br { background: #d93025; } | |
| .br:hover { background: #c5221f; } | |
| .bs { | |
| padding: 6px 12px; | |
| font-size: 12px; | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0,0,0,0.6); | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .modal.show { display: flex; } | |
| .mc { | |
| background: #fff; | |
| padding: 30px; | |
| border-radius: 12px; | |
| max-width: 900px; | |
| width: 90%; | |
| max-height: 85vh; | |
| overflow-y: auto; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| } | |
| .mc h3 { | |
| margin-top: 0; | |
| color: #1a73e8; | |
| font-size: 24px; | |
| } | |
| .mc input { | |
| width: 100%; | |
| padding: 12px; | |
| margin: 10px 0; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| font-size: 14px; | |
| } | |
| .ri { | |
| background: #fff; | |
| padding: 15px; | |
| margin: 10px 0; | |
| border-radius: 8px; | |
| border-left: 4px solid #1a73e8; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| } | |
| .rh { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .rh strong { | |
| font-size: 16px; | |
| color: #333; | |
| } | |
| .rh small { | |
| color: #666; | |
| font-size: 12px; | |
| } | |
| .ri-stats { | |
| display: flex; | |
| gap: 15px; | |
| margin-top: 8px; | |
| font-size: 13px; | |
| color: #666; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 13px; | |
| margin-top: 20px; | |
| background: #fff; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| } | |
| th, td { | |
| border: 1px solid #e0e0e0; | |
| padding: 10px; | |
| text-align: left; | |
| } | |
| th { | |
| background: #f8f9fa; | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| tr:hover { background: #f5f5f5; } | |
| .tag { | |
| padding: 4px 10px; | |
| background: #e3f2fd; | |
| border-radius: 3px; | |
| font-size: 11px; | |
| font-weight: 500; | |
| color: #1565c0; | |
| display: inline-block; | |
| } | |
| .seed { | |
| background: #fff3cd; | |
| padding: 8px 15px; | |
| border-radius: 4px; | |
| display: inline-block; | |
| margin-bottom: 10px; | |
| font-weight: 600; | |
| color: #856404; | |
| } | |
| .preview-collapsed { | |
| max-height: 400px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .preview-collapsed::after { | |
| content: ""; | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| height: 100px; | |
| background: linear-gradient(transparent, #fff); | |
| pointer-events: none; | |
| } | |
| .expand-btn { | |
| margin-top: 10px; | |
| text-align: center; | |
| } | |
| .welcome { | |
| text-align: center; | |
| padding: 60px 20px; | |
| background: #fff; | |
| border-radius: 8px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| } | |
| .welcome h1 { | |
| color: #1a73e8; | |
| margin-bottom: 20px; | |
| } | |
| .welcome p { | |
| color: #666; | |
| font-size: 16px; | |
| margin: 10px 0; | |
| } | |
| .welcome code { | |
| background: #f8f9fa; | |
| padding: 2px 8px; | |
| border-radius: 3px; | |
| font-family: monospace; | |
| color: #d93025; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="welcomeScreen" class="welcome"> | |
| <h1>🎯 KW Manager</h1> | |
| <p><strong>Ready to collect keywords!</strong></p> | |
| <p>Run the bookmarklet on any Google SERP page with the keyword explorer extension active.</p> | |
| <p>Your data will be stored here permanently at:</p> | |
| <p><code id="currentUrl"></code></p> | |
| <p style="margin-top: 30px; color: #999; font-size: 12px;">Waiting for data...</p> | |
| </div> | |
| <div id="mainContent" style="display:none;"> | |
| <div class="hdr"> | |
| <h2>KW Manager</h2> | |
| <div class="run-selector"> | |
| <label>Current Run:</label> | |
| <select id="runSelect"> | |
| <option value="">Select a run...</option> | |
| </select> | |
| <button id="newRunBtn" class="btn bg bs" style="margin-left:10px">+ New Run</button> | |
| <button id="switchRunBtn" class="btn bs">Switch Run</button> | |
| </div> | |
| <div class="stats"> | |
| <div class="stat"> | |
| <strong id="cc">0</strong> | |
| <span>Current Batch</span> | |
| </div> | |
| <div class="stat"> | |
| <strong id="rc">0</strong> | |
| <span>Run Keywords</span> | |
| </div> | |
| <div class="stat"> | |
| <strong id="tr">0</strong> | |
| <span>Total Runs</span> | |
| </div> | |
| <div class="stat"> | |
| <strong id="tk">0</strong> | |
| <span>Total Keywords</span> | |
| </div> | |
| </div> | |
| <div> | |
| <button id="addBtn" class="btn bg">Add to Current Run</button> | |
| <button id="manageBtn" class="btn by">Manage Runs</button> | |
| <button id="dlBtn" class="btn">Download Current</button> | |
| <button id="cpBtn" class="btn">Copy Current</button> | |
| </div> | |
| </div> | |
| <div class="auto-search"> | |
| <h3>🔄 Auto-Search Queue</h3> | |
| <p>Enter keywords separated by commas or new lines (e.g., "keyword1, keyword2" or one per line)</p> | |
| <div class="search-input-group"> | |
| <textarea id="seedInput" placeholder="Enter seed keywords... keyword1, keyword2, keyword3 or one per line"></textarea> | |
| <button id="addQueueBtn" class="btn by">Add to Queue</button> | |
| </div> | |
| <div style="display:flex;gap:10px;margin-bottom:10px"> | |
| <button id="startQueueBtn" class="btn bg" disabled>Start Queue</button> | |
| <button id="stopQueueBtn" class="btn br" style="display:none">Stop</button> | |
| <button id="clearQueueBtn" class="btn" style="background:#666">Clear Queue</button> | |
| </div> | |
| <div class="run-info" id="targetRunInfo">⚠️ Select or create a run first</div> | |
| <div class="queue-list" id="queueList"></div> | |
| </div> | |
| <div id="manageModal" class="modal"> | |
| <div class="mc"> | |
| <h3>Manage Runs</h3> | |
| <div id="runsList"></div> | |
| <button id="closeManage" class="btn" style="margin-top:20px">Close</button> | |
| </div> | |
| </div> | |
| <div id="newRunModal" class="modal"> | |
| <div class="mc" style="max-width:500px"> | |
| <h3>Create New Run</h3> | |
| <p>Enter a name for this run:</p> | |
| <input type="text" id="newRunName" placeholder="e.g., Main Keywords, Client Project, etc."> | |
| <div style="margin-top:20px"> | |
| <button id="cancelNewRun" class="btn" style="background:#666">Cancel</button> | |
| <button id="saveNewRun" class="btn bg">Create Run</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="previewSection" style="overflow:auto;background:#fff;padding:20px;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.1)"> | |
| <div class="seed" id="seedDisplay">Seed Keyword: -</div> | |
| <h3>Preview (<span id="previewCount">0</span> keywords)</h3> | |
| <div id="previewContainer" class="preview-collapsed"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Source</th> | |
| <th>Keyword</th> | |
| <th>Volume</th> | |
| <th>CPC</th> | |
| <th>Competition</th> | |
| </tr> | |
| </thead> | |
| <tbody id="previewBody"> | |
| <tr> | |
| <td colspan="5" style="text-align:center;color:#666;padding:40px">No data yet - run the bookmarklet to collect keywords</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="expand-btn"> | |
| <button id="expandBtn" class="btn bs">Show All Keywords</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Display current URL | |
| document.getElementById('currentUrl').textContent = window.location.href; | |
| // This will be populated by the bookmarklet | |
| window.kwManagerInit = function(data, header, seed) { | |
| document.getElementById('welcomeScreen').style.display = 'none'; | |
| document.getElementById('mainContent').style.display = 'block'; | |
| if (!window.kwManager) { | |
| window.kwManager = { | |
| initialData: data, | |
| header: header, | |
| seed: seed, | |
| allPreviewData: [...data], | |
| queue: [], | |
| isProcessing: false, | |
| shouldStop: false | |
| }; | |
| initializeApp(); | |
| } else { | |
| // Update with new data | |
| window.kwManager.initialData = data; | |
| window.kwManager.allPreviewData = [...window.kwManager.allPreviewData, ...data]; | |
| document.getElementById('cc').textContent = data.length; | |
| document.getElementById('seedDisplay').textContent = 'Seed Keyword: ' + seed; | |
| document.getElementById('previewCount').textContent = window.kwManager.allPreviewData.length; | |
| refreshPreviewTable(); | |
| } | |
| }; | |
| function initializeApp() { | |
| const km = window.kwManager; | |
| const boxAbbrev = {0:"PAA", 1:"Topical", 2:"SERP", 3:"Related", 4:"Long-Tail"}; | |
| document.getElementById('cc').textContent = km.initialData.length; | |
| document.getElementById('seedDisplay').textContent = 'Seed Keyword: ' + km.seed; | |
| document.getElementById('previewCount').textContent = km.allPreviewData.length; | |
| // Render initial preview | |
| refreshPreviewTable(); | |
| function getRuns() { | |
| const r = []; | |
| for (let k in localStorage) { | |
| if (k.startsWith("kw_run_")) { | |
| try { r.push(JSON.parse(localStorage[k])); } catch(e) {} | |
| } | |
| } | |
| return r.sort((a,b) => b.timestamp - a.timestamp); | |
| } | |
| function getCurrentRun() { | |
| return localStorage.getItem("kw_current_run") || ""; | |
| } | |
| function setCurrentRun(id) { | |
| localStorage.setItem("kw_current_run", id); | |
| updateStats(); | |
| updateRunSelector(); | |
| updateTargetRunInfo(); | |
| } | |
| function updateStats() { | |
| const runs = getRuns(); | |
| const total = runs.reduce((s,r) => s + r.keywords.length, 0); | |
| document.getElementById("tr").textContent = runs.length; | |
| document.getElementById("tk").textContent = total; | |
| const curr = getCurrentRun(); | |
| if (curr) { | |
| const run = runs.find(r => r.id === curr); | |
| if (run) { | |
| const unique = new Set(run.keywords.map(k => k.data)).size; | |
| document.getElementById("rc").textContent = unique; | |
| } else { | |
| document.getElementById("rc").textContent = 0; | |
| } | |
| } else { | |
| document.getElementById("rc").textContent = 0; | |
| } | |
| } | |
| function updateRunSelector() { | |
| const sel = document.getElementById("runSelect"); | |
| const curr = getCurrentRun(); | |
| const runs = getRuns(); | |
| sel.innerHTML = '<option value="">Select a run...</option>'; | |
| runs.forEach(r => { | |
| const opt = document.createElement("option"); | |
| opt.value = r.id; | |
| opt.textContent = r.name + " (" + r.keywords.length + " kw)"; | |
| if (r.id === curr) opt.selected = true; | |
| sel.appendChild(opt); | |
| }); | |
| if (curr) { | |
| const run = runs.find(r => r.id === curr); | |
| if (run) { | |
| document.querySelector(".run-selector").style.background = "#e8f5e9"; | |
| document.querySelector(".run-selector").style.borderColor = "#0f9d58"; | |
| } | |
| } | |
| } | |
| function updateTargetRunInfo() { | |
| const curr = getCurrentRun(); | |
| const info = document.getElementById("targetRunInfo"); | |
| if (!curr) { | |
| info.innerHTML = "⚠️ Select or create a run first"; | |
| info.style.background = "#fff3cd"; | |
| info.style.color = "#856404"; | |
| } else { | |
| const runs = getRuns(); | |
| const run = runs.find(r => r.id === curr); | |
| if (run) { | |
| info.innerHTML = "✓ Keywords will be added to: <strong>" + run.name + "</strong> (" + run.keywords.length + " kw)"; | |
| info.style.background = "#d4edda"; | |
| info.style.color = "#155724"; | |
| } | |
| } | |
| } | |
| function updateQueueDisplay() { | |
| const list = document.getElementById("queueList"); | |
| if (km.queue.length === 0) { | |
| list.innerHTML = '<div style="text-align:center;padding:20px;color:#666">No keywords in queue</div>'; | |
| return; | |
| } | |
| let html = ""; | |
| km.queue.forEach((item, i) => { | |
| let cls = "queue-item"; | |
| let statusText = "⏳ Waiting"; | |
| let detail = ""; | |
| if (item.status === "processing") { | |
| cls += " processing"; | |
| statusText = "🔄 Processing..."; | |
| detail = '<div class="queue-detail">Waiting for extension buttons to load...</div>'; | |
| } else if (item.status === "completed") { | |
| cls += " completed"; | |
| statusText = "✓ Completed"; | |
| const sources = []; | |
| if (item.boxCounts) { | |
| for (let b in item.boxCounts) { | |
| if (item.boxCounts[b] > 0) sources.push(boxAbbrev[b] + ": " + item.boxCounts[b]); | |
| } | |
| } | |
| detail = '<div class="queue-detail">' + (item.count || 0) + ' kw > ' + (sources.length > 0 ? sources.join(", ") : "No data") + '</div>'; | |
| } else if (item.status === "error") { | |
| cls += " error"; | |
| statusText = "✗ Error"; | |
| detail = '<div class="queue-detail">Failed to collect data</div>'; | |
| } | |
| html += '<div class="' + cls + '"><div class="queue-main"><div><strong>' + item.keyword + '</strong> - ' + statusText + '</div><button onclick="removeFromQueue(' + i + ')" class="btn br bs" ' + ((item.status === "processing") ? "disabled" : "") + '>Remove</button></div>' + detail + '</div>'; | |
| }); | |
| list.innerHTML = html; | |
| } | |
| window.removeFromQueue = function(idx) { | |
| if (km.isProcessing && km.queue[idx].status === "processing") { | |
| alert("Cannot remove currently processing item"); | |
| return; | |
| } | |
| km.queue.splice(idx, 1); | |
| updateQueueDisplay(); | |
| if (km.queue.length === 0) { | |
| document.getElementById("startQueueBtn").disabled = true; | |
| } | |
| }; | |
| document.getElementById("addQueueBtn").onclick = function() { | |
| const input = document.getElementById("seedInput"); | |
| const text = input.value.trim(); | |
| if (!text) { | |
| alert("Enter keywords"); | |
| return; | |
| } | |
| const keywords = text.split(/[,\n]+/).map(k => k.trim()).filter(k => k.length > 0); | |
| if (keywords.length === 0) { | |
| alert("No valid keywords found"); | |
| return; | |
| } | |
| let added = 0; | |
| keywords.forEach(kw => { | |
| if (!km.queue.some(q => q.keyword.toLowerCase() === kw.toLowerCase())) { | |
| km.queue.push({keyword: kw, status: "waiting", count: 0, boxCounts: {}}); | |
| added++; | |
| } | |
| }); | |
| input.value = ""; | |
| updateQueueDisplay(); | |
| document.getElementById("startQueueBtn").disabled = false; | |
| alert("Added " + added + " keyword(s) to queue"); | |
| }; | |
| document.getElementById("clearQueueBtn").onclick = function() { | |
| if (km.isProcessing) { | |
| alert("Cannot clear queue while processing"); | |
| return; | |
| } | |
| if (km.queue.length === 0) { | |
| alert("Queue is already empty"); | |
| return; | |
| } | |
| if (confirm("Clear all " + km.queue.length + " keywords from queue?")) { | |
| km.queue = []; | |
| updateQueueDisplay(); | |
| document.getElementById("startQueueBtn").disabled = true; | |
| } | |
| }; | |
| document.getElementById("startQueueBtn").onclick = async function() { | |
| const curr = getCurrentRun(); | |
| if (!curr) { | |
| alert("Please select or create a run first!"); | |
| return; | |
| } | |
| if (km.queue.length === 0) { | |
| alert("Add keywords to queue first"); | |
| return; | |
| } | |
| km.isProcessing = true; | |
| km.shouldStop = false; | |
| document.getElementById("startQueueBtn").style.display = "none"; | |
| document.getElementById("stopQueueBtn").style.display = "inline-block"; | |
| document.getElementById("addQueueBtn").disabled = true; | |
| document.getElementById("clearQueueBtn").disabled = true; | |
| for (let i = 0; i < km.queue.length; i++) { | |
| if (km.shouldStop) { | |
| km.queue[i].status = "waiting"; | |
| break; | |
| } | |
| km.queue[i].status = "processing"; | |
| updateQueueDisplay(); | |
| const delay = Math.floor(Math.random() * 4000) + 6000; | |
| await new Promise(r => setTimeout(r, delay)); | |
| try { | |
| const result = await searchAndCollect(km.queue[i].keyword); | |
| if (result.success) { | |
| km.queue[i].status = "completed"; | |
| km.queue[i].count = result.count; | |
| km.queue[i].boxCounts = result.boxCounts; | |
| addToPreview(result.data); | |
| } else { | |
| km.queue[i].status = "error"; | |
| } | |
| } catch(e) { | |
| km.queue[i].status = "error"; | |
| } | |
| updateQueueDisplay(); | |
| updateStats(); | |
| } | |
| km.isProcessing = false; | |
| document.getElementById("startQueueBtn").style.display = "inline-block"; | |
| document.getElementById("stopQueueBtn").style.display = "none"; | |
| document.getElementById("addQueueBtn").disabled = false; | |
| document.getElementById("clearQueueBtn").disabled = false; | |
| alert("Queue processing complete!"); | |
| }; | |
| document.getElementById("stopQueueBtn").onclick = function() { | |
| km.shouldStop = true; | |
| this.disabled = true; | |
| this.textContent = "Stopping..."; | |
| }; | |
| async function searchAndCollect(keyword) { | |
| return new Promise((resolve) => { | |
| const searchUrl = "https://www.google.com/search?q=" + encodeURIComponent(keyword); | |
| const tab = window.open(searchUrl, "_blank"); | |
| if (!tab) { | |
| resolve({success: false, count: 0, data: [], boxCounts: {}}); | |
| return; | |
| } | |
| let waited = 0; | |
| const waitForButtons = setInterval(async () => { | |
| waited += 500; | |
| try { | |
| if (tab.closed) { | |
| clearInterval(waitForButtons); | |
| resolve({success: false, count: 0, data: [], boxCounts: {}}); | |
| } else if (waited >= 12000) { | |
| const buttons = tab.document.querySelectorAll("button.xt-copy-csv.xt-ke-btn"); | |
| if (buttons.length === 5) { | |
| clearInterval(waitForButtons); | |
| let collectedData = []; | |
| let boxCounts = {0:0, 1:0, 2:0, 3:0, 4:0}; | |
| const boxLabels = ["People Also Ask", "Topical Keywords", "SERP Keywords", "Related Keywords", "Long-Tail Keywords"]; | |
| for (let i = 0; i < 5; i++) { | |
| buttons[i].click(); | |
| await new Promise(r => setTimeout(r, 800)); | |
| try { | |
| const clipText = await tab.navigator.clipboard.readText(); | |
| const rows = clipText.trim().split("\n"); | |
| rows.slice(1).forEach(row => { | |
| if (row.trim() && row.includes(";")) { | |
| collectedData.push({box: boxLabels[i], data: row}); | |
| boxCounts[i]++; | |
| } | |
| }); | |
| } catch(e) {} | |
| } | |
| const curr = getCurrentRun(); | |
| const runs = getRuns(); | |
| const run = runs.find(r => r.id === curr); | |
| if (run) { | |
| const existing = new Set(run.keywords.map(k => k.data)); | |
| collectedData.forEach(item => { | |
| if (!existing.has(item.data)) { | |
| run.keywords.push({box: item.box, data: item.data, added: Date.now()}); | |
| existing.add(item.data); | |
| } | |
| }); | |
| localStorage.setItem(run.id, JSON.stringify(run)); | |
| } | |
| tab.close(); | |
| resolve({success: true, count: collectedData.length, data: collectedData, boxCounts: boxCounts}); | |
| } else { | |
| clearInterval(waitForButtons); | |
| tab.close(); | |
| resolve({success: false, count: 0, data: [], boxCounts: {}}); | |
| } | |
| } else { | |
| try { | |
| const buttons = tab.document.querySelectorAll("button.xt-copy-csv.xt-ke-btn"); | |
| if (buttons.length === 5) { | |
| clearInterval(waitForButtons); | |
| let collectedData = []; | |
| let boxCounts = {0:0, 1:0, 2:0, 3:0, 4:0}; | |
| const boxLabels = ["People Also Ask", "Topical Keywords", "SERP Keywords", "Related Keywords", "Long-Tail Keywords"]; | |
| for (let i = 0; i < 5; i++) { | |
| buttons[i].click(); | |
| await new Promise(r => setTimeout(r, 800)); | |
| try { | |
| const clipText = await tab.navigator.clipboard.readText(); | |
| const rows = clipText.trim().split("\n"); | |
| rows.slice(1).forEach(row => { | |
| if (row.trim() && row.includes(";")) { | |
| collectedData.push({box: boxLabels[i], data: row}); | |
| boxCounts[i]++; | |
| } | |
| }); | |
| } catch(e) {} | |
| } | |
| const curr = getCurrentRun(); | |
| const runs = getRuns(); | |
| const run = runs.find(r => r.id === curr); | |
| if (run) { | |
| const existing = new Set(run.keywords.map(k => k.data)); | |
| collectedData.forEach(item => { | |
| if (!existing.has(item.data)) { | |
| run.keywords.push({box: item.box, data: item.data, added: Date.now()}); | |
| existing.add(item.data); | |
| } | |
| }); | |
| localStorage.setItem(run.id, JSON.stringify(run)); | |
| } | |
| tab.close(); | |
| resolve({success: true, count: collectedData.length, data: collectedData, boxCounts: boxCounts}); | |
| } | |
| } catch(e) {} | |
| } | |
| } catch(e) {} | |
| }, 500); | |
| setTimeout(() => { | |
| clearInterval(waitForButtons); | |
| if (!tab.closed) tab.close(); | |
| resolve({success: false, count: 0, data: [], boxCounts: {}}); | |
| }, 60000); | |
| }); | |
| } | |
| function addToPreview(newData) { | |
| km.allPreviewData = [...km.allPreviewData, ...newData]; | |
| document.getElementById("previewCount").textContent = km.allPreviewData.length; | |
| const container = document.getElementById("previewContainer"); | |
| if (!container.classList.contains("preview-collapsed")) { | |
| refreshPreviewTable(); | |
| } | |
| } | |
| function refreshPreviewTable() { | |
| const tbody = document.getElementById("previewBody"); | |
| const container = document.getElementById("previewContainer"); | |
| const showAll = !container.classList.contains("preview-collapsed"); | |
| const dataToShow = showAll ? km.allPreviewData : km.allPreviewData.slice(0, 30); | |
| let html = ""; | |
| dataToShow.forEach(x => { | |
| const p = x.data.split(";").map(v => v.replace(/"/g, "").trim()); | |
| html += '<tr><td><span class="tag">' + x.box + '</span></td><td>' + p[0] + '</td><td>' + p[1] + '</td><td>' + p[2] + '</td><td>' + p[3] + '</td></tr>'; | |
| }); | |
| if (!showAll && km.allPreviewData.length > 30) { | |
| html += '<tr><td colspan="5" style="text-align:center;color:#666;font-style:italic">... and ' + (km.allPreviewData.length - 30) + ' more keywords (click "Show All Keywords" to expand)</td></tr>'; | |
| } | |
| tbody.innerHTML = html; | |
| } | |
| document.getElementById("expandBtn").onclick = function() { | |
| const container = document.getElementById("previewContainer"); | |
| if (container.classList.contains("preview-collapsed")) { | |
| container.classList.remove("preview-collapsed"); | |
| this.textContent = "Show Less"; | |
| refreshPreviewTable(); | |
| } else { | |
| container.classList.add("preview-collapsed"); | |
| this.textContent = "Show All Keywords"; | |
| refreshPreviewTable(); | |
| } | |
| }; | |
| document.getElementById("runSelect").onchange = function() { | |
| if (this.value) { | |
| setCurrentRun(this.value); | |
| } | |
| }; | |
| document.getElementById("switchRunBtn").onclick = function() { | |
| const sel = document.getElementById("runSelect"); | |
| if (sel.value) { | |
| setCurrentRun(sel.value); | |
| alert("Switched to: " + sel.options[sel.selectedIndex].text); | |
| } else { | |
| alert("Please select a run first"); | |
| } | |
| }; | |
| document.getElementById("newRunBtn").onclick = function() { | |
| document.getElementById("newRunName").value = ""; | |
| document.getElementById("newRunModal").classList.add("show"); | |
| document.getElementById("newRunName").focus(); | |
| }; | |
| document.getElementById("cancelNewRun").onclick = function() { | |
| document.getElementById("newRunModal").classList.remove("show"); | |
| }; | |
| document.getElementById("saveNewRun").onclick = function() { | |
| const name = document.getElementById("newRunName").value.trim(); | |
| if (!name) { | |
| alert("Please enter a run name"); | |
| return; | |
| } | |
| const id = "kw_run_" + Date.now(); | |
| const run = { | |
| id: id, | |
| name: name, | |
| timestamp: Date.now(), | |
| created: new Date().toLocaleString(), | |
| keywords: [] | |
| }; | |
| try { | |
| localStorage.setItem(id, JSON.stringify(run)); | |
| setCurrentRun(id); | |
| document.getElementById("newRunModal").classList.remove("show"); | |
| alert("Created new run: " + name); | |
| updateStats(); | |
| updateRunSelector(); | |
| } catch(e) { | |
| alert("Error creating run: " + e.message); | |
| } | |
| }; | |
| document.getElementById("addBtn").onclick = function() { | |
| const curr = getCurrentRun(); | |
| if (!curr) { | |
| alert("Please create or select a run first!"); | |
| document.getElementById("newRunBtn").click(); | |
| return; | |
| } | |
| const runs = getRuns(); | |
| const run = runs.find(r => r.id === curr); | |
| if (!run) { | |
| alert("Run not found!"); | |
| return; | |
| } | |
| const existing = new Set(run.keywords.map(k => k.data)); | |
| let added = 0; | |
| km.initialData.forEach(item => { | |
| if (!existing.has(item.data)) { | |
| run.keywords.push({box: item.box, data: item.data, added: Date.now()}); | |
| existing.add(item.data); | |
| added++; | |
| } | |
| }); | |
| try { | |
| localStorage.setItem(run.id, JSON.stringify(run)); | |
| alert("Added " + added + " new keywords to \"" + run.name + "\"\nTotal in run: " + run.keywords.length); | |
| updateStats(); | |
| updateRunSelector(); | |
| } catch(e) { | |
| alert("Error: " + e.message); | |
| } | |
| }; | |
| document.getElementById("manageBtn").onclick = function() { | |
| const runs = getRuns(); | |
| let html = ""; | |
| if (!runs.length) { | |
| html = '<div style="text-align:center;padding:40px;color:#666"><p style="font-size:18px">No runs yet</p><p>Create your first run to start organizing keywords!</p></div>'; | |
| } else { | |
| runs.forEach((r, i) => { | |
| const unique = new Set(r.keywords.map(k => k.data)).size; | |
| html += '<div class="ri"><div class="rh"><div><strong>' + r.name + '</strong><br><small>Created: ' + r.created + '</small></div><div><button onclick="exportRun(\'' + r.id + '\')" class="btn bg bs">Export</button> <button onclick="deleteRun(\'' + r.id + '\')" class="btn br bs">Delete</button></div></div><div class="ri-stats"><span><strong>' + r.keywords.length + '</strong> total keywords</span><span><strong>' + unique + '</strong> unique</span></div></div>'; | |
| }); | |
| } | |
| document.getElementById("runsList").innerHTML = html; | |
| document.getElementById("manageModal").classList.add("show"); | |
| }; | |
| document.getElementById("closeManage").onclick = function() { | |
| document.getElementById("manageModal").classList.remove("show"); | |
| }; | |
| window.exportRun = function(id) { | |
| const runs = getRuns(); | |
| const run = runs.find(r => r.id === id); | |
| if (!run) { | |
| alert("Run not found"); | |
| return; | |
| } | |
| const rows = [["Source", "Keyword", "Volume", "CPC", "Competition"]]; | |
| run.keywords.forEach(k => { | |
| const p = k.data.split(";").map(v => v.replace(/"/g, "").trim()); | |
| rows.push([k.box, p[0] || "", p[1] || "", p[2] || "", p[3] || ""]); | |
| }); | |
| const csv = rows.map(row => row.map(cell => { | |
| const s = String(cell); | |
| return (s.includes(",") || s.includes("\n") || s.includes('"')) ? '"' + s.replace(/"/g, '""') + '"' : s; | |
| }).join(",")).join("\n"); | |
| const blob = new Blob(["\uFEFF" + csv], {type: "text/csv"}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = run.name.replace(/[^a-z0-9]/gi, "_") + "_" + Date.now() + ".csv"; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| alert("Exported: " + run.name); | |
| }; | |
| window.deleteRun = function(id) { | |
| const runs = getRuns(); | |
| const run = runs.find(r => r.id === id); | |
| if (!run) { | |
| alert("Run not found"); | |
| return; | |
| } | |
| if (!confirm('Delete "' + run.name + '" with ' + run.keywords.length + ' keywords?\n\nThis cannot be undone!')) { | |
| return; | |
| } | |
| localStorage.removeItem(id); | |
| if (getCurrentRun() === id) { | |
| localStorage.removeItem("kw_current_run"); | |
| } | |
| alert("Deleted: " + run.name); | |
| updateStats(); | |
| updateRunSelector(); | |
| document.getElementById("manageBtn").click(); | |
| }; | |
| document.getElementById("dlBtn").onclick = function() { | |
| const curr = getCurrentRun(); | |
| if (!curr) { | |
| alert("No run selected"); | |
| return; | |
| } | |
| const runs = getRuns(); | |
| const run = runs.find(r => r.id === curr); | |
| if (!run) { | |
| alert("Run not found"); | |
| return; | |
| } | |
| exportRun(run.id); | |
| }; | |
| document.getElementById("cpBtn").onclick = function() { | |
| const curr = getCurrentRun(); | |
| if (!curr) { | |
| alert("No run selected"); | |
| return; | |
| } | |
| const runs = getRuns(); | |
| const run = runs.find(r => r.id === curr); | |
| if (!run) { | |
| alert("Run not found"); | |
| return; | |
| } | |
| const rows = [["Source", "Keyword", "Volume", "CPC", "Competition"]]; | |
| run.keywords.forEach(k => { | |
| const p = k.data.split(";").map(v => v.replace(/"/g, "").trim()); | |
| rows.push([k.box, p[0] || "", p[1] || "", p[2] || "", p[3] || ""]); | |
| }); | |
| const txt = rows.map(row => row.join("\t")).join("\n"); | |
| navigator.clipboard.writeText(txt).then(() => { | |
| alert("Copied " + run.keywords.length + " keywords!\nPaste into Google Sheets with Ctrl+V"); | |
| }).catch(() => { | |
| const ta = document.createElement("textarea"); | |
| ta.value = txt; | |
| ta.style.position = "fixed"; | |
| ta.style.left = "-9999px"; | |
| document.body.appendChild(ta); | |
| ta.select(); | |
| document.execCommand("copy"); | |
| document.body.removeChild(ta); | |
| alert("Copied!"); | |
| }); | |
| }; | |
| updateStats(); | |
| updateRunSelector(); | |
| updateTargetRunInfo(); | |
| } | |
| </script> | |
| </body> | |
| </html> |