const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); function normMarker(v) { return String(v || '') .trim() .replace(/^"+|"+$/g, '') .replace(/[\s_]+/g, '') .toUpperCase(); } function isVisible(el) { if (!el) return false; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false; const rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } async function waitForInputEl(timeoutMs) { const start = Date.now(); while (Date.now() - start < timeoutMs) { const named = document.querySelector('textarea[name="search"]'); if (isVisible(named)) return named; const els = [ ...document.querySelectorAll('textarea'), ...document.querySelectorAll('div[contenteditable="true"]'), ].filter(isVisible); const best = els.find(e => (e.getAttribute('aria-label') || '').toLowerCase().includes('message')) || els.find(e => (e.getAttribute('aria-label') || '').toLowerCase().includes('prompt')) || els[0]; if (best) return best; await sleep(250); } return null; } function setInputText(el, text) { el.focus(); if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const desc = Object.getOwnPropertyDescriptor(proto, 'value'); if (desc && desc.set) desc.set.call(el, text); else el.value = text; try { el.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'insertFromPaste', data: text })); } catch {} try { el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste', data: text })); } catch { el.dispatchEvent(new Event('input', { bubbles: true })); } el.dispatchEvent(new Event('change', { bubbles: true })); return; } const editable = el.getAttribute('contenteditable') === 'true'; if (editable) { el.focus(); const sel = window.getSelection && window.getSelection(); if (sel) { const range = document.createRange(); range.selectNodeContents(el); sel.removeAllRanges(); sel.addRange(range); } try { document.execCommand('insertText', false, text); } catch { el.textContent = text; } } else { el.textContent = text; } try { el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste', data: text })); } catch { el.dispatchEvent(new Event('input', { bubbles: true })); } } function isAriaDisabled(el) { const v = (el?.getAttribute('aria-disabled') || '').toLowerCase(); return v === 'true'; } function findSendButtonNear(inputEl) { const containers = []; let p = inputEl; for (let i = 0; i < 7 && p; i++) { if (p.parentElement) containers.push(p.parentElement); p = p.parentElement; } const selectors = [ 'div._52c986b[role="button"]', 'div._52c986b', 'div.ds-icon-button[role="button"]', '[role="button"].ds-icon-button', 'button[type="submit"]', 'button[aria-label*="Send"]', 'button[aria-label*="send"]', ]; for (const c of containers) { for (const sel of selectors) { const btns = [...c.querySelectorAll(sel)].filter(isVisible); const usable = btns.filter(b => !isAriaDisabled(b)); if (usable.length) return usable[usable.length - 1]; } } const any = [ ...document.querySelectorAll('div._52c986b[role="button"]'), ...document.querySelectorAll('div._52c986b'), ...document.querySelectorAll('div.ds-icon-button[role="button"]'), ].filter(isVisible); const usable = any.filter(b => !isAriaDisabled(b)); return usable.length ? usable[usable.length - 1] : null; } function clickLikeAUser(el) { if (!el) return; const r = el.getBoundingClientRect ? el.getBoundingClientRect() : null; const clientX = r ? Math.round(r.left + r.width / 2) : 1; const clientY = r ? Math.round(r.top + r.height / 2) : 1; try { el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX, clientY, pointerType: 'mouse', isPrimary: true })); } catch {} try { el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX, clientY })); } catch {} try { el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX, clientY, pointerType: 'mouse', isPrimary: true })); } catch {} try { el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX, clientY })); } catch {} try { el.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX, clientY })); } catch {} try { el.click(); } catch {} } function isGenerating() { const stop = document.querySelector('[aria-label*="Stop"], [aria-label*="stop"]'); if (isVisible(stop)) return true; const progress = document.querySelector('[role="progressbar"]'); if (isVisible(progress)) return true; const spinner = document.querySelector('.ds-loading, .loading, [class*="loading"]'); if (isVisible(spinner)) return true; return false; } function extractJsonObjects(text, maxObjects = 25) { if (!text) return []; const s = String(text); const out = []; let i = 0; while (i < s.length && out.length < maxObjects) { const start = s.indexOf('{', i); if (start === -1) break; let depth = 0; let inStr = false; let esc = false; for (let j = start; j < s.length; j++) { const ch = s[j]; if (inStr) { if (esc) esc = false; else if (ch === '\\\\') esc = true; else if (ch === '"') inStr = false; continue; } if (ch === '"') { inStr = true; continue; } if (ch === '{') depth++; else if (ch === '}') { depth--; if (depth === 0) { out.push(s.slice(start, j + 1)); i = j + 1; break; } } if (j === s.length - 1) i = s.length; } if (i === start) i = start + 1; } return out; } function chooseBestJsonFromText(text, marker) { const markerNorm = normMarker(marker); const candidates = extractJsonObjects(text, 25); const expectedKeys = [ 'summary', 'store_description', 'features', 'sound_profile', 'highlight_tracks', 'production', 'market_insight', 'sales_angles', 'embedding', ]; let best = null; let bestScore = -Infinity; for (const cand of candidates) { let parsed; try { parsed = JSON.parse(cand); } catch { continue; } if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue; let score = 0; const eomNorm = normMarker(parsed.eom); if (markerNorm && eomNorm === markerNorm) score += 200; for (const k of expectedKeys) if (Object.prototype.hasOwnProperty.call(parsed, k)) score += 10; if (parsed.summary === 'Short factual description (1-2 sentences)') score -= 50; if (parsed.store_description === 'Punchy, slightly opinionated record store blurb (max 80 words, human tone, not generic AI)') score -= 50; if (cand.length > 2000) score += 5; if (score > bestScore) { bestScore = score; best = parsed; } } return best; } function extractLastResponseText() { const selectors = [ 'main', '[role="main"]', '[class*="chat"]', ]; const parts = []; for (const sel of selectors) { const el = document.querySelector(sel); if (!el || !isVisible(el)) continue; const t = (el.innerText || '').trim(); if (t.length > 50) parts.push(t); } if (!parts.length) return (document.body?.innerText || '').trim() || null; parts.sort((a, b) => b.length - a.length); return parts[0]; } async function waitForJsonAndText(marker, timeoutMs) { const start = Date.now(); const want = normMarker(marker); while (Date.now() - start < timeoutMs) { const txt = extractLastResponseText() || ''; if (txt) { const parsed = chooseBestJsonFromText(txt, marker); if (parsed) { const got = normMarker(parsed?.eom); if (!want || got === want) return { text: txt, json: parsed }; return { text: txt, json: parsed }; } if (!isGenerating() && txt.trimStart().startsWith('{')) { const fallback = chooseBestJsonFromText(txt, marker); if (fallback) return { text: txt, json: fallback }; } } await sleep(500); } const finalTxt = extractLastResponseText(); return { text: finalTxt, json: chooseBestJsonFromText(finalTxt, marker) || null }; } async function sendToBackground(message) { return await new Promise(resolve => { try { chrome.runtime.sendMessage(message, (resp) => { const err = chrome.runtime?.lastError?.message; if (err) resolve({ ok: false, error: err }); else resolve(resp || { ok: false, error: 'No response' }); }); } catch (e) { resolve({ ok: false, error: e.message || 'sendMessage failed' }); } }); } async function run() { const job = await new Promise(resolve => chrome.storage.local.get(['geminiJob'], r => resolve(r.geminiJob || null))); if (!job || !job.prompt) return; const provider = job.provider || 'gemini'; if (provider !== 'deepseek') return; if (job.status !== 'pending' && job.status !== 'running') return; if (job.status === 'pending') { await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'running', started_at: new Date().toISOString() } }, resolve)); } const inputEl = await waitForInputEl(45000); if (!inputEl) { await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'error', error: 'DeepSeek input box not found.', failed_at: new Date().toISOString() } }, resolve)); return; } setInputText(inputEl, job.prompt); await sleep(600); const sendBtn = findSendButtonNear(inputEl); if (!sendBtn) { await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'error', error: 'DeepSeek send button not found.', failed_at: new Date().toISOString() } }, resolve)); return; } try { sendBtn.scrollIntoView({ block: 'center', inline: 'center' }); } catch {} clickLikeAUser(sendBtn); const marker = '__PLICE_EOM__'; const { text: responseText, json: outputJson } = await waitForJsonAndText(marker, 300000); if (!responseText) { await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'error', error: 'No DeepSeek response detected.', failed_at: new Date().toISOString() } }, resolve)); return; } const result = { job_id: job.job_id, model: job.model || 'deepseek-web', provider: 'deepseek', release_id: job.release_id || null, discogs_url: job.discogs_url || null, ai_url: location.href, created_at: job.created_at, prompt: job.prompt, input_json: job.input_json || null, output_text: responseText, output_json: outputJson, }; await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'posting', posting_at: new Date().toISOString() } }, resolve)); const postResp = await sendToBackground({ action: 'postGeminiResult', result }); if (!postResp || !postResp.ok) { const err = postResp?.error || 'Post failed.'; await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'error', error: err, failed_at: new Date().toISOString() }, geminiLastExchange: result }, resolve)); return; } await new Promise(resolve => chrome.storage.local.set({ geminiJob: { ...job, status: 'done', done_at: new Date().toISOString() }, geminiLastExchange: result }, resolve)); const closeOnSuccess = await new Promise(resolve => chrome.storage.local.get(['geminiCloseTabOnSuccess'], r => resolve(r.geminiCloseTabOnSuccess !== false)) ); if (closeOnSuccess) { await sendToBackground({ action: 'closeSenderTab' }); } } if (document.readyState === 'complete' || document.readyState === 'interactive') { run(); } else { window.addEventListener('DOMContentLoaded', () => run(), { once: true }); }