api usage cooldown
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,54 @@ const FEED_HOME_PATHS = ['/', '/home.php'];
|
|||||||
const sessionSearchRecordedUrls = new Set();
|
const sessionSearchRecordedUrls = new Set();
|
||||||
const sessionSearchInfoCache = new Map();
|
const sessionSearchInfoCache = new Map();
|
||||||
|
|
||||||
|
const trackerElementsByPost = new WeakMap();
|
||||||
|
|
||||||
|
function getTrackerElementForPost(postElement) {
|
||||||
|
if (!postElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracker = trackerElementsByPost.get(postElement);
|
||||||
|
if (tracker && tracker.isConnected) {
|
||||||
|
return tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracker) {
|
||||||
|
trackerElementsByPost.delete(postElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTrackerElementForPost(postElement, trackerElement) {
|
||||||
|
if (!postElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackerElement && trackerElement.isConnected) {
|
||||||
|
trackerElementsByPost.set(postElement, trackerElement);
|
||||||
|
} else {
|
||||||
|
trackerElementsByPost.delete(postElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTrackerElementForPost(postElement, trackerElement = null) {
|
||||||
|
if (!postElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trackerElementsByPost.has(postElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = trackerElementsByPost.get(postElement);
|
||||||
|
if (trackerElement && current && current !== trackerElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackerElementsByPost.delete(postElement);
|
||||||
|
}
|
||||||
|
|
||||||
const AI_CREDENTIAL_CACHE_TTL = 60 * 1000; // 1 minute cache
|
const AI_CREDENTIAL_CACHE_TTL = 60 * 1000; // 1 minute cache
|
||||||
const aiCredentialCache = {
|
const aiCredentialCache = {
|
||||||
data: null,
|
data: null,
|
||||||
@@ -1600,9 +1648,22 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
|
|||||||
// Normalize to top-level post container if nested element passed
|
// Normalize to top-level post container if nested element passed
|
||||||
postElement = ensurePrimaryPostElement(postElement);
|
postElement = ensurePrimaryPostElement(postElement);
|
||||||
|
|
||||||
const existingUI = postElement.querySelector('.fb-tracker-ui');
|
let existingUI = getTrackerElementForPost(postElement);
|
||||||
|
if (!existingUI) {
|
||||||
|
existingUI = postElement.querySelector('.fb-tracker-ui');
|
||||||
|
if (existingUI && existingUI.isConnected) {
|
||||||
|
setTrackerElementForPost(postElement, existingUI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingUI && !existingUI.isConnected) {
|
||||||
|
clearTrackerElementForPost(postElement, existingUI);
|
||||||
|
existingUI = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (existingUI) {
|
if (existingUI) {
|
||||||
console.log('[FB Tracker] Post #' + postNum + ' - UI already exists on normalized element');
|
console.log('[FB Tracker] Post #' + postNum + ' - UI already exists on normalized element');
|
||||||
|
postElement.setAttribute(PROCESSED_ATTR, '1');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1617,6 +1678,7 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
|
|||||||
if (!postUrlData.url) {
|
if (!postUrlData.url) {
|
||||||
console.log('[FB Tracker] Post #' + postNum + ' - No URL found:', postElement);
|
console.log('[FB Tracker] Post #' + postNum + ' - No URL found:', postElement);
|
||||||
postElement.removeAttribute(PROCESSED_ATTR);
|
postElement.removeAttribute(PROCESSED_ATTR);
|
||||||
|
clearTrackerElementForPost(postElement);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1628,6 +1690,7 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
|
|||||||
if (existingEntry && existingEntry.element && existingEntry.element !== postElement) {
|
if (existingEntry && existingEntry.element && existingEntry.element !== postElement) {
|
||||||
if (document.body.contains(existingEntry.element)) {
|
if (document.body.contains(existingEntry.element)) {
|
||||||
existingEntry.element.removeAttribute(PROCESSED_ATTR);
|
existingEntry.element.removeAttribute(PROCESSED_ATTR);
|
||||||
|
clearTrackerElementForPost(existingEntry.element);
|
||||||
} else {
|
} else {
|
||||||
processedPostUrls.delete(encodedUrl);
|
processedPostUrls.delete(encodedUrl);
|
||||||
}
|
}
|
||||||
@@ -2149,6 +2212,7 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
|
|||||||
: null,
|
: null,
|
||||||
hidden: false
|
hidden: false
|
||||||
});
|
});
|
||||||
|
setTrackerElementForPost(postElement, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monitor if the UI gets removed and re-insert it
|
// Monitor if the UI gets removed and re-insert it
|
||||||
@@ -2163,6 +2227,9 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
|
|||||||
} else if (postElement.parentElement) {
|
} else if (postElement.parentElement) {
|
||||||
postElement.parentElement.appendChild(container);
|
postElement.parentElement.appendChild(container);
|
||||||
}
|
}
|
||||||
|
if (container.isConnected) {
|
||||||
|
setTrackerElementForPost(postElement, container);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2271,10 +2338,29 @@ function findPosts() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingTracker = container.querySelector('.fb-tracker-ui');
|
let existingTracker = getTrackerElementForPost(container);
|
||||||
|
if (!existingTracker) {
|
||||||
|
existingTracker = container.querySelector('.fb-tracker-ui');
|
||||||
|
if (existingTracker && existingTracker.isConnected) {
|
||||||
|
setTrackerElementForPost(container, existingTracker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingTracker && !existingTracker.isConnected) {
|
||||||
|
clearTrackerElementForPost(container, existingTracker);
|
||||||
|
existingTracker = null;
|
||||||
|
}
|
||||||
|
|
||||||
const alreadyProcessed = container.getAttribute(PROCESSED_ATTR) === '1';
|
const alreadyProcessed = container.getAttribute(PROCESSED_ATTR) === '1';
|
||||||
const trackerDialogRoot = existingTracker ? existingTracker.closest(DIALOG_ROOT_SELECTOR) : null;
|
const trackerDialogRoot = existingTracker ? existingTracker.closest(DIALOG_ROOT_SELECTOR) : null;
|
||||||
const trackerInSameDialog = Boolean(existingTracker && existingTracker.isConnected && trackerDialogRoot === dialogRoot);
|
const trackerInSameDialog = Boolean(
|
||||||
|
existingTracker
|
||||||
|
&& existingTracker.isConnected
|
||||||
|
&& (
|
||||||
|
trackerDialogRoot === dialogRoot
|
||||||
|
|| (dialogRoot && dialogRoot.contains(existingTracker))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (isInDialog) {
|
if (isInDialog) {
|
||||||
if (trackerInSameDialog) {
|
if (trackerInSameDialog) {
|
||||||
@@ -2284,6 +2370,7 @@ function findPosts() {
|
|||||||
|
|
||||||
if (existingTracker && existingTracker.isConnected) {
|
if (existingTracker && existingTracker.isConnected) {
|
||||||
existingTracker.remove();
|
existingTracker.remove();
|
||||||
|
clearTrackerElementForPost(container, existingTracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alreadyProcessed) {
|
if (alreadyProcessed) {
|
||||||
@@ -2477,6 +2564,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
const existingUI = postContainer.querySelector('.fb-tracker-ui');
|
const existingUI = postContainer.querySelector('.fb-tracker-ui');
|
||||||
if (existingUI) {
|
if (existingUI) {
|
||||||
existingUI.remove();
|
existingUI.remove();
|
||||||
|
clearTrackerElementForPost(postContainer, existingUI);
|
||||||
console.log('[FB Tracker] Removed existing UI');
|
console.log('[FB Tracker] Removed existing UI');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2805,12 +2893,16 @@ function findAndClickCommentButton(postElement) {
|
|||||||
/**
|
/**
|
||||||
* Find comment input field on current page
|
* Find comment input field on current page
|
||||||
*/
|
*/
|
||||||
function findCommentInput(postElement) {
|
function findCommentInput(postElement, options = {}) {
|
||||||
if (!postElement) {
|
const {
|
||||||
|
preferredRoot = null,
|
||||||
|
includeParents = true
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (!postElement && !preferredRoot) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try multiple selectors for comment input
|
|
||||||
const selectors = [
|
const selectors = [
|
||||||
'div[contenteditable="true"][role="textbox"]',
|
'div[contenteditable="true"][role="textbox"]',
|
||||||
'div[aria-label*="Kommentar"][contenteditable="true"]',
|
'div[aria-label*="Kommentar"][contenteditable="true"]',
|
||||||
@@ -2818,24 +2910,48 @@ function findCommentInput(postElement) {
|
|||||||
'div[aria-label*="Write a comment"][contenteditable="true"]'
|
'div[aria-label*="Write a comment"][contenteditable="true"]'
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const selector of selectors) {
|
const searchInRoot = (root) => {
|
||||||
const input = postElement.querySelector(selector);
|
if (!root) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (const selector of selectors) {
|
||||||
|
const input = root.querySelector(selector);
|
||||||
|
if (input && isElementVisible(input)) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const roots = [];
|
||||||
|
|
||||||
|
if (postElement) {
|
||||||
|
roots.push(postElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferredRoot && preferredRoot.isConnected && !roots.includes(preferredRoot)) {
|
||||||
|
roots.push(preferredRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const root of roots) {
|
||||||
|
const input = searchInRoot(root);
|
||||||
if (input) {
|
if (input) {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search in parent containers
|
if (includeParents && postElement) {
|
||||||
let parent = postElement;
|
let parent = postElement.parentElement;
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3 && parent; i++) {
|
||||||
parent = parent.parentElement;
|
if (preferredRoot && !preferredRoot.contains(parent)) {
|
||||||
if (!parent) break;
|
parent = parent.parentElement;
|
||||||
|
continue;
|
||||||
for (const selector of selectors) {
|
}
|
||||||
const input = parent.querySelector(selector);
|
const input = searchInRoot(parent);
|
||||||
if (input) {
|
if (input) {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
parent = parent.parentElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2879,11 +2995,15 @@ async function waitForCommentInput(postElement, options = {}) {
|
|||||||
encodedPostUrl = null,
|
encodedPostUrl = null,
|
||||||
timeout = 6000,
|
timeout = 6000,
|
||||||
interval = 200,
|
interval = 200,
|
||||||
context = null
|
context = null,
|
||||||
|
preferredRoot: rawPreferredRoot = null
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const deadline = Date.now() + Math.max(timeout, 0);
|
const deadline = Date.now() + Math.max(timeout, 0);
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
|
const preferredRoot = rawPreferredRoot && rawPreferredRoot.isConnected
|
||||||
|
? rawPreferredRoot
|
||||||
|
: null;
|
||||||
|
|
||||||
const findByEncodedUrl = () => {
|
const findByEncodedUrl = () => {
|
||||||
if (context && context.cancelled) {
|
if (context && context.cancelled) {
|
||||||
@@ -2900,15 +3020,19 @@ async function waitForCommentInput(postElement, options = {}) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preferredRoot && !preferredRoot.contains(tracker)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const trackerContainer = tracker.closest('div[aria-posinset], article[role="article"], article');
|
const trackerContainer = tracker.closest('div[aria-posinset], article[role="article"], article');
|
||||||
if (trackerContainer) {
|
if (trackerContainer) {
|
||||||
const input = findCommentInput(trackerContainer);
|
const input = findCommentInput(trackerContainer, { preferredRoot });
|
||||||
if (isElementVisible(input)) {
|
if (isElementVisible(input)) {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialogRoot = tracker.closest(DIALOG_ROOT_SELECTOR);
|
const dialogRoot = preferredRoot || tracker.closest(DIALOG_ROOT_SELECTOR);
|
||||||
if (dialogRoot) {
|
if (dialogRoot) {
|
||||||
const dialogInput = dialogRoot.querySelector('div[contenteditable="true"][role="textbox"]');
|
const dialogInput = dialogRoot.querySelector('div[contenteditable="true"][role="textbox"]');
|
||||||
if (isElementVisible(dialogInput)) {
|
if (isElementVisible(dialogInput)) {
|
||||||
@@ -2927,7 +3051,7 @@ async function waitForCommentInput(postElement, options = {}) {
|
|||||||
|
|
||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
let input = findCommentInput(postElement);
|
let input = findCommentInput(postElement, { preferredRoot });
|
||||||
if (isElementVisible(input)) {
|
if (isElementVisible(input)) {
|
||||||
if (attempts > 1) {
|
if (attempts > 1) {
|
||||||
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (post context)');
|
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (post context)');
|
||||||
@@ -2943,7 +3067,8 @@ async function waitForCommentInput(postElement, options = {}) {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialogRootFromPost = postElement ? postElement.closest(DIALOG_ROOT_SELECTOR) : null;
|
const dialogRootFromPost = preferredRoot
|
||||||
|
|| (postElement ? postElement.closest(DIALOG_ROOT_SELECTOR) : null);
|
||||||
if (dialogRootFromPost) {
|
if (dialogRootFromPost) {
|
||||||
const dialogInput = dialogRootFromPost.querySelector('div[contenteditable="true"][role="textbox"]');
|
const dialogInput = dialogRootFromPost.querySelector('div[contenteditable="true"][role="textbox"]');
|
||||||
if (isElementVisible(dialogInput)) {
|
if (isElementVisible(dialogInput)) {
|
||||||
@@ -2954,18 +3079,32 @@ async function waitForCommentInput(postElement, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackDialog = document.querySelector(DIALOG_ROOT_SELECTOR);
|
if (!preferredRoot) {
|
||||||
if (fallbackDialog && fallbackDialog !== dialogRootFromPost) {
|
const fallbackDialog = document.querySelector(DIALOG_ROOT_SELECTOR);
|
||||||
const dialogInput = fallbackDialog.querySelector('div[contenteditable="true"][role="textbox"]');
|
if (fallbackDialog && fallbackDialog !== dialogRootFromPost) {
|
||||||
if (isElementVisible(dialogInput)) {
|
const dialogInput = fallbackDialog.querySelector('div[contenteditable="true"][role="textbox"]');
|
||||||
if (attempts > 1) {
|
if (isElementVisible(dialogInput)) {
|
||||||
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (fallback dialog)');
|
if (attempts > 1) {
|
||||||
|
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (fallback dialog)');
|
||||||
|
}
|
||||||
|
return dialogInput;
|
||||||
}
|
}
|
||||||
return dialogInput;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalInputs = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"]')).filter(isElementVisible);
|
const globalInputs = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"]')).filter(isElementVisible);
|
||||||
|
if (preferredRoot) {
|
||||||
|
const scopedInputs = globalInputs.filter(input => preferredRoot.contains(input));
|
||||||
|
if (scopedInputs.length > 0) {
|
||||||
|
const lastInput = scopedInputs[scopedInputs.length - 1];
|
||||||
|
if (lastInput) {
|
||||||
|
if (attempts > 1) {
|
||||||
|
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (preferred root fallback)');
|
||||||
|
}
|
||||||
|
return lastInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (globalInputs.length > 0) {
|
if (globalInputs.length > 0) {
|
||||||
const lastInput = globalInputs[globalInputs.length - 1];
|
const lastInput = globalInputs[globalInputs.length - 1];
|
||||||
if (lastInput) {
|
if (lastInput) {
|
||||||
@@ -3235,7 +3374,11 @@ async function addAICommentButton(container, postElement) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
button.addEventListener('pointerdown', () => {
|
button.addEventListener('pointerdown', () => {
|
||||||
cacheSelectionForPost(postElement);
|
const contextElement = container.closest('div[aria-posinset], article[role="article"], article');
|
||||||
|
const normalized = contextElement ? ensurePrimaryPostElement(contextElement) : null;
|
||||||
|
const fallbackNormalized = postElement ? ensurePrimaryPostElement(postElement) : null;
|
||||||
|
const target = normalized || fallbackNormalized || contextElement || postElement || container;
|
||||||
|
cacheSelectionForPost(target);
|
||||||
});
|
});
|
||||||
|
|
||||||
button.dataset.aiState = 'idle';
|
button.dataset.aiState = 'idle';
|
||||||
@@ -3469,9 +3612,38 @@ async function addAICommentButton(container, postElement) {
|
|||||||
button.textContent = '⏳ Generiere...';
|
button.textContent = '⏳ Generiere...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const contextCandidate = container.closest('div[aria-posinset], article[role="article"], article');
|
||||||
|
const normalizedContext = contextCandidate ? ensurePrimaryPostElement(contextCandidate) : null;
|
||||||
|
const fallbackContext = postElement ? ensurePrimaryPostElement(postElement) : null;
|
||||||
|
const postContext = normalizedContext || fallbackContext || contextCandidate || postElement || container;
|
||||||
|
|
||||||
|
const selectionKeys = [];
|
||||||
|
if (postContext) {
|
||||||
|
selectionKeys.push(postContext);
|
||||||
|
}
|
||||||
|
if (postElement && postElement !== postContext) {
|
||||||
|
selectionKeys.push(postElement);
|
||||||
|
}
|
||||||
|
if (contextCandidate && contextCandidate !== postContext && contextCandidate !== postElement) {
|
||||||
|
selectionKeys.push(contextCandidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveRecentSelection = () => {
|
||||||
|
for (const key of selectionKeys) {
|
||||||
|
if (!key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entry = postSelectionCache.get(key);
|
||||||
|
if (entry && Date.now() - entry.timestamp < LAST_SELECTION_MAX_AGE) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
let postText = '';
|
let postText = '';
|
||||||
const cachedSelection = postSelectionCache.get(postElement);
|
const cachedSelection = resolveRecentSelection();
|
||||||
if (cachedSelection && Date.now() - cachedSelection.timestamp < LAST_SELECTION_MAX_AGE) {
|
if (cachedSelection) {
|
||||||
console.log('[FB Tracker] Using cached selection text');
|
console.log('[FB Tracker] Using cached selection text');
|
||||||
postText = cachedSelection.text;
|
postText = cachedSelection.text;
|
||||||
}
|
}
|
||||||
@@ -3479,22 +3651,25 @@ async function addAICommentButton(container, postElement) {
|
|||||||
throwIfCancelled();
|
throwIfCancelled();
|
||||||
|
|
||||||
if (!postText) {
|
if (!postText) {
|
||||||
postText = getSelectedTextFromPost(postElement);
|
const selectionSource = postContext || postElement;
|
||||||
if (postText) {
|
if (selectionSource) {
|
||||||
console.log('[FB Tracker] Using active selection text');
|
postText = getSelectedTextFromPost(selectionSource);
|
||||||
|
if (postText) {
|
||||||
|
console.log('[FB Tracker] Using active selection text');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!postText) {
|
if (!postText) {
|
||||||
const latestCached = postSelectionCache.get(postElement);
|
const latestCached = resolveRecentSelection();
|
||||||
if (latestCached && Date.now() - latestCached.timestamp < LAST_SELECTION_MAX_AGE) {
|
if (latestCached) {
|
||||||
console.log('[FB Tracker] Using latest cached selection after check');
|
console.log('[FB Tracker] Using latest cached selection after check');
|
||||||
postText = latestCached.text;
|
postText = latestCached.text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!postText) {
|
if (!postText) {
|
||||||
postText = extractPostText(postElement);
|
postText = extractPostText(postContext);
|
||||||
if (postText) {
|
if (postText) {
|
||||||
console.log('[FB Tracker] Fallback to DOM extraction');
|
console.log('[FB Tracker] Fallback to DOM extraction');
|
||||||
}
|
}
|
||||||
@@ -3504,7 +3679,11 @@ async function addAICommentButton(container, postElement) {
|
|||||||
throw new Error('Konnte Post-Text nicht extrahieren');
|
throw new Error('Konnte Post-Text nicht extrahieren');
|
||||||
}
|
}
|
||||||
|
|
||||||
postSelectionCache.delete(postElement);
|
selectionKeys.forEach((key) => {
|
||||||
|
if (key) {
|
||||||
|
postSelectionCache.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
throwIfCancelled();
|
throwIfCancelled();
|
||||||
|
|
||||||
@@ -3523,32 +3702,45 @@ async function addAICommentButton(container, postElement) {
|
|||||||
|
|
||||||
console.log('[FB Tracker] Generated comment:', comment);
|
console.log('[FB Tracker] Generated comment:', comment);
|
||||||
|
|
||||||
let commentInput = findCommentInput(postElement);
|
const dialogRoot = container.closest(DIALOG_ROOT_SELECTOR)
|
||||||
|
|| (postContext && postContext.closest(DIALOG_ROOT_SELECTOR));
|
||||||
|
|
||||||
|
let commentInput = findCommentInput(postContext, { preferredRoot: dialogRoot });
|
||||||
let waitedForInput = false;
|
let waitedForInput = false;
|
||||||
|
|
||||||
if (!commentInput) {
|
if (!commentInput) {
|
||||||
console.log('[FB Tracker] Comment input not found, trying to click comment button');
|
console.log('[FB Tracker] Comment input not found, trying to click comment button');
|
||||||
const buttonClicked = findAndClickCommentButton(postElement);
|
let buttonClicked = findAndClickCommentButton(postContext);
|
||||||
|
|
||||||
|
if (!buttonClicked && dialogRoot) {
|
||||||
|
const dialogCommentButton = dialogRoot.querySelector('[data-ad-rendering-role="comment_button"], [aria-label*="Kommentieren"], [aria-label*="Comment"]');
|
||||||
|
if (dialogCommentButton && isElementVisible(dialogCommentButton)) {
|
||||||
|
dialogCommentButton.click();
|
||||||
|
buttonClicked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateProcessingText(buttonClicked ? '⏳ Öffne Kommentare...' : '⏳ Suche Kommentarfeld...');
|
updateProcessingText(buttonClicked ? '⏳ Öffne Kommentare...' : '⏳ Suche Kommentarfeld...');
|
||||||
|
|
||||||
waitedForInput = true;
|
waitedForInput = true;
|
||||||
commentInput = await waitForCommentInput(postElement, {
|
commentInput = await waitForCommentInput(postContext, {
|
||||||
encodedPostUrl,
|
encodedPostUrl,
|
||||||
timeout: buttonClicked ? 8000 : 5000,
|
timeout: buttonClicked ? 8000 : 5000,
|
||||||
interval: 250,
|
interval: 250,
|
||||||
context: aiContext
|
context: aiContext,
|
||||||
|
preferredRoot: dialogRoot
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!commentInput && !waitedForInput) {
|
if (!commentInput && !waitedForInput) {
|
||||||
updateProcessingText('⏳ Suche Kommentarfeld...');
|
updateProcessingText('⏳ Suche Kommentarfeld...');
|
||||||
waitedForInput = true;
|
waitedForInput = true;
|
||||||
commentInput = await waitForCommentInput(postElement, {
|
commentInput = await waitForCommentInput(postContext, {
|
||||||
encodedPostUrl,
|
encodedPostUrl,
|
||||||
timeout: 4000,
|
timeout: 4000,
|
||||||
interval: 200,
|
interval: 200,
|
||||||
context: aiContext
|
context: aiContext,
|
||||||
|
preferredRoot: dialogRoot
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,68 @@
|
|||||||
color: #65676b;
|
color: #65676b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.credential-item__status {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #f0f2f5;
|
||||||
|
color: #1c1e21;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-status.status-badge--success {
|
||||||
|
background: rgba(66, 183, 42, 0.12);
|
||||||
|
color: #2d7a32;
|
||||||
|
border-color: rgba(66, 183, 42, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-status.status-badge--warning {
|
||||||
|
background: rgba(252, 160, 0, 0.12);
|
||||||
|
color: #9f580a;
|
||||||
|
border-color: rgba(252, 160, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-status.status-badge--danger {
|
||||||
|
background: rgba(231, 76, 60, 0.12);
|
||||||
|
color: #a5281b;
|
||||||
|
border-color: rgba(231, 76, 60, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-status.status-badge--info {
|
||||||
|
background: rgba(24, 119, 242, 0.1);
|
||||||
|
color: #1659c7;
|
||||||
|
border-color: rgba(24, 119, 242, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-status.status-badge--neutral {
|
||||||
|
background: rgba(101, 103, 107, 0.12);
|
||||||
|
color: #42464b;
|
||||||
|
border-color: rgba(101, 103, 107, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-status.status-badge--muted {
|
||||||
|
background: rgba(148, 153, 160, 0.12);
|
||||||
|
color: #4b4f56;
|
||||||
|
border-color: rgba(148, 153, 160, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-item__meta {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #65676b;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.credential-item__actions {
|
.credential-item__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
138
web/settings.js
138
web/settings.js
@@ -123,6 +123,134 @@ async function loadSettings() {
|
|||||||
'Schreibe einen freundlichen, authentischen Kommentar auf Deutsch zu folgendem Facebook-Post. Der Kommentar soll natürlich wirken und maximal 2-3 Sätze lang sein:\n\n';
|
'Schreibe einen freundlichen, authentischen Kommentar auf Deutsch zu folgendem Facebook-Post. Der Kommentar soll natürlich wirken und maximal 2-3 Sätze lang sein:\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shorten(text, maxLength = 80) {
|
||||||
|
if (typeof text !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (text.length <= maxLength) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return `${text.slice(0, maxLength - 3)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtmlAttr(text) {
|
||||||
|
return escapeHtml(text || '').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimeLabel(iso) {
|
||||||
|
if (!iso) return '';
|
||||||
|
const date = new Date(iso);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRelativePast(iso) {
|
||||||
|
if (!iso) return '';
|
||||||
|
const date = new Date(iso);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
const diffMs = Date.now() - date.getTime();
|
||||||
|
if (diffMs < 0) return 'gerade eben';
|
||||||
|
const diffMinutes = Math.round(diffMs / 60000);
|
||||||
|
if (diffMinutes <= 1) return 'gerade eben';
|
||||||
|
if (diffMinutes < 60) return `vor ${diffMinutes} Min`;
|
||||||
|
const diffHours = Math.round(diffMinutes / 60);
|
||||||
|
if (diffHours < 24) return `vor ${diffHours} Std`;
|
||||||
|
const diffDays = Math.round(diffHours / 24);
|
||||||
|
if (diffDays === 1) return 'gestern';
|
||||||
|
if (diffDays < 7) return `vor ${diffDays} Tagen`;
|
||||||
|
return date.toLocaleDateString('de-DE');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRelativeFuture(iso) {
|
||||||
|
if (!iso) return '';
|
||||||
|
const date = new Date(iso);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
const diffMs = date.getTime() - Date.now();
|
||||||
|
if (diffMs <= 0) return 'gleich';
|
||||||
|
const diffMinutes = Math.round(diffMs / 60000);
|
||||||
|
if (diffMinutes < 1) return 'gleich';
|
||||||
|
if (diffMinutes < 60) return `in ${diffMinutes} Min`;
|
||||||
|
const diffHours = Math.round(diffMinutes / 60);
|
||||||
|
if (diffHours < 24) return `in ${diffHours} Std`;
|
||||||
|
return date.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCredentialBadges(credential) {
|
||||||
|
const badges = [];
|
||||||
|
if (!credential.is_active) {
|
||||||
|
badges.push({
|
||||||
|
label: 'Deaktiviert',
|
||||||
|
className: 'status-badge--muted',
|
||||||
|
title: 'Dieser Login ist derzeit deaktiviert'
|
||||||
|
});
|
||||||
|
} else if (credential.auto_disabled) {
|
||||||
|
const untilText = credential.auto_disabled_until ? formatRelativeFuture(credential.auto_disabled_until) : 'läuft';
|
||||||
|
const untilTime = credential.auto_disabled_until ? formatTimeLabel(credential.auto_disabled_until) : '';
|
||||||
|
const reason = credential.auto_disabled_reason ? credential.auto_disabled_reason.replace(/^AUTO:/, '').trim() : 'Automatisch deaktiviert';
|
||||||
|
badges.push({
|
||||||
|
label: `Cooldown ${untilText}${untilTime ? ` (${untilTime})` : ''}`.trim(),
|
||||||
|
className: 'status-badge--warning',
|
||||||
|
title: reason || 'Automatisch deaktiviert'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
badges.push({
|
||||||
|
label: 'Aktiv',
|
||||||
|
className: 'status-badge--success',
|
||||||
|
title: 'Login ist aktiv'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential.last_error_message) {
|
||||||
|
badges.push({
|
||||||
|
label: `Fehler ${formatRelativePast(credential.last_error_at)}`.trim(),
|
||||||
|
className: 'status-badge--danger',
|
||||||
|
title: credential.last_error_message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential.usage_24h_count) {
|
||||||
|
const resetHint = credential.usage_24h_reset_at ? `Reset ${formatRelativeFuture(credential.usage_24h_reset_at)}` : '24h Nutzung';
|
||||||
|
badges.push({
|
||||||
|
label: `24h: ${credential.usage_24h_count}`,
|
||||||
|
className: 'status-badge--info',
|
||||||
|
title: resetHint
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential.last_rate_limit_remaining) {
|
||||||
|
badges.push({
|
||||||
|
label: `Limit: ${credential.last_rate_limit_remaining}`,
|
||||||
|
className: 'status-badge--neutral',
|
||||||
|
title: 'Letzter „rate limit remaining“-Wert'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCredentialMetaLines(credential) {
|
||||||
|
const lines = [];
|
||||||
|
if (credential.last_success_at) {
|
||||||
|
lines.push(`Zuletzt erfolgreich: ${formatRelativePast(credential.last_success_at)}`);
|
||||||
|
}
|
||||||
|
if (!credential.last_success_at && credential.last_used_at) {
|
||||||
|
lines.push(`Zuletzt genutzt: ${formatRelativePast(credential.last_used_at)}`);
|
||||||
|
}
|
||||||
|
if (credential.rate_limit_reset_at && !credential.auto_disabled) {
|
||||||
|
lines.push(`Limit-Reset ${formatRelativeFuture(credential.rate_limit_reset_at)}`);
|
||||||
|
}
|
||||||
|
if (credential.latest_event && credential.latest_event.type) {
|
||||||
|
const typeLabel = credential.latest_event.type.replace(/_/g, ' ');
|
||||||
|
const eventTime = credential.latest_event.created_at ? formatRelativePast(credential.latest_event.created_at) : '';
|
||||||
|
const message = credential.latest_event.message ? shorten(credential.latest_event.message, 90) : '';
|
||||||
|
const parts = [`Letztes Event (${typeLabel})`];
|
||||||
|
if (eventTime) parts.push(eventTime);
|
||||||
|
if (message) parts.push(`– ${message}`);
|
||||||
|
lines.push(parts.join(' '));
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
function renderCredentials() {
|
function renderCredentials() {
|
||||||
const list = document.getElementById('credentialsList');
|
const list = document.getElementById('credentialsList');
|
||||||
if (!credentials.length) {
|
if (!credentials.length) {
|
||||||
@@ -133,6 +261,14 @@ function renderCredentials() {
|
|||||||
const providerName = escapeHtml(PROVIDER_INFO[c.provider]?.name || c.provider);
|
const providerName = escapeHtml(PROVIDER_INFO[c.provider]?.name || c.provider);
|
||||||
const modelLabel = c.model ? ` · ${escapeHtml(c.model)}` : '';
|
const modelLabel = c.model ? ` · ${escapeHtml(c.model)}` : '';
|
||||||
const endpointLabel = c.base_url ? ` · ${escapeHtml(c.base_url)}` : '';
|
const endpointLabel = c.base_url ? ` · ${escapeHtml(c.base_url)}` : '';
|
||||||
|
const badges = buildCredentialBadges(c);
|
||||||
|
const badgesHtml = badges.length
|
||||||
|
? `<div class="credential-item__status">${badges.map(badge => `<span class="credential-status ${badge.className}"${badge.title ? ` title="${escapeHtmlAttr(badge.title)}"` : ''}>${escapeHtml(badge.label)}</span>`).join('')}</div>`
|
||||||
|
: '';
|
||||||
|
const metaLines = buildCredentialMetaLines(c);
|
||||||
|
const metaHtml = metaLines.length
|
||||||
|
? `<div class="credential-item__meta">${metaLines.map(line => `<div>${escapeHtml(line)}</div>`).join('')}</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="credential-item" draggable="true" data-credential-id="${c.id}" data-index="${index}">
|
<div class="credential-item" draggable="true" data-credential-id="${c.id}" data-index="${index}">
|
||||||
@@ -145,6 +281,8 @@ function renderCredentials() {
|
|||||||
<div>
|
<div>
|
||||||
<div class="credential-item__name">${escapeHtml(c.name)}</div>
|
<div class="credential-item__name">${escapeHtml(c.name)}</div>
|
||||||
<div class="credential-item__provider">${providerName}${modelLabel}${endpointLabel}</div>
|
<div class="credential-item__provider">${providerName}${modelLabel}${endpointLabel}</div>
|
||||||
|
${badgesHtml}
|
||||||
|
${metaHtml}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user