sonstiges
This commit is contained in:
@@ -18,6 +18,7 @@ const sessionSearchRecordedUrls = new Set();
|
||||
const sessionSearchInfoCache = new Map();
|
||||
|
||||
const trackerElementsByPost = new WeakMap();
|
||||
const postAdditionalNotes = new WeakMap();
|
||||
|
||||
const REELS_PATH_PREFIX = '/reel/';
|
||||
|
||||
@@ -2614,6 +2615,11 @@ document.addEventListener('contextmenu', (event) => {
|
||||
|
||||
// Listen for manual reparse command
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message && message.type === 'generateSelectionAI') {
|
||||
handleSelectionAIRequest(message.selectionText || '', sendResponse);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message && message.type === 'reparsePost') {
|
||||
console.log('[FB Tracker] Manual reparse triggered');
|
||||
|
||||
@@ -2752,6 +2758,38 @@ function showToast(message, type = 'info') {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text) {
|
||||
if (typeof text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('[FB Tracker] navigator.clipboard.writeText failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.top = '-999px';
|
||||
textarea.style.left = '-999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
const success = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.warn('[FB Tracker] execCommand copy fallback failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract post text from a Facebook post element
|
||||
*/
|
||||
@@ -3328,6 +3366,38 @@ async function generateAIComment(postText, profileNumber, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSelectionAIRequest(selectionText, sendResponse) {
|
||||
try {
|
||||
const normalizedSelection = normalizeSelectionText(selectionText);
|
||||
if (!normalizedSelection) {
|
||||
showToast('Keine gültige Auswahl gefunden', 'error');
|
||||
sendResponse({ success: false, error: 'Keine gültige Auswahl gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
showToast('AI verarbeitet Auswahl...', 'info');
|
||||
|
||||
const profileNumber = await getProfileNumber();
|
||||
const comment = await generateAIComment(normalizedSelection, profileNumber, {});
|
||||
|
||||
if (!comment) {
|
||||
throw new Error('Keine Antwort vom AI-Dienst erhalten');
|
||||
}
|
||||
|
||||
const copied = await copyTextToClipboard(comment);
|
||||
if (!copied) {
|
||||
throw new Error('Antwort konnte nicht in die Zwischenablage kopiert werden');
|
||||
}
|
||||
|
||||
showToast('AI-Antwort in die Zwischenablage kopiert', 'success');
|
||||
sendResponse({ success: true, comment });
|
||||
} catch (error) {
|
||||
console.error('[FB Tracker] Selection AI error:', error);
|
||||
showToast(`❌ ${error.message || 'Fehler bei AI-Anfrage'}`, 'error');
|
||||
sendResponse({ success: false, error: error.message || 'Unbekannter Fehler' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if AI is enabled
|
||||
*/
|
||||
@@ -3419,9 +3489,6 @@ async function addAICommentButton(container, postElement) {
|
||||
dropdown.className = 'fb-tracker-ai-dropdown';
|
||||
dropdown.style.cssText = `
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 6px);
|
||||
min-width: 220px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
@@ -3435,6 +3502,75 @@ async function addAICommentButton(container, postElement) {
|
||||
wrapper.appendChild(dropdown);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
const baseButtonText = button.textContent;
|
||||
|
||||
const resolvePostContexts = () => {
|
||||
const contextCandidate = container.closest('div[aria-posinset], article[role="article"], article, div[role="complementary"]');
|
||||
const normalizedContext = contextCandidate ? ensurePrimaryPostElement(contextCandidate) : null;
|
||||
const fallbackContext = postElement ? ensurePrimaryPostElement(postElement) : null;
|
||||
const postContext = normalizedContext || fallbackContext || contextCandidate || postElement || container;
|
||||
return { postContext, contextCandidate, fallbackContext, normalizedContext };
|
||||
};
|
||||
|
||||
const resolvePostContext = () => resolvePostContexts().postContext;
|
||||
|
||||
const getAdditionalNote = () => {
|
||||
const context = resolvePostContext();
|
||||
return context ? (postAdditionalNotes.get(context) || '') : '';
|
||||
};
|
||||
|
||||
let notePreviewElement = null;
|
||||
let noteClearButton = null;
|
||||
|
||||
const truncateNoteForPreview = (note) => {
|
||||
if (!note) {
|
||||
return '';
|
||||
}
|
||||
return note.length > 120 ? `${note.slice(0, 117)}…` : note;
|
||||
};
|
||||
|
||||
const updateNoteIndicator = () => {
|
||||
const note = getAdditionalNote();
|
||||
const hasNote = note.trim().length > 0;
|
||||
button.dataset.aiOriginalText = hasNote ? `${baseButtonText} ✎` : baseButtonText;
|
||||
if ((button.dataset.aiState || 'idle') === 'idle' && !button._aiContext) {
|
||||
button.textContent = button.dataset.aiOriginalText;
|
||||
}
|
||||
button.title = hasNote
|
||||
? 'Generiere automatisch einen passenden Kommentar (mit Zusatzinfo)'
|
||||
: 'Generiere automatisch einen passenden Kommentar';
|
||||
};
|
||||
|
||||
const updateNotePreview = () => {
|
||||
updateNoteIndicator();
|
||||
if (notePreviewElement) {
|
||||
const note = getAdditionalNote();
|
||||
notePreviewElement.textContent = note
|
||||
? `Aktuelle Zusatzinfo: ${truncateNoteForPreview(note)}`
|
||||
: 'Keine Zusatzinfo gesetzt';
|
||||
if (noteClearButton) {
|
||||
const hasNote = note.trim().length > 0;
|
||||
noteClearButton.disabled = !hasNote;
|
||||
noteClearButton.style.opacity = hasNote ? '1' : '0.6';
|
||||
noteClearButton.style.cursor = hasNote ? 'pointer' : 'default';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setAdditionalNote = (value) => {
|
||||
const context = resolvePostContext();
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
const trimmed = (value || '').trim();
|
||||
if (trimmed) {
|
||||
postAdditionalNotes.set(context, trimmed);
|
||||
} else {
|
||||
postAdditionalNotes.delete(context);
|
||||
}
|
||||
updateNotePreview();
|
||||
};
|
||||
|
||||
button.addEventListener('mouseenter', () => {
|
||||
if ((button.dataset.aiState || 'idle') === 'idle') {
|
||||
button.style.transform = 'translateY(-2px)';
|
||||
@@ -3470,10 +3606,8 @@ async function addAICommentButton(container, postElement) {
|
||||
});
|
||||
|
||||
button.addEventListener('pointerdown', () => {
|
||||
const contextElement = container.closest('div[aria-posinset], article[role="article"], article, div[role="complementary"]');
|
||||
const normalized = contextElement ? ensurePrimaryPostElement(contextElement) : null;
|
||||
const fallbackNormalized = postElement ? ensurePrimaryPostElement(postElement) : null;
|
||||
const target = normalized || fallbackNormalized || contextElement || postElement || container;
|
||||
const context = resolvePostContext();
|
||||
const target = context || postElement || container;
|
||||
cacheSelectionForPost(target);
|
||||
});
|
||||
|
||||
@@ -3481,6 +3615,32 @@ async function addAICommentButton(container, postElement) {
|
||||
button.dataset.aiOriginalText = button.textContent;
|
||||
|
||||
let dropdownOpen = false;
|
||||
let dropdownPortalParent = null;
|
||||
|
||||
const resolveDropdownPortalParent = () => {
|
||||
if (dropdownPortalParent && dropdownPortalParent.isConnected) {
|
||||
return dropdownPortalParent;
|
||||
}
|
||||
const candidate = document.body || document.documentElement;
|
||||
dropdownPortalParent = candidate;
|
||||
return dropdownPortalParent;
|
||||
};
|
||||
|
||||
const mountDropdownInPortal = () => {
|
||||
const portalParent = resolveDropdownPortalParent();
|
||||
if (!portalParent) {
|
||||
return;
|
||||
}
|
||||
if (dropdown.parentElement !== portalParent) {
|
||||
portalParent.appendChild(dropdown);
|
||||
}
|
||||
};
|
||||
|
||||
const restoreDropdownToWrapper = () => {
|
||||
if (dropdown.parentElement !== wrapper) {
|
||||
wrapper.appendChild(dropdown);
|
||||
}
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
if (!dropdownOpen) {
|
||||
@@ -3497,10 +3657,18 @@ async function addAICommentButton(container, postElement) {
|
||||
dropdownButton.style.transform = 'translateY(0)';
|
||||
button.style.boxShadow = 'none';
|
||||
dropdownButton.style.boxShadow = 'none';
|
||||
dropdown.style.position = '';
|
||||
dropdown.style.top = '';
|
||||
dropdown.style.left = '';
|
||||
dropdown.style.maxHeight = '';
|
||||
dropdown.style.overflowY = '';
|
||||
restoreDropdownToWrapper();
|
||||
window.removeEventListener('scroll', repositionDropdown, true);
|
||||
window.removeEventListener('resize', repositionDropdown);
|
||||
};
|
||||
|
||||
const handleOutsideClick = (event) => {
|
||||
if (!wrapper.contains(event.target)) {
|
||||
if (!wrapper.contains(event.target) && !dropdown.contains(event.target)) {
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
@@ -3518,82 +3686,187 @@ async function addAICommentButton(container, postElement) {
|
||||
loading.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #555;';
|
||||
dropdown.appendChild(loading);
|
||||
|
||||
const appendNoteUI = () => {
|
||||
noteClearButton = null;
|
||||
const noteSection = document.createElement('div');
|
||||
noteSection.style.cssText = 'padding: 10px 14px; display: flex; flex-direction: column; gap: 6px;';
|
||||
|
||||
notePreviewElement = document.createElement('div');
|
||||
notePreviewElement.style.cssText = 'font-size: 12px; color: #65676b; white-space: normal;';
|
||||
noteSection.appendChild(notePreviewElement);
|
||||
|
||||
const buttonsRow = document.createElement('div');
|
||||
buttonsRow.style.cssText = 'display: flex; gap: 8px; flex-wrap: wrap;';
|
||||
|
||||
const editButton = document.createElement('button');
|
||||
editButton.type = 'button';
|
||||
editButton.textContent = 'Zusatzinfo bearbeiten';
|
||||
editButton.style.cssText = 'padding: 6px 10px; border-radius: 6px; border: 1px solid #ccd0d5; background: #fff; cursor: pointer; font-size: 12px; font-weight: 600; color: #1d2129;';
|
||||
editButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const existingNote = getAdditionalNote();
|
||||
const input = window.prompt('Zusatzinfo für den AI-Prompt eingeben:', existingNote);
|
||||
if (input === null) {
|
||||
return;
|
||||
}
|
||||
const trimmed = (input || '').trim();
|
||||
setAdditionalNote(trimmed);
|
||||
if (trimmed) {
|
||||
showToast('Zusatzinfo gespeichert', 'success');
|
||||
} else {
|
||||
showToast('Zusatzinfo entfernt', 'success');
|
||||
}
|
||||
});
|
||||
buttonsRow.appendChild(editButton);
|
||||
|
||||
const clearButton = document.createElement('button');
|
||||
clearButton.type = 'button';
|
||||
clearButton.textContent = 'Zurücksetzen';
|
||||
clearButton.style.cssText = 'padding: 6px 10px; border-radius: 6px; border: 1px solid #ccd0d5; background: #fff; cursor: pointer; font-size: 12px; font-weight: 600; color: #a11900;';
|
||||
clearButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!getAdditionalNote()) {
|
||||
return;
|
||||
}
|
||||
setAdditionalNote('');
|
||||
showToast('Zusatzinfo entfernt', 'success');
|
||||
});
|
||||
buttonsRow.appendChild(clearButton);
|
||||
noteClearButton = clearButton;
|
||||
|
||||
noteSection.appendChild(buttonsRow);
|
||||
dropdown.appendChild(noteSection);
|
||||
|
||||
updateNotePreview();
|
||||
};
|
||||
|
||||
try {
|
||||
const credentials = await fetchActiveAICredentials();
|
||||
dropdown.innerHTML = '';
|
||||
appendNoteUI();
|
||||
|
||||
if (!credentials || credentials.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.textContent = 'Keine aktiven AI-Anbieter gefunden';
|
||||
empty.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #888;';
|
||||
dropdown.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const divider = document.createElement('div');
|
||||
divider.style.cssText = 'border-top: 1px solid #e4e6eb; margin: 6px 0;';
|
||||
dropdown.appendChild(divider);
|
||||
|
||||
credentials.forEach((credential) => {
|
||||
const option = document.createElement('button');
|
||||
option.type = 'button';
|
||||
option.className = 'fb-tracker-ai-option';
|
||||
option.style.cssText = `
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
`;
|
||||
credentials.forEach((credential) => {
|
||||
const option = document.createElement('button');
|
||||
option.type = 'button';
|
||||
option.className = 'fb-tracker-ai-option';
|
||||
option.style.cssText = `
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
option.addEventListener('mouseenter', () => {
|
||||
option.style.background = '#f0f2f5';
|
||||
});
|
||||
option.addEventListener('mouseenter', () => {
|
||||
option.style.background = '#f0f2f5';
|
||||
});
|
||||
|
||||
option.addEventListener('mouseleave', () => {
|
||||
option.style.background = 'transparent';
|
||||
});
|
||||
option.addEventListener('mouseleave', () => {
|
||||
option.style.background = 'transparent';
|
||||
});
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = formatAICredentialLabel(credential);
|
||||
label.style.cssText = 'font-weight: 600; color: #1d2129;';
|
||||
const label = document.createElement('span');
|
||||
label.textContent = formatAICredentialLabel(credential);
|
||||
label.style.cssText = 'font-weight: 600; color: #1d2129;';
|
||||
|
||||
const metaParts = [];
|
||||
if (credential.provider) {
|
||||
metaParts.push(`Provider: ${credential.provider}`);
|
||||
}
|
||||
if (credential.model) {
|
||||
metaParts.push(`Modell: ${credential.model}`);
|
||||
}
|
||||
|
||||
if (metaParts.length > 0) {
|
||||
const meta = document.createElement('span');
|
||||
meta.textContent = metaParts.join(' · ');
|
||||
meta.style.cssText = 'font-size: 12px; color: #65676b;';
|
||||
option.appendChild(label);
|
||||
option.appendChild(meta);
|
||||
} else {
|
||||
option.appendChild(label);
|
||||
}
|
||||
|
||||
option.addEventListener('click', () => {
|
||||
closeDropdown();
|
||||
if ((button.dataset.aiState || 'idle') === 'idle') {
|
||||
cacheSelectionForPost(postElement);
|
||||
startAIFlow(credential.id);
|
||||
const metaParts = [];
|
||||
if (credential.provider) {
|
||||
metaParts.push(`Provider: ${credential.provider}`);
|
||||
}
|
||||
if (credential.model) {
|
||||
metaParts.push(`Modell: ${credential.model}`);
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
if (metaParts.length > 0) {
|
||||
const meta = document.createElement('span');
|
||||
meta.textContent = metaParts.join(' · ');
|
||||
meta.style.cssText = 'font-size: 12px; color: #65676b;';
|
||||
option.appendChild(label);
|
||||
option.appendChild(meta);
|
||||
} else {
|
||||
option.appendChild(label);
|
||||
}
|
||||
|
||||
option.addEventListener('click', () => {
|
||||
closeDropdown();
|
||||
if ((button.dataset.aiState || 'idle') === 'idle') {
|
||||
cacheSelectionForPost(postElement);
|
||||
startAIFlow(credential.id);
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
dropdown.innerHTML = '';
|
||||
appendNoteUI();
|
||||
const errorItem = document.createElement('div');
|
||||
errorItem.textContent = error.message || 'AI-Anbieter konnten nicht geladen werden';
|
||||
errorItem.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #c0392b; white-space: normal;';
|
||||
dropdown.appendChild(errorItem);
|
||||
}
|
||||
};
|
||||
|
||||
const positionDropdown = () => {
|
||||
if (!dropdownOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
mountDropdownInPortal();
|
||||
dropdown.style.position = 'fixed';
|
||||
dropdown.style.maxHeight = `${Math.max(200, Math.floor(window.innerHeight * 0.6))}px`;
|
||||
dropdown.style.overflowY = 'auto';
|
||||
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const dropdownRect = dropdown.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
|
||||
let top = rect.top - dropdownRect.height - margin;
|
||||
if (top < margin) {
|
||||
top = rect.bottom + margin;
|
||||
}
|
||||
|
||||
const viewportPadding = 8;
|
||||
let left = rect.right - dropdownRect.width;
|
||||
if (left < viewportPadding) {
|
||||
left = viewportPadding;
|
||||
}
|
||||
const maxLeft = window.innerWidth - dropdownRect.width - viewportPadding;
|
||||
if (left > maxLeft) {
|
||||
left = Math.max(viewportPadding, maxLeft);
|
||||
}
|
||||
|
||||
const maxTop = window.innerHeight - dropdownRect.height - margin;
|
||||
if (top > maxTop) {
|
||||
top = Math.max(viewportPadding, maxTop);
|
||||
}
|
||||
|
||||
dropdown.style.top = `${top}px`;
|
||||
dropdown.style.left = `${left}px`;
|
||||
};
|
||||
|
||||
const repositionDropdown = () => {
|
||||
if (dropdownOpen) {
|
||||
positionDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDropdown = async () => {
|
||||
@@ -3609,12 +3882,16 @@ async function addAICommentButton(container, postElement) {
|
||||
dropdownOpen = true;
|
||||
wrapper.classList.add('fb-tracker-ai-wrapper--open');
|
||||
dropdownButton.textContent = '▴';
|
||||
mountDropdownInPortal();
|
||||
dropdown.style.display = 'block';
|
||||
dropdownButton.setAttribute('aria-expanded', 'true');
|
||||
document.addEventListener('click', handleOutsideClick, true);
|
||||
document.addEventListener('keydown', handleKeydown, true);
|
||||
|
||||
await renderDropdownItems();
|
||||
positionDropdown();
|
||||
window.addEventListener('scroll', repositionDropdown, true);
|
||||
window.addEventListener('resize', repositionDropdown);
|
||||
};
|
||||
|
||||
dropdownButton.addEventListener('click', (event) => {
|
||||
@@ -3708,10 +3985,8 @@ async function addAICommentButton(container, postElement) {
|
||||
button.textContent = '⏳ Generiere...';
|
||||
|
||||
try {
|
||||
const contextCandidate = container.closest('div[aria-posinset], article[role="article"], article, div[role="complementary"]');
|
||||
const normalizedContext = contextCandidate ? ensurePrimaryPostElement(contextCandidate) : null;
|
||||
const fallbackContext = postElement ? ensurePrimaryPostElement(postElement) : null;
|
||||
const postContext = normalizedContext || fallbackContext || contextCandidate || postElement || container;
|
||||
const contexts = resolvePostContexts();
|
||||
const { postContext, contextCandidate, fallbackContext } = contexts;
|
||||
|
||||
const selectionKeys = [];
|
||||
if (postContext) {
|
||||
@@ -3723,6 +3998,12 @@ async function addAICommentButton(container, postElement) {
|
||||
if (contextCandidate && contextCandidate !== postContext && contextCandidate !== postElement) {
|
||||
selectionKeys.push(contextCandidate);
|
||||
}
|
||||
if (fallbackContext
|
||||
&& fallbackContext !== postContext
|
||||
&& fallbackContext !== postElement
|
||||
&& fallbackContext !== contextCandidate) {
|
||||
selectionKeys.push(fallbackContext);
|
||||
}
|
||||
|
||||
const resolveRecentSelection = () => {
|
||||
for (const key of selectionKeys) {
|
||||
@@ -3775,15 +4056,20 @@ async function addAICommentButton(container, postElement) {
|
||||
throw new Error('Konnte Post-Text nicht extrahieren');
|
||||
}
|
||||
|
||||
selectionKeys.forEach((key) => {
|
||||
if (key) {
|
||||
postSelectionCache.delete(key);
|
||||
}
|
||||
});
|
||||
selectionKeys.forEach((key) => {
|
||||
if (key) {
|
||||
postSelectionCache.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
throwIfCancelled();
|
||||
const additionalNote = postContext ? (postAdditionalNotes.get(postContext) || '') : '';
|
||||
if (additionalNote) {
|
||||
postText = `${postText}\n\nZusatzinfo:\n${additionalNote}`;
|
||||
}
|
||||
|
||||
console.log('[FB Tracker] Generating AI comment for:', postText.substring(0, 100));
|
||||
throwIfCancelled();
|
||||
|
||||
console.log('[FB Tracker] Generating AI comment for:', postText.substring(0, 100));
|
||||
|
||||
const profileNumber = await getProfileNumber();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user