diff --git a/extension/content.js b/extension/content.js index ab2e2e3..85dc6fc 100644 --- a/extension/content.js +++ b/extension/content.js @@ -2101,10 +2101,13 @@ function collectRegexMatches(regex, text, limit = 20) { return Array.from(new Set(matches)).slice(0, limit); } -function filterScorelines(candidates = []) { +function filterScorelines(candidates = [], sourceText = '') { const filtered = []; + const lowerSource = typeof sourceText === 'string' ? sourceText.toLowerCase() : ''; for (const raw of candidates) { - const parts = raw.split(':').map((part) => part.trim()); + const value = typeof raw === 'string' ? raw : (raw && raw.value) || ''; + const index = typeof raw === 'string' ? -1 : (raw && typeof raw.index === 'number' ? raw.index : -1); + const parts = value.split(':').map((part) => part.trim()); if (parts.length !== 2) { continue; } @@ -2118,6 +2121,17 @@ function filterScorelines(candidates = []) { if (a > 15 || b > 15) { continue; } + if (index >= 0 && lowerSource) { + const contextStart = Math.max(0, index - 12); + const contextEnd = Math.min(lowerSource.length, index + value.length + 8); + const context = lowerSource.slice(contextStart, contextEnd); + const before = lowerSource.slice(Math.max(0, index - 6), index); + const hasTimeIndicatorBefore = /\bum\s*$/.test(before); + const hasTimeIndicatorAfter = /\buhr/.test(context); + if (hasTimeIndicatorBefore || hasTimeIndicatorAfter) { + continue; + } + } filtered.push(`${a}:${b}`); } return filtered; @@ -2184,8 +2198,12 @@ function evaluateSportsScore(text, moderationSettings = null) { const hitDetails = []; let score = 0; - const scorelineMatchesRaw = collectRegexMatches(/\b\d{1,2}\s*:\s*\d{1,2}\b/g, normalizedText); - const scorelineMatches = filterScorelines(scorelineMatchesRaw); + const scorelineMatchesRaw = Array.from(normalizedText.matchAll(/\b\d{1,2}\s*:\s*\d{1,2}\b/g)) + .map((match) => ({ + value: match[0], + index: typeof match.index === 'number' ? match.index : -1 + })); + const scorelineMatches = filterScorelines(scorelineMatchesRaw, normalizedText); applyWeight(scorelineMatches.length, weights.scoreline, 'Ergebnis', scorelineMatches); const scoreEmojiMatches = collectRegexMatches(/[0-9]️⃣/g, normalizedText) @@ -3409,6 +3427,283 @@ document.addEventListener('contextmenu', (event) => { console.log('[FB Tracker] Context menu opened on:', contextMenuTarget); }, true); +// Floating AI button on text selection +let selectionAIContainer = null; +let selectionAIButton = null; +let selectionAINoteButton = null; +let selectionAIRaf = null; +let selectionAIHideTimeout = null; +let selectionAIEnabledCached = null; + +const clearSelectionAIHideTimeout = () => { + if (selectionAIHideTimeout) { + clearTimeout(selectionAIHideTimeout); + selectionAIHideTimeout = null; + } +}; + +const hideSelectionAIButton = () => { + clearSelectionAIHideTimeout(); + if (selectionAIContainer) { + selectionAIContainer.style.display = 'none'; + } + if (selectionAIButton) { + selectionAIButton.dataset.selectionText = ''; + } + if (selectionAINoteButton) { + selectionAINoteButton.dataset.selectionText = ''; + } +}; + +const ensureSelectionAIButton = () => { + if (selectionAIContainer && selectionAIContainer.isConnected && selectionAIButton && selectionAINoteButton) { + return selectionAIContainer; + } + + const container = document.createElement('div'); + container.style.cssText = ` + position: fixed; + z-index: 2147483647; + display: none; + align-items: center; + gap: 8px; + pointer-events: auto; + `; + + const noteButton = document.createElement('button'); + noteButton.type = 'button'; + noteButton.textContent = '➕ Zusatzinfo'; + noteButton.title = 'Aktuelle Auswahl als Zusatzinfo speichern'; + noteButton.style.cssText = ` + padding: 7px 10px; + border-radius: 10px; + border: 1px solid #d1d5db; + background: #fff; + color: #111827; + font-weight: 700; + font-size: 12px; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); + cursor: pointer; + transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease; + display: inline-flex; + align-items: center; + gap: 6px; + `; + noteButton.addEventListener('mouseenter', () => { + noteButton.style.transform = 'translateY(-1px)'; + noteButton.style.boxShadow = '0 8px 18px rgba(0, 0, 0, 0.18)'; + }); + noteButton.addEventListener('mouseleave', () => { + noteButton.style.transform = 'translateY(0)'; + noteButton.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.12)'; + }); + noteButton.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + const selectedText = noteButton.dataset.selectionText || ''; + if (!selectedText.trim()) { + showToast('Keine Textauswahl gefunden', 'error'); + return; + } + const selection = window.getSelection(); + const anchorNode = selection ? (selection.anchorNode || selection.focusNode) : null; + if (anchorNode && isSelectionInsideEditable(anchorNode)) { + showToast('Textauswahl aus Eingabefeldern kann nicht genutzt werden', 'error'); + return; + } + const anchorElement = anchorNode && anchorNode.nodeType === Node.TEXT_NODE + ? anchorNode.parentElement + : anchorNode; + const postContext = anchorElement ? ensurePrimaryPostElement(anchorElement) : null; + if (!postContext) { + showToast('Keinen zugehörigen Beitrag gefunden', 'error'); + return; + } + const normalized = normalizeSelectionText(selectedText); + if (!normalized) { + showToast('Keine Textauswahl gefunden', 'error'); + return; + } + postAdditionalNotes.set(postContext, normalized); + showToast('Auswahl als Zusatzinfo gesetzt', 'success'); + }); + + const button = document.createElement('button'); + button.type = 'button'; + button.textContent = '✨ AI'; + button.title = 'Auswahl mit AI beantworten'; + button.style.cssText = ` + padding: 8px 12px; + padding: 8px 12px; + border-radius: 999px; + border: none; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + font-weight: 700; + font-size: 13px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.22); + cursor: pointer; + align-items: center; + gap: 6px; + transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease; + `; + + button.addEventListener('mouseenter', () => { + button.style.transform = 'translateY(-1px) scale(1.02)'; + button.style.boxShadow = '0 10px 22px rgba(0, 0, 0, 0.26)'; + }); + button.addEventListener('mouseleave', () => { + button.style.transform = 'translateY(0)'; + button.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.22)'; + }); + + button.addEventListener('click', async (event) => { + event.preventDefault(); + event.stopPropagation(); + const selectedText = button.dataset.selectionText || ''; + hideSelectionAIButton(); + if (!selectedText.trim()) { + return; + } + const originalLabel = button.textContent; + button.textContent = '⏳ AI läuft...'; + try { + await handleSelectionAIRequest(selectedText, () => {}); + } finally { + button.textContent = originalLabel; + } + }); + + container.appendChild(noteButton); + container.appendChild(button); + document.body.appendChild(container); + selectionAIContainer = container; + selectionAIButton = button; + selectionAINoteButton = noteButton; + selectionAIButton = button; + return container; +}; + +const isSelectionInsideEditable = (node) => { + if (!node) { + return false; + } + if (node.nodeType === Node.ELEMENT_NODE) { + const el = node; + if (el.closest('input, textarea, [contenteditable="true"]')) { + return true; + } + } + if (node.parentElement && node.parentElement.closest('input, textarea, [contenteditable="true"]')) { + return true; + } + return false; +}; + +const positionSelectionAIButton = (rect) => { + if (!selectionAIContainer || !rect) { + return; + } + + const viewportPadding = 8; + const containerWidth = selectionAIContainer.offsetWidth || 160; + let left = rect.right + 8; + let top = rect.top - (selectionAIContainer.offsetHeight || 40) - 8; + + if (left + containerWidth + viewportPadding > window.innerWidth) { + left = Math.max(viewportPadding, rect.right - containerWidth - 8); + } + + if (top < viewportPadding) { + top = rect.bottom + 8; + } + + selectionAIContainer.style.left = `${Math.max(viewportPadding, left)}px`; + selectionAIContainer.style.top = `${Math.max(viewportPadding, top)}px`; +}; + +const updateSelectionAIButton = async () => { + clearSelectionAIHideTimeout(); + + if (selectionAIEnabledCached === null) { + try { + selectionAIEnabledCached = await isAIEnabled(); + } catch (error) { + console.warn('[FB Tracker] AI enable check failed for selection button:', error); + selectionAIEnabledCached = false; + } + } + + if (!selectionAIEnabledCached) { + hideSelectionAIButton(); + return; + } + + const selection = window.getSelection(); + if (!selection || selection.isCollapsed) { + hideSelectionAIButton(); + return; + } + + const selectionText = (selection.toString() || '').trim(); + if (!selectionText || selectionText.length > MAX_SELECTION_LENGTH) { + hideSelectionAIButton(); + return; + } + + const anchorNode = selection.anchorNode || selection.focusNode; + if (isSelectionInsideEditable(anchorNode)) { + hideSelectionAIButton(); + return; + } + + if (!selection.rangeCount) { + hideSelectionAIButton(); + return; + } + + const range = selection.getRangeAt(0); + const rect = range.getBoundingClientRect(); + if (!rect || (rect.width === 0 && rect.height === 0)) { + hideSelectionAIButton(); + return; + } + + const container = ensureSelectionAIButton(); + if (!selectionAIButton || !selectionAINoteButton) { + hideSelectionAIButton(); + return; + } + selectionAIButton.dataset.selectionText = selectionText; + selectionAINoteButton.dataset.selectionText = selectionText; + container.style.display = 'inline-flex'; + positionSelectionAIButton(rect); + + selectionAIHideTimeout = setTimeout(() => { + hideSelectionAIButton(); + }, 8000); +}; + +const scheduleSelectionAIUpdate = () => { + if (selectionAIRaf) { + return; + } + selectionAIRaf = requestAnimationFrame(() => { + selectionAIRaf = null; + updateSelectionAIButton(); + }); +}; + +const initSelectionAIFloatingButton = () => { + document.addEventListener('selectionchange', scheduleSelectionAIUpdate, true); + document.addEventListener('mouseup', scheduleSelectionAIUpdate, true); + document.addEventListener('keyup', scheduleSelectionAIUpdate, true); + window.addEventListener('scroll', hideSelectionAIButton, true); + window.addEventListener('blur', hideSelectionAIButton, true); +}; + +initSelectionAIFloatingButton(); + // Listen for manual reparse command chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message && message.type === 'generateSelectionAI') { @@ -4793,6 +5088,40 @@ async function addAICommentButton(container, postElement) { const buttonsRow = document.createElement('div'); buttonsRow.style.cssText = 'display: flex; gap: 8px; flex-wrap: wrap;'; + const selectionButton = document.createElement('button'); + selectionButton.type = 'button'; + selectionButton.textContent = 'Auswahl als Zusatzinfo'; + selectionButton.style.cssText = 'padding: 6px 10px; border-radius: 6px; border: 1px solid #ccd0d5; background: #fff; cursor: pointer; font-size: 12px; font-weight: 600; color: #1d2129;'; + selectionButton.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + + const selection = window.getSelection(); + const anchorNode = selection ? (selection.anchorNode || selection.focusNode) : null; + if (anchorNode && isSelectionInsideEditable(anchorNode)) { + showToast('Textauswahl aus Eingabefeldern kann nicht genutzt werden', 'error'); + return; + } + + const context = resolvePostContext(); + let selectedText = context ? getSelectedTextFromPost(context) : ''; + if (!selectedText && selection) { + selectedText = normalizeSelectionText(selection.toString()); + } + if (!selectedText && lastGlobalSelection.text && Date.now() - lastGlobalSelection.timestamp < LAST_SELECTION_MAX_AGE) { + selectedText = normalizeSelectionText(lastGlobalSelection.text); + } + + if (!selectedText) { + showToast('Keine Textauswahl gefunden', 'error'); + return; + } + + setAdditionalNote(selectedText); + showToast('Auswahl als Zusatzinfo gesetzt', 'success'); + }); + buttonsRow.appendChild(selectionButton); + const editButton = document.createElement('button'); editButton.type = 'button'; editButton.textContent = 'Zusatzinfo bearbeiten';