([^<]+)<\/FOLLOW_UP_QUESTIONS>/);
let analysisContent = "";
let scoreContent = "";
let questionsContent = "";
if (analysisMatch && analysisMatch[1] !== undefined) {
analysisContent = analysisMatch[1].trim();
} else if (!scoreMatch && !questionsMatch) {
analysisContent = fullDescription;
} else {
analysisContent = "*Waiting for analysis...*";
}
if (scoreMatch && scoreMatch[1] !== undefined) {
scoreContent = scoreMatch[1].trim();
}
if (questionsMatch && questionsMatch[1] !== undefined) {
questionsContent = questionsMatch[1].trim();
}
let contentChanged = false;
const formattedAnalysis = formatTooltipDescription(analysisContent).description;
if (this.descriptionElement.innerHTML !== formattedAnalysis) {
this.descriptionElement.innerHTML = formattedAnalysis;
contentChanged = true;
}
if (scoreContent) {
const formattedScoreText = scoreContent
.replace(//g, '>') // Basic escaping
.replace(/SCORE_(\d+)/g, 'SCORE: $1') // Apply highlighting
.replace(/\n/g, '
');
if (this.scoreTextElement.innerHTML !== formattedScoreText) {
this.scoreTextElement.innerHTML = formattedScoreText;
contentChanged = true;
}
this.scoreTextElement.style.display = 'block';
} else {
if (this.scoreTextElement.style.display !== 'none') {
this.scoreTextElement.style.display = 'none';
this.scoreTextElement.innerHTML = '';
contentChanged = true;
}
}
if (questionsContent) {
const formattedQuestionsText = questionsContent.replace(//g, '>').replace(/\n/g, '
');
if (this.followUpQuestionsTextElement.innerHTML !== formattedQuestionsText) {
this.followUpQuestionsTextElement.innerHTML = formattedQuestionsText;
}
} else {
if (this.followUpQuestionsTextElement.innerHTML !== '') {
this.followUpQuestionsTextElement.innerHTML = '';
}
}
this.followUpQuestionsTextElement.style.display = 'none';
const formattedReasoning = formatTooltipDescription("", this.reasoning).reasoning;
if (this.reasoningTextElement.innerHTML !== formattedReasoning) {
this.reasoningTextElement.innerHTML = formattedReasoning;
contentChanged = true;
}
const showReasoning = !!formattedReasoning;
if ((this.reasoningDropdown.style.display === 'none') === showReasoning) {
this.reasoningDropdown.style.display = showReasoning ? 'block' : 'none';
contentChanged = true;
}
const renderedHistory = this._renderConversationHistory();
if (this.conversationContainerElement.innerHTML !== renderedHistory) {
this.conversationContainerElement.innerHTML = renderedHistory;
this.conversationContainerElement.style.display = this.conversationHistory.length > 0 ? 'block' : 'none';
contentChanged = true;
}
let questionsButtonsChanged = false;
if (this.followUpQuestionsElement.children.length !== (this.questions?.length || 0)) {
questionsButtonsChanged = true;
} else {
this.questions?.forEach((q, i) => {
const button = this.followUpQuestionsElement.children[i];
if (!button || button.dataset.questionText !== q) {
questionsButtonsChanged = true;
}
});
}
if (questionsButtonsChanged) {
this.followUpQuestionsElement.innerHTML = '';
if (this.questions && this.questions.length > 0) {
this.questions.forEach((question, index) => {
const questionButton = document.createElement('button');
questionButton.className = 'follow-up-question-button';
questionButton.textContent = `🤔 ${question}`;
questionButton.dataset.questionIndex = index;
questionButton.dataset.questionText = question;
this.followUpQuestionsElement.appendChild(questionButton);
});
this.followUpQuestionsElement.style.display = 'block';
} else {
this.followUpQuestionsElement.style.display = 'none';
}
contentChanged = true;
}
let metadataHTML = '';
let showMetadata = false;
const hasFullMetadata = this.metadata && Object.keys(this.metadata).length > 1 && this.metadata.model;
const hasOnlyGenId = this.metadata && this.metadata.generationId && Object.keys(this.metadata).length === 1;
if (hasFullMetadata) {
metadataHTML += '
';
metadataHTML += `Model: ${this.metadata.model}
`;
metadataHTML += `Tokens: prompt: ${this.metadata.promptTokens} / completion: ${this.metadata.completionTokens}
`;
if (this.metadata.reasoningTokens > 0) {
metadataHTML += `Reasoning Tokens: ${this.metadata.reasoningTokens}
`;
}
metadataHTML += `Latency: ${this.metadata.latency}
`;
if (this.metadata.mediaInputs > 0) {
metadataHTML += `Media: ${this.metadata.mediaInputs}
`;
}
metadataHTML += `Price: ${this.metadata.price}
`;
showMetadata = true;
} else if (hasOnlyGenId) {
metadataHTML += '
';
metadataHTML += `Generation ID: ${this.metadata.generationId} (fetching details...)
`;
showMetadata = true;
}
if (this.metadataElement.innerHTML !== metadataHTML) {
this.metadataElement.innerHTML = metadataHTML;
contentChanged = true;
}
if ((this.metadataElement.style.display === 'none') === showMetadata) {
this.metadataElement.style.display = showMetadata ? 'block' : 'none';
contentChanged = true;
}
const isStreaming = this.status === 'streaming';
if (this.tooltipElement.classList.contains('streaming-tooltip') !== isStreaming) {
this.tooltipElement.classList.toggle('streaming-tooltip', isStreaming);
contentChanged = true;
}
if (contentChanged) {
requestAnimationFrame(() => {
if (this.autoScroll && (wasNearBottom || !previousScrollHeight)) {
this._performAutoScroll();
} else if (!this.autoScroll && previousScrollHeight > 0) {
const newScrollHeight = this.tooltipElement.scrollHeight;
const scrollDiff = newScrollHeight - previousScrollHeight;
this.tooltipElement.scrollTop = previousScrollTop + scrollDiff;
}
this._updateScrollButtonVisibility();
});
} else {
this._updateScrollButtonVisibility();
}
}
_renderConversationHistory() {
if (!this.conversationHistory || this.conversationHistory.length === 0) {
return '';
}
const expandedStates = new Map();
if (this.conversationContainerElement) {
this.conversationContainerElement.querySelectorAll('.conversation-reasoning').forEach((dropdown, index) => {
expandedStates.set(index, dropdown.classList.contains('expanded'));
});
}
let historyHtml = '';
this.conversationHistory.forEach((turn, index) => {
const formattedQuestion = turn.question
.replace(//g, '>');
let uploadedImageHtml = '';
if (turn.uploadedImages && turn.uploadedImages.length > 0) {
uploadedImageHtml = `
${turn.uploadedImages.map(imageUrl => `

`).join('')}
`;
}
let formattedAnswer;
if (turn.answer === 'pending') {
formattedAnswer = 'Answering...';
} else {
formattedAnswer = turn.answer
.replace(//g, '>') // Escape potential raw HTML first
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // Added class
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/\*(.*?)\*/g, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/\n/g, '
');
}
if (index > 0) {
historyHtml += '
';
}
let reasoningHtml = '';
if (turn.reasoning && turn.reasoning.trim() !== '') {
const formattedReasoning = formatTooltipDescription("", turn.reasoning).reasoning;
const wasExpanded = expandedStates.get(index);
const expandedClass = wasExpanded ? ' expanded' : '';
const arrowChar = wasExpanded ? '▼' : '▶';
const contentStyle = wasExpanded ? 'style="max-height: 200px; padding: 8px;"' : 'style="max-height: 0; padding: 0 8px;"';
reasoningHtml = `
${arrowChar} Show Reasoning Trace
`;
}
historyHtml += `
You: ${formattedQuestion}
${uploadedImageHtml}
${reasoningHtml}
AI: ${formattedAnswer}
`;
});
if (this.conversationContainerElement) {
this.conversationContainerElement.innerHTML = historyHtml;
this._attachConversationReasoningListeners();
}
return historyHtml;
}
_attachConversationReasoningListeners() {
if (!this.conversationContainerElement) return;
this.conversationContainerElement.removeEventListener('click', this._handleConversationReasoningToggle);
this.conversationContainerElement.addEventListener('click', (e) => {
const toggleButton = e.target.closest('.conversation-reasoning .reasoning-toggle');
if (!toggleButton) return;
e.stopPropagation();
const dropdown = toggleButton.closest('.reasoning-dropdown');
const content = dropdown?.querySelector('.reasoning-content');
const arrow = dropdown?.querySelector('.reasoning-arrow');
if (!dropdown || !content || !arrow) return;
const isExpanded = dropdown.classList.toggle('expanded');
arrow.textContent = isExpanded ? '▼' : '▶';
toggleButton.setAttribute('aria-expanded', isExpanded);
content.style.maxHeight = isExpanded ? '200px' : '0';
content.style.padding = isExpanded ? '8px' : '0 8px';
});
}
_performAutoScroll() {
if (!this.tooltipElement || !this.autoScroll) return;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (this.tooltipElement && this.autoScroll && this.isVisible) {
const targetScroll = this.tooltipElement.scrollHeight;
this.tooltipElement.scrollTo({
top: targetScroll,
behavior: 'instant'
});
setTimeout(() => {
if (this.tooltipElement && this.autoScroll && this.isVisible) {
const isNearBottom = this.tooltipElement.scrollHeight - this.tooltipElement.scrollTop - this.tooltipElement.clientHeight < 5;
if (!isNearBottom) {
this.tooltipElement.scrollTop = this.tooltipElement.scrollHeight;
}
}
}, 50);
}
});
});
}
_setPosition() {
if (!this.isVisible || !this.indicatorElement || !this.tooltipElement) return;
const indicatorRect = this.indicatorElement.getBoundingClientRect();
const tooltip = this.tooltipElement;
const margin = 10;
const isMobile = isMobileDevice();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const safeAreaHeight = viewportHeight - margin;
const safeAreaWidth = viewportWidth - margin;
tooltip.style.maxHeight = '';
tooltip.style.overflowY = '';
tooltip.style.visibility = 'hidden';
tooltip.style.display = 'block';
const computedStyle = window.getComputedStyle(tooltip);
const tooltipWidth = parseFloat(computedStyle.width);
let tooltipHeight = parseFloat(computedStyle.height);
let left, top;
let finalMaxHeight = '';
let finalOverflowY = '';
if (isMobile) {
left = Math.max(margin, (viewportWidth - tooltipWidth) / 2);
if (left + tooltipWidth > safeAreaWidth) {
left = safeAreaWidth - tooltipWidth;
}
const maxTooltipHeight = viewportHeight * 0.8;
if (tooltipHeight > maxTooltipHeight) {
finalMaxHeight = `${maxTooltipHeight}px`;
finalOverflowY = 'scroll';
tooltipHeight = maxTooltipHeight;
}
top = Math.max(margin, (viewportHeight - tooltipHeight) / 2);
if (top + tooltipHeight > safeAreaHeight) {
top = safeAreaHeight - tooltipHeight;
}
} else {
left = indicatorRect.right + margin;
top = indicatorRect.top + (indicatorRect.height / 2) - (tooltipHeight / 2);
if (left + tooltipWidth > safeAreaWidth) {
left = indicatorRect.left - tooltipWidth - margin;
if (left < margin) {
left = Math.max(margin, (viewportWidth - tooltipWidth) / 2);
if (indicatorRect.bottom + tooltipHeight + margin <= safeAreaHeight) {
top = indicatorRect.bottom + margin;
}
else if (indicatorRect.top - tooltipHeight - margin >= margin) {
top = indicatorRect.top - tooltipHeight - margin;
}
else {
top = margin;
finalMaxHeight = `${safeAreaHeight - margin}px`;
finalOverflowY = 'scroll';
tooltipHeight = safeAreaHeight - margin;
}
}
}
if (top < margin) {
top = margin;
}
if (top + tooltipHeight > safeAreaHeight) {
if (tooltipHeight > safeAreaHeight - margin) {
top = margin;
finalMaxHeight = `${safeAreaHeight - margin}px`;
finalOverflowY = 'scroll';
} else {
top = safeAreaHeight - tooltipHeight;
}
}
}
tooltip.style.position = 'fixed';
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
tooltip.style.zIndex = '99999999';
tooltip.style.maxHeight = finalMaxHeight;
tooltip.style.overflowY = finalOverflowY;
if (finalOverflowY === 'scroll') {
tooltip.style.webkitOverflowScrolling = 'touch';
}
tooltip.style.visibility = 'visible';
}
_updateScrollButtonVisibility() {
if (!this.tooltipElement || !this.scrollButton) return;
const isStreaming = this.status === 'streaming';
if (!isStreaming) {
this.scrollButton.style.display = 'none';
return;
}
const isNearBottom = this.tooltipElement.scrollHeight - this.tooltipElement.scrollTop - this.tooltipElement.clientHeight < (isMobileDevice() ? 40 : 55);
this.scrollButton.style.display = isNearBottom ? 'none' : 'block';
}
_handleMouseEnter(event) {
if (isMobileDevice()) return;
this.show();
}
_handleMouseLeave(event) {
if (isMobileDevice()) return;
setTimeout(() => {
if (this.tooltipElement && !this.tooltipElement.matches(':hover') &&
this.indicatorElement && !this.indicatorElement.matches(':hover')) {
this.hide();
}
}, 100);
}
_handleIndicatorClick(event) {
event.stopPropagation();
event.preventDefault();
this.toggle();
}
_handleTooltipMouseEnter() {
if (!this.isPinned) {
this.show();
}
}
_handleTooltipMouseLeave() {
if (!this.isPinned) {
this.hide();
}
}
_handleTooltipClick(event) {
}
_handleTooltipScroll() {
if (!this.tooltipElement) return;
const isNearBottom = this.tooltipElement.scrollHeight - this.tooltipElement.scrollTop - this.tooltipElement.clientHeight < (isMobileDevice() ? 40 : 55);
if (!isNearBottom) {
if (this.autoScroll) {
this.autoScroll = false;
this.tooltipElement.dataset.autoScroll = 'false';
this.userInitiatedScroll = true;
}
} else {
if (this.userInitiatedScroll) {
this.autoScroll = true;
this.tooltipElement.dataset.autoScroll = 'true';
this.userInitiatedScroll = false;
}
}
this._updateScrollButtonVisibility();
}
_handlePinClick(e) {
e.stopPropagation();
if (this.isPinned) {
this.unpin();
} else {
this.pin();
}
}
_handleCopyClick(e) {
e.stopPropagation();
if (!this.descriptionElement || !this.reasoningTextElement || !this.copyButton) return;
let textToCopy = this.descriptionElement.textContent || '';
const reasoningContent = this.reasoningTextElement.textContent || '';
if (reasoningContent) {
textToCopy += '\n\nReasoning:\n' + reasoningContent;
}
navigator.clipboard.writeText(textToCopy).then(() => {
const originalText = this.copyButton.innerHTML;
this.copyButton.innerHTML = '✓';
this.copyButton.disabled = true;
setTimeout(() => {
this.copyButton.innerHTML = originalText;
this.copyButton.disabled = false;
}, 1500);
}).catch(err => {
});
}
_handleReasoningToggleClick(e) {
e.stopPropagation();
if (!this.reasoningDropdown || !this.reasoningContent || !this.reasoningArrow) return;
const isExpanded = this.reasoningDropdown.classList.toggle('expanded');
this.reasoningArrow.textContent = isExpanded ? '▼' : '▶';
if (isExpanded) {
this.reasoningContent.style.maxHeight = '300px';
this.reasoningContent.style.padding = '10px';
} else {
this.reasoningContent.style.maxHeight = '0';
this.reasoningContent.style.padding = '0 10px';
}
}
_handleScrollButtonClick(e) {
e.stopPropagation();
if (!this.tooltipElement) return;
this.autoScroll = true;
this.tooltipElement.dataset.autoScroll = 'true';
this._performAutoScroll();
this._updateScrollButtonVisibility();
}
_handleFollowUpQuestionClick(event) {
const isMockEvent = event.target && event.target.dataset && event.target.dataset.questionText && typeof event.target.closest !== 'function';
const button = isMockEvent ? event.target : event.target.closest('.follow-up-question-button');
if (!button) return;
event.stopPropagation();
const questionText = button.dataset.questionText;
const apiKey = browserGet('openrouter-api-key', '');
if (!isMockEvent) {
button.disabled = true;
button.textContent = `🤔 Asking: ${questionText}...`;
this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = true);
} else {
if (this.customQuestionInput) this.customQuestionInput.disabled = true;
if (this.customQuestionButton) {
this.customQuestionButton.disabled = true;
this.customQuestionButton.textContent = 'Asking...';
}
}
this.conversationHistory.push({
question: questionText,
answer: 'pending',
uploadedImages: [...this.uploadedImageDataUrls], // Store a copy of the image URLs array
reasoning: '' // Initialize reasoning for this turn
});
this._updateTooltipUI();
this.questions = [];
this._updateTooltipUI();
const userMessageContent = [{ type: "text", text: questionText }];
if (this.uploadedImageDataUrls && this.uploadedImageDataUrls.length > 0) {
this.uploadedImageDataUrls.forEach(url => {
userMessageContent.push({ type: "image_url", image_url: { "url": url } });
});
}
const userApiMessage = { role: "user", content: userMessageContent };
const historyForApiCall = [...this.qaConversationHistory, userApiMessage];
if (!apiKey) {
showStatus('API key missing. Cannot answer question.', 'error');
this._updateConversationHistory(questionText, "Error: API Key missing.", "");
if (!isMockEvent) {
button.disabled = false;
this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = false);
}
if (this.customQuestionInput) this.customQuestionInput.disabled = false;
if (this.customQuestionButton) {
this.customQuestionButton.disabled = false;
this.customQuestionButton.textContent = 'Ask';
}
this._clearFollowUpImage();
return;
}
if (!questionText) {
this._updateConversationHistory(questionText || "Error: Empty Question", "Error: Could not identify question.", "");
if (!isMockEvent) {
button.disabled = false;
this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = false);
}
if (this.customQuestionInput) this.customQuestionInput.disabled = false;
if (this.customQuestionButton) {
this.customQuestionButton.disabled = false;
this.customQuestionButton.textContent = 'Ask';
}
this._clearFollowUpImage();
return;
}
const currentArticle = this.findCurrentArticleElement();
try {
answerFollowUpQuestion(this.tweetId, historyForApiCall, apiKey, currentArticle, this);
} finally {
setTimeout(() => {
if (this.followUpQuestionsElement) {
this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => {
btn.disabled = false;
});
}
if (this.customQuestionInput) this.customQuestionInput.disabled = false;
if (this.customQuestionButton) {
this.customQuestionButton.disabled = false;
this.customQuestionButton.textContent = 'Ask';
}
this._clearFollowUpImage();
}, 100);
}
}
_handleCustomQuestionClick() {
if (!this.customQuestionInput || !this.customQuestionButton) return;
const questionText = this.customQuestionInput.value.trim();
if (!questionText) {
showStatus("Please enter a question.", "warning");
this.customQuestionInput.focus();
return;
}
const mockButton = {
dataset: { questionText: questionText },
disabled: false,
textContent: ''
};
this.followUpQuestionsElement?.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = true);
this._handleFollowUpQuestionClick({ target: mockButton, stopPropagation: () => {} });
if (this.customQuestionInput) {
this.customQuestionInput.value = '';
}
}
_handleFollowUpImageSelect(event) {
const files = event.target.files;
if (!files || files.length === 0) return;
if (this.followUpImageContainer && files.length > 0) {
this.followUpImageContainer.style.display = 'flex';
}
Array.from(files).forEach(file => {
if (file && file.type.startsWith('image/')) {
resizeImage(file, 512) // Resize to max 512px
.then(resizedDataUrl => {
this.uploadedImageDataUrls.push(resizedDataUrl);
this._addPreviewToContainer(resizedDataUrl);
})
.catch(error => {
showStatus(`Could not process image ${file.name}: ${error.message}`, "error");
});
} else if (file) {
showStatus(`Skipping non-image file: ${file.name}`, "warning");
}
});
event.target.value = null;
}
_addPreviewToContainer(imageDataUrl) {
if (!this.followUpImageContainer) return;
const previewItem = document.createElement('div');
previewItem.className = 'follow-up-image-preview-item';
previewItem.dataset.imageDataUrl = imageDataUrl;
const img = document.createElement('img');
img.src = imageDataUrl;
img.className = 'follow-up-image-preview-thumbnail';
const removeBtn = document.createElement('button');
removeBtn.textContent = '×';
removeBtn.className = 'follow-up-image-remove-btn';
removeBtn.title = 'Remove this image';
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this._removeSpecificUploadedImage(imageDataUrl);
});
previewItem.appendChild(img);
previewItem.appendChild(removeBtn);
this.followUpImageContainer.appendChild(previewItem);
}
_removeSpecificUploadedImage(imageDataUrl) {
this.uploadedImageDataUrls = this.uploadedImageDataUrls.filter(url => url !== imageDataUrl);
if (this.followUpImageContainer) {
const previewItemToRemove = this.followUpImageContainer.querySelector(`div.follow-up-image-preview-item[data-image-data-url="${CSS.escape(imageDataUrl)}"]`);
if (previewItemToRemove) {
previewItemToRemove.remove();
}
if (this.uploadedImageDataUrls.length === 0) {
this.followUpImageContainer.style.display = 'none';
}
}
}
_clearFollowUpImage() {
this.uploadedImageDataUrls = [];
if (this.followUpImageContainer) {
this.followUpImageContainer.innerHTML = '';
this.followUpImageContainer.style.display = 'none';
}
if (this.followUpImageInput) {
this.followUpImageInput.value = null;
}
}
_updateConversationHistory(question, answer, reasoning = '') {
const entryIndex = this.conversationHistory.findIndex(turn => turn.question === question && turn.answer === 'pending');
if (entryIndex !== -1) {
this.conversationHistory[entryIndex].answer = answer;
this.conversationHistory[entryIndex].reasoning = reasoning;
this._updateTooltipUI();
} else {
}
}
_renderStreamingAnswer(streamingText, reasoningText = '') {
if (!this.conversationContainerElement) return;
const conversationTurns = this.conversationContainerElement.querySelectorAll('.conversation-turn');
const lastTurnElement = conversationTurns.length > 0 ? conversationTurns[conversationTurns.length - 1] : null;
if (!lastTurnElement) {
return;
}
const lastHistoryEntry = this.conversationHistory.length > 0 ? this.conversationHistory[this.conversationHistory.length -1] : null;
if (!(lastHistoryEntry && lastHistoryEntry.answer === 'pending')) {
return;
}
let reasoningDropdown = lastTurnElement.querySelector('.reasoning-dropdown');
const hasReasoning = reasoningText && reasoningText.trim() !== '';
if (hasReasoning && !reasoningDropdown) {
reasoningDropdown = document.createElement('div');
reasoningDropdown.className = 'reasoning-dropdown conversation-reasoning';
const reasoningToggle = document.createElement('div');
reasoningToggle.className = 'reasoning-toggle';
const reasoningArrow = document.createElement('span');
reasoningArrow.className = 'reasoning-arrow';
reasoningArrow.textContent = '▶';
reasoningToggle.appendChild(reasoningArrow);
reasoningToggle.appendChild(document.createTextNode(' Show Reasoning Trace'));
const reasoningContent = document.createElement('div');
reasoningContent.className = 'reasoning-content';
const reasoningTextElement = document.createElement('p');
reasoningTextElement.className = 'reasoning-text';
reasoningContent.appendChild(reasoningTextElement);
reasoningDropdown.appendChild(reasoningToggle);
reasoningDropdown.appendChild(reasoningContent);
const answerElement = lastTurnElement.querySelector('.conversation-answer');
if (answerElement) {
lastTurnElement.insertBefore(reasoningDropdown, answerElement);
} else {
lastTurnElement.appendChild(reasoningDropdown);
}
reasoningToggle.addEventListener('click', (e) => {
e.stopPropagation();
const dropdown = e.target.closest('.reasoning-dropdown');
const content = dropdown?.querySelector('.reasoning-content');
const arrow = dropdown?.querySelector('.reasoning-arrow');
if (!dropdown || !content || !arrow) return;
const isExpanded = dropdown.classList.toggle('expanded');
arrow.textContent = isExpanded ? '▼' : '▶';
content.style.maxHeight = isExpanded ? '200px' : '0';
content.style.padding = isExpanded ? '8px' : '0 8px';
});
}
if (reasoningDropdown && hasReasoning) {
const reasoningTextElement = reasoningDropdown.querySelector('.reasoning-text');
if (reasoningTextElement) {
const formattedReasoning = formatTooltipDescription("", reasoningText).reasoning;
if (reasoningTextElement.innerHTML !== formattedReasoning) {
reasoningTextElement.innerHTML = formattedReasoning;
}
}
reasoningDropdown.style.display = 'block';
} else if (reasoningDropdown) {
reasoningDropdown.style.display = 'none';
}
const lastAnswerElement = lastTurnElement.querySelector('.conversation-answer');
if (lastAnswerElement) {
const formattedStreamingAnswer = streamingText
.replace(//g, '>') // Escape potential raw HTML first
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/\*(.*?)\*/g, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/\n/g, '
');
lastAnswerElement.innerHTML = `AI: ${formattedStreamingAnswer}|`;
} else {
}
if (this.autoScroll) {
this._performAutoScroll();
}
}
update({ status, score = null, description = '', reasoning = '', metadata = null, questions = undefined }) {
const statusChanged = status !== undefined && this.status !== status;
const scoreChanged = score !== null && this.score !== score;
const descriptionChanged = description !== '' && this.description !== description;
const reasoningChanged = reasoning !== '' && this.reasoning !== reasoning;
const metadataChanged = metadata !== null && JSON.stringify(this.metadata) !== JSON.stringify(metadata);
const questionsChanged = questions !== undefined && JSON.stringify(this.questions) !== JSON.stringify(questions);
if (!statusChanged && !scoreChanged && !descriptionChanged && !reasoningChanged && !metadataChanged && !questionsChanged) {
return;
}
if (statusChanged) this.status = status;
if (scoreChanged || statusChanged) {
this.score = (this.status === 'pending' || this.status === 'error') ? score : // Allow score display for error state if provided
(this.status === 'streaming' && score === null) ? this.score : // Keep existing score during streaming if new one is null
score;
}
if (descriptionChanged) this.description = description;
if (reasoningChanged) this.reasoning = reasoning;
if (metadataChanged) this.metadata = metadata;
if (questionsChanged) this.questions = questions;
if (statusChanged) {
const shouldAutoScroll = (this.status === 'pending' || this.status === 'streaming');
if (this.autoScroll !== shouldAutoScroll) {
this.autoScroll = shouldAutoScroll;
if (this.tooltipElement) {
this.tooltipElement.dataset.autoScroll = this.autoScroll ? 'true' : 'false';
}
}
}
if (statusChanged || scoreChanged) {
this._updateIndicatorUI();
}
if (descriptionChanged || reasoningChanged || statusChanged || metadataChanged || questionsChanged) {
this._updateTooltipUI();
} else {
this._updateScrollButtonVisibility();
}
this.updateDatasetAttributes();
}
show() {
if (!this.tooltipElement) return;
this.isVisible = true;
this.tooltipElement.style.display = 'block';
this._setPosition();
if (this.autoScroll && (this.status === 'streaming' || this.status === 'pending')) {
this._performAutoScroll();
}
this._updateScrollButtonVisibility();
}
hide() {
if (!this.isPinned && this.tooltipElement) {
this.isVisible = false;
this.tooltipElement.style.display = 'none';
} else if (this.isPinned) {
}
}
toggle() {
if (this.isVisible && !this.isPinned) {
this.hide();
} else {
this.show();
}
}
pin() {
if (!this.tooltipElement || !this.pinButton) return;
this.isPinned = true;
this.tooltipElement.classList.add('pinned');
this.pinButton.innerHTML = '📍';
this.pinButton.title = 'Unpin tooltip';
}
unpin() {
if (!this.tooltipElement || !this.pinButton) return;
this.isPinned = false;
this.tooltipElement.classList.remove('pinned');
this.pinButton.innerHTML = '📌';
this.pinButton.title = 'Pin tooltip (prevents auto-closing)';
setTimeout(() => {
if (this.tooltipElement && !this.tooltipElement.matches(':hover') &&
this.indicatorElement && !this.indicatorElement.matches(':hover')) {
this.hide();
}
}, 0);
}
_handleCloseClick(e) {
e.stopPropagation();
this.hide();
}
destroy() {
if (window.activeStreamingRequests && window.activeStreamingRequests[this.tweetId]) {
window.activeStreamingRequests[this.tweetId].abort();
delete window.activeStreamingRequests[this.tweetId];
}
this.indicatorElement?.removeEventListener('mouseenter', this._handleMouseEnter);
this.indicatorElement?.removeEventListener('mouseleave', this._handleMouseLeave);
this.indicatorElement?.removeEventListener('click', this._handleIndicatorClick);
this.tooltipElement?.removeEventListener('mouseenter', this._handleTooltipMouseEnter);
this.tooltipElement?.removeEventListener('mouseleave', this._handleTooltipMouseLeave);
this.tooltipElement?.removeEventListener('scroll', this._handleTooltipScroll);
this.pinButton?.removeEventListener('click', this._handlePinClick);
this.copyButton?.removeEventListener('click', this._handleCopyClick);
this.tooltipCloseButton?.removeEventListener('click', this._handleCloseClick);
this.reasoningToggle?.removeEventListener('click', this._handleReasoningToggleClick);
this.scrollButton?.removeEventListener('click', this._handleScrollButtonClick);
this.followUpQuestionsElement?.removeEventListener('click', this._handleFollowUpQuestionClick);
this.customQuestionButton?.removeEventListener('click', this._handleCustomQuestionClick);
this.customQuestionInput?.removeEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this._handleCustomQuestionClick();
}
});
this.indicatorElement?.remove();
this.tooltipElement?.remove();
ScoreIndicatorRegistry.remove(this.tweetId);
const currentArticle = this.findCurrentArticleElement();
if (currentArticle) {
delete currentArticle.dataset.hasScoreIndicator;
}
this.tweetArticle = null;
this.indicatorElement = null;
this.tooltipElement = null;
this.pinButton = null;
this.copyButton = null;
this.tooltipCloseButton = null;
this.reasoningToggle = null;
this.scrollButton = null;
this.conversationContainerElement = null;
this.followUpQuestionsElement = null;
this.customQuestionContainer = null;
this.customQuestionInput = null;
this.customQuestionButton = null;
this.followUpImageContainer = null;
this.followUpImageInput = null;
this.followUpImagePreview = null;
this.followUpRemoveImageButton = null;
this.attachImageButton = null;
this.uploadedImageDataUrls = [];
}
ensureIndicatorAttached() {
if (!this.indicatorElement) return;
const currentArticle = this.findCurrentArticleElement();
if (!currentArticle) {
return;
}
if (this.indicatorElement.parentElement !== currentArticle) {
const currentPosition = window.getComputedStyle(currentArticle).position;
if (currentPosition !== 'relative' && currentPosition !== 'absolute' && currentPosition !== 'fixed' && currentPosition !== 'sticky') {
currentArticle.style.position = 'relative';
}
currentArticle.appendChild(this.indicatorElement);
}
this.updateDatasetAttributes(currentArticle);
}
findCurrentArticleElement() {
const timeline = document.querySelector('main') || document.querySelector('div[data-testid="primaryColumn"]');
if (!timeline) return null;
const linkSelector = `a[href*="/status/${this.tweetId}"]`;
const linkElement = timeline.querySelector(linkSelector);
const article = linkElement?.closest('article[data-testid="tweet"]');
if (article) {
if (getTweetID(article) === this.tweetId) {
return article;
}
}
const articles = timeline.querySelectorAll('article[data-testid="tweet"]');
for (const art of articles) {
if (getTweetID(art) === this.tweetId) {
return art;
}
}
return null;
}
updateInitialReviewAndBuildHistory({ fullContext, mediaUrls, apiResponseContent, reviewSystemPrompt, followUpSystemPrompt }) {
const analysisMatch = apiResponseContent.match(/([\s\S]*?)<\/ANALYSIS>/);
const scoreMatch = apiResponseContent.match(/\s*SCORE_(\d+)\s*<\/SCORE>/);
const initialQuestions = extractFollowUpQuestions(apiResponseContent);
this.score = scoreMatch ? parseInt(scoreMatch[1], 10) : null;
this.description = analysisMatch ? analysisMatch[1].trim() : apiResponseContent;
this.questions = initialQuestions;
this.status = this.score !== null ? 'rated' : 'error';
const userMessageContent = [{ type: "text", text: fullContext }];
mediaUrls.forEach(url => {
userMessageContent.push({ type: "image_url", image_url: { "url": url } });
});
this.qaConversationHistory = [
{ role: "system", content: [{ type: "text", text: reviewSystemPrompt }] },
{ role: "user", content: userMessageContent },
{ role: "assistant", content: [{ type: "text", text: apiResponseContent }] },
{ role: "system", content: [{ type: "text", text: followUpSystemPrompt }] }
];
this._updateIndicatorUI();
this._updateTooltipUI();
this.updateDatasetAttributes();
}
updateAfterFollowUp({ assistantResponseContent, updatedQaHistory }) {
this.qaConversationHistory = updatedQaHistory;
const answerMatch = assistantResponseContent.match(/([\s\S]*?)<\/ANSWER>/);
const newFollowUpQuestions = extractFollowUpQuestions(assistantResponseContent);
const answerText = answerMatch ? answerMatch[1].trim() : assistantResponseContent;
this.questions = newFollowUpQuestions;
if (this.conversationHistory.length > 0) {
const lastTurn = this.conversationHistory[this.conversationHistory.length - 1];
if (lastTurn.answer === 'pending') {
lastTurn.answer = answerText;
}
}
this._updateTooltipUI();
this.updateDatasetAttributes();
}
rehydrateFromCache(cachedData) {
this.score = cachedData.score;
this.description = cachedData.description;
this.reasoning = cachedData.reasoning;
this.questions = cachedData.questions || [];
this.status = cachedData.status || (cachedData.score !== null ? (cachedData.fromStorage ? 'cached' : 'rated') : 'error');
this.metadata = cachedData.metadata || null;
this.qaConversationHistory = cachedData.qaConversationHistory || [];
this.isPinned = cachedData.isPinned || false;
this.conversationHistory = [];
if (this.qaConversationHistory.length > 0) {
let currentQuestion = null;
let currentUploadedImages = [];
let startIndex = 0;
for(let i=0; i < this.qaConversationHistory.length; i++) {
if (this.qaConversationHistory[i].role === 'system' && this.qaConversationHistory[i].content[0].text.includes('FOLLOW_UP_SYSTEM_PROMPT')) {
startIndex = i + 1;
break;
}
if (i === 3 && this.qaConversationHistory[i].role === 'system') {
startIndex = i + 1;
}
}
for (let i = startIndex; i < this.qaConversationHistory.length; i++) {
const message = this.qaConversationHistory[i];
if (message.role === 'user') {
const textContent = message.content.find(c => c.type === 'text');
currentQuestion = textContent ? textContent.text : "[Question not found]";
currentUploadedImages = message.content
.filter(c => c.type === 'image_url' && c.image_url && c.image_url.url.startsWith('data:image'))
.map(c => c.image_url.url);
} else if (message.role === 'assistant' && currentQuestion) {
const assistantTextContent = message.content.find(c => c.type === 'text');
const assistantAnswer = assistantTextContent ? assistantTextContent.text : "[Answer not found]";
const answerMatch = assistantAnswer.match(/([\s\S]*?)<\/ANSWER>/);
const uiAnswer = answerMatch ? answerMatch[1].trim() : assistantAnswer;
this.conversationHistory.push({
question: currentQuestion,
answer: uiAnswer,
uploadedImages: currentUploadedImages,
reasoning: '' // Reasoning extraction from assistant's full response for UI needs more logic
});
currentQuestion = null;
currentUploadedImages = [];
}
}
}
if (this.isPinned) {
this.pinButton.innerHTML = '📍';
this.tooltipElement?.classList.add('pinned');
} else {
this.pinButton.innerHTML = '📌';
this.tooltipElement?.classList.remove('pinned');
}
this._updateIndicatorUI();
this._updateTooltipUI();
this.updateDatasetAttributes();
}
}
const ScoreIndicatorRegistry = {
managers: new Map(),
get(tweetId, tweetArticle = null) {
if (!tweetId) {
return null;
}
if (this.managers.has(tweetId)) {
const existingManager = this.managers.get(tweetId);
if (tweetArticle && existingManager.tweetArticle !== tweetArticle) {
}
return existingManager;
} else if (tweetArticle) {
try {
const existingIndicator = tweetArticle.querySelector(`.score-indicator[data-tweet-id="${tweetId}"]`);
const existingTooltip = document.querySelector(`.score-description[data-tweet-id="${tweetId}"]`);
if (existingIndicator || existingTooltip) {
existingIndicator?.remove();
existingTooltip?.remove();
}
return new ScoreIndicator(tweetArticle);
} catch (e) {
return null;
}
}
return null;
},
add(tweetId, instance) {
if (this.managers.has(tweetId)) {
}
this.managers.set(tweetId, instance);
},
remove(tweetId) {
if (this.managers.has(tweetId)) {
this.managers.delete(tweetId);
}
},
cleanupOrphaned() {
let removedCount = 0;
const observedTimeline = document.querySelector('main') || document.querySelector('div[data-testid="primaryColumn"]');
if (!observedTimeline) return;
const visibleTweetIds = new Set();
observedTimeline.querySelectorAll('article[data-testid="tweet"]').forEach(article => {
const id = getTweetID(article);
if (id) visibleTweetIds.add(id);
});
for (const [tweetId, manager] of this.managers.entries()) {
const isConnected = manager.indicatorElement?.isConnected;
const isVisible = visibleTweetIds.has(tweetId);
if (!isConnected || !isVisible) {
manager.destroy();
removedCount++;
}
}
},
destroyAll() {
[...this.managers.values()].forEach(manager => manager.destroy());
this.managers.clear();
}
};
function formatTooltipDescription(description = "", reasoning = "") {
let formattedDescription = description === "*Waiting for analysis...*" ? description :
(description || "*waiting for content...*")
.replace(//g, '>') // Escape HTML tags first
.replace(/^# (.*$)/gm, '$1
')
.replace(/^## (.*$)/gm, '$1
')
.replace(/^### (.*$)/gm, '$1
')
.replace(/^#### (.*$)/gm, '$1
')
.replace(/\*\*(.*?)\*\*/g, '$1') // Bold
.replace(/\*(.*?)\*/g, '$1') // Italic
.replace(/`([^`]+)`/g, '$1') // Inline code
.replace(/SCORE_(\d+)/g, 'SCORE: $1') // Score highlight class
.replace(/\n\n/g, '
') // Paragraph breaks
.replace(/\n/g, '
');
let formattedReasoning = '';
if (reasoning && reasoning.trim()) {
formattedReasoning = reasoning
.replace(//g, '>')
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/\*(.*?)\*/g, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/\n\n/g, '
')
.replace(/\n/g, '
');
}
return { description: formattedDescription, reasoning: formattedReasoning };
}
// ----- ui/ui.js -----
function toggleElementVisibility(element, toggleButton, openText, closedText) {
if (!element || !toggleButton) return;
const isCurrentlyHidden = element.classList.contains('hidden');
toggleButton.innerHTML = isCurrentlyHidden ? openText : closedText;
if (isCurrentlyHidden) {
element.style.display = 'flex';
element.offsetHeight;
element.classList.remove('hidden');
} else {
element.classList.add('hidden');
setTimeout(() => {
if (element.classList.contains('hidden')) {
element.style.display = 'none';
}
}, 500);
}
if (element.id === 'tweet-filter-container') {
const filterToggle = document.getElementById('filter-toggle');
if (filterToggle) {
if (!isCurrentlyHidden) {
setTimeout(() => {
filterToggle.style.display = 'block';
}, 500);
} else {
filterToggle.style.display = 'none';
}
}
}
}
function injectUI() {
let menuHTML;
if (MENU) {
menuHTML = MENU;
} else {
menuHTML = browserGet('menuHTML');
}
if (!menuHTML) {
showStatus('Error: Could not load UI components.');
return null;
}
const containerId = 'tweetfilter-root-container';
let uiContainer = document.getElementById(containerId);
if (uiContainer) {
return uiContainer;
}
uiContainer = document.createElement('div');
uiContainer.id = containerId;
uiContainer.innerHTML = menuHTML;
document.body.appendChild(uiContainer);
const versionInfo = uiContainer.querySelector('#version-info');
if (versionInfo) {
versionInfo.textContent = `Twitter De-Sloppifier v${VERSION}`;
}
return uiContainer;
}
function initializeEventListeners(uiContainer) {
if (!uiContainer) {
return;
}
const settingsContainer = uiContainer.querySelector('#settings-container');
const filterContainer = uiContainer.querySelector('#tweet-filter-container');
const settingsToggleBtn = uiContainer.querySelector('#settings-toggle');
const filterToggleBtn = uiContainer.querySelector('#filter-toggle');
uiContainer.addEventListener('click', (event) => {
const target = event.target;
const actionElement = target.closest('[data-action]');
const action = actionElement?.dataset.action;
const setting = target.dataset.setting;
const paramName = target.closest('.parameter-row')?.dataset.paramName;
const tab = target.dataset.tab;
const toggleTargetId = target.closest('[data-toggle]')?.dataset.toggle;
if (action) {
switch (action) {
case 'close-filter':
toggleElementVisibility(filterContainer, filterToggleBtn, 'Filter Slider', 'Filter Slider');
break;
case 'toggle-settings':
case 'close-settings':
toggleElementVisibility(settingsContainer, settingsToggleBtn, '✕ Close', '⚙️ Settings');
break;
case 'save-api-key':
saveApiKey();
break;
case 'clear-cache':
clearTweetRatingsAndRefreshUI();
break;
case 'reset-settings':
resetSettings(isMobileDevice());
break;
case 'save-instructions':
saveInstructions();
break;
case 'add-handle':
addHandleFromInput();
break;
case 'clear-instructions-history':
clearInstructionsHistory();
break;
case 'export-cache':
exportCacheToJson();
break;
}
}
if (target.classList.contains('remove-handle')) {
const handleItem = target.closest('.handle-item');
const handleTextElement = handleItem?.querySelector('.handle-text');
if (handleTextElement) {
const handle = handleTextElement.textContent.substring(1);
removeHandleFromBlacklist(handle);
}
}
if (tab) {
switchTab(tab);
}
if (toggleTargetId) {
toggleAdvancedOptions(toggleTargetId);
}
});
uiContainer.addEventListener('input', (event) => {
const target = event.target;
const setting = target.dataset.setting;
const paramName = target.closest('.parameter-row')?.dataset.paramName;
if (setting) {
handleSettingChange(target, setting);
}
if (paramName) {
handleParameterChange(target, paramName);
}
if (target.id === 'tweet-filter-slider') {
handleFilterSliderChange(target);
}
if (target.id === 'tweet-filter-value') {
handleFilterValueInput(target);
}
});
uiContainer.addEventListener('change', (event) => {
const target = event.target;
const setting = target.dataset.setting;
if (setting === 'modelSortOrder') {
handleSettingChange(target, setting);
fetchAvailableModels();
}
if (setting === 'enableImageDescriptions') {
handleSettingChange(target, setting);
}
});
if (filterToggleBtn) {
filterToggleBtn.onclick = () => {
if (filterContainer) {
filterContainer.style.display = 'flex';
filterContainer.offsetHeight;
filterContainer.classList.remove('hidden');
}
filterToggleBtn.style.display = 'none';
};
}
document.addEventListener('click', closeAllSelectBoxes);
const showFreeModelsCheckbox = uiContainer.querySelector('#show-free-models');
if (showFreeModelsCheckbox) {
showFreeModelsCheckbox.addEventListener('change', function () {
showFreeModels = this.checked;
browserSet('showFreeModels', showFreeModels);
refreshModelsUI();
});
}
const sortDirectionBtn = uiContainer.querySelector('#sort-direction');
if (sortDirectionBtn) {
sortDirectionBtn.addEventListener('click', function () {
const currentDirection = browserGet('sortDirection', 'default');
const newDirection = currentDirection === 'default' ? 'reverse' : 'default';
browserSet('sortDirection', newDirection);
this.dataset.value = newDirection;
refreshModelsUI();
});
}
const modelSortSelect = uiContainer.querySelector('#model-sort-order');
if (modelSortSelect) {
modelSortSelect.addEventListener('change', function () {
browserSet('modelSortOrder', this.value);
if (this.value === 'latency-low-to-high') {
browserSet('sortDirection', 'default');
} else if (this.value === '') {
browserSet('sortDirection', 'default');
}
refreshModelsUI();
});
}
const providerSortSelect = uiContainer.querySelector('#provider-sort');
if (providerSortSelect) {
providerSortSelect.addEventListener('change', function () {
providerSort = this.value;
browserSet('providerSort', providerSort);
});
}
}
function saveApiKey() {
const apiKeyInput = document.getElementById('openrouter-api-key');
const apiKey = apiKeyInput.value.trim();
let previousAPIKey = browserGet('openrouter-api-key', '').length > 0 ? true : false;
if (apiKey) {
if (!previousAPIKey) {
resetSettings(true);
}
browserSet('openrouter-api-key', apiKey);
showStatus('API key saved successfully!');
fetchAvailableModels();
location.reload();
} else {
showStatus('Please enter a valid API key');
}
}
function exportCacheToJson() {
if (!tweetCache) {
showStatus('Error: Tweet cache not found.', 'error');
return;
}
try {
const cacheData = tweetCache.cache;
if (!cacheData || Object.keys(cacheData).length === 0) {
showStatus('Cache is empty. Nothing to export.', 'warning');
return;
}
const jsonString = JSON.stringify(cacheData, null, 2);
const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
link.setAttribute('download', `tweet-filter-cache-${timestamp}.json`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showStatus(`Cache exported successfully (${Object.keys(cacheData).length} items).`);
} catch (error) {
showStatus('Error exporting cache. Check console for details.', 'error');
}
}
function clearTweetRatingsAndRefreshUI() {
if (isMobileDevice() || confirm('Are you sure you want to clear all cached tweet ratings?')) {
tweetCache.clear(true);
pendingRequests = 0;
if (window.threadRelationships) {
window.threadRelationships = {};
browserSet('threadRelationships', '{}');
}
showStatus('All cached ratings and thread relationships cleared!');
if (observedTargetNode) {
observedTargetNode.querySelectorAll('article[data-testid="tweet"]').forEach(tweet => {
tweet.removeAttribute('data-sloppiness-score');
tweet.removeAttribute('data-rating-status');
tweet.removeAttribute('data-rating-description');
tweet.removeAttribute('data-cached-rating');
const indicator = tweet.querySelector('.score-indicator');
if (indicator) {
indicator.remove();
}
const tweetId = getTweetID(tweet);
if (tweetId) {
processedTweets.delete(tweetId);
const indicatorInstance = ScoreIndicatorRegistry.get(tweetId);
if (indicatorInstance) {
indicatorInstance.destroy();
}
scheduleTweetProcessing(tweet);
}
});
}
document.querySelectorAll('div[aria-label="Timeline: Conversation"], div[aria-label^="Timeline: Conversation"]').forEach(conversation => {
delete conversation.dataset.threadMapping;
delete conversation.dataset.threadMappedAt;
delete conversation.dataset.threadMappingInProgress;
delete conversation.dataset.threadHist;
delete conversation.dataset.threadMediaUrls;
});
}
}
function addHandleFromInput() {
const handleInput = document.getElementById('handle-input');
const handle = handleInput.value.trim();
if (handle) {
addHandleToBlacklist(handle);
handleInput.value = '';
}
}
function handleSettingChange(target, settingName) {
let value;
if (target.type === 'checkbox') {
value = target.checked;
} else {
value = target.value;
}
if (window[settingName] !== undefined) {
window[settingName] = value;
}
browserSet(settingName, value);
if (settingName === 'enableImageDescriptions') {
const imageModelContainer = document.getElementById('image-model-container');
if (imageModelContainer) {
imageModelContainer.style.display = value ? 'block' : 'none';
}
showStatus('Image descriptions ' + (value ? 'enabled' : 'disabled'));
}
}
function handleParameterChange(target, paramName) {
const row = target.closest('.parameter-row');
if (!row) return;
const slider = row.querySelector('.parameter-slider');
const valueInput = row.querySelector('.parameter-value');
const min = parseFloat(slider.min);
const max = parseFloat(slider.max);
let newValue = parseFloat(target.value);
if (target.type === 'number' && !isNaN(newValue)) {
newValue = Math.max(min, Math.min(max, newValue));
}
if (slider && valueInput) {
slider.value = newValue;
valueInput.value = newValue;
}
if (window[paramName] !== undefined) {
window[paramName] = newValue;
}
browserSet(paramName, newValue);
}
function handleFilterSliderChange(slider) {
const valueInput = document.getElementById('tweet-filter-value');
currentFilterThreshold = parseInt(slider.value, 10);
if (valueInput) {
valueInput.value = currentFilterThreshold.toString();
}
const percentage = (currentFilterThreshold / 10) * 100;
slider.style.setProperty('--slider-percent', `${percentage}%`);
browserSet('filterThreshold', currentFilterThreshold);
applyFilteringToAll();
}
function handleFilterValueInput(input) {
let value = parseInt(input.value, 10);
value = Math.max(0, Math.min(10, value));
input.value = value.toString();
const slider = document.getElementById('tweet-filter-slider');
if (slider) {
slider.value = value.toString();
const percentage = (value / 10) * 100;
slider.style.setProperty('--slider-percent', `${percentage}%`);
}
currentFilterThreshold = value;
browserSet('filterThreshold', currentFilterThreshold);
applyFilteringToAll();
}
function switchTab(tabName) {
const settingsContent = document.querySelector('#settings-container .settings-content');
if (!settingsContent) return;
const tabs = settingsContent.querySelectorAll('.tab-content');
const buttons = settingsContent.querySelectorAll('.tab-navigation .tab-button');
tabs.forEach(tab => tab.classList.remove('active'));
buttons.forEach(btn => btn.classList.remove('active'));
const tabToShow = settingsContent.querySelector(`#${tabName}-tab`);
const buttonToActivate = settingsContent.querySelector(`.tab-navigation .tab-button[data-tab="${tabName}"]`);
if (tabToShow) tabToShow.classList.add('active');
if (buttonToActivate) buttonToActivate.classList.add('active');
}
function toggleAdvancedOptions(contentId) {
const content = document.getElementById(contentId);
const toggle = document.querySelector(`[data-toggle="${contentId}"]`);
if (!content || !toggle) return;
const icon = toggle.querySelector('.advanced-toggle-icon');
const isExpanded = content.classList.toggle('expanded');
if (icon) {
icon.classList.toggle('expanded', isExpanded);
}
if (isExpanded) {
content.style.maxHeight = content.scrollHeight + 'px';
} else {
content.style.maxHeight = '0';
}
}
function refreshSettingsUI() {
document.querySelectorAll('[data-setting]').forEach(input => {
const settingName = input.dataset.setting;
const value = browserGet(settingName, window[settingName]);
if (input.type === 'checkbox') {
input.checked = value;
handleSettingChange(input, settingName);
} else {
input.value = value;
}
});
document.querySelectorAll('.parameter-row[data-param-name]').forEach(row => {
const paramName = row.dataset.paramName;
const slider = row.querySelector('.parameter-slider');
const valueInput = row.querySelector('.parameter-value');
const value = browserGet(paramName, window[paramName]);
if (slider) slider.value = value;
if (valueInput) valueInput.value = value;
});
const filterSlider = document.getElementById('tweet-filter-slider');
const filterValueInput = document.getElementById('tweet-filter-value');
const currentThreshold = browserGet('filterThreshold', '5');
if (filterSlider && filterValueInput) {
filterSlider.value = currentThreshold;
filterValueInput.value = currentThreshold;
const percentage = (parseInt(currentThreshold, 10) / 10) * 100;
filterSlider.style.setProperty('--slider-percent', `${percentage}%`);
}
refreshHandleList(document.getElementById('handle-list'));
refreshModelsUI();
document.querySelectorAll('.advanced-content').forEach(content => {
if (!content.classList.contains('expanded')) {
content.style.maxHeight = '0';
}
});
document.querySelectorAll('.advanced-toggle-icon.expanded').forEach(icon => {
if (!icon.closest('.advanced-toggle')?.nextElementSibling?.classList.contains('expanded')) {
icon.classList.remove('expanded');
}
});
refreshInstructionsHistory();
}
function refreshHandleList(listElement) {
if (!listElement) return;
listElement.innerHTML = '';
if (blacklistedHandles.length === 0) {
const emptyMsg = document.createElement('div');
emptyMsg.style.cssText = 'padding: 8px; opacity: 0.7; font-style: italic;';
emptyMsg.textContent = 'No handles added yet';
listElement.appendChild(emptyMsg);
return;
}
blacklistedHandles.forEach(handle => {
const item = document.createElement('div');
item.className = 'handle-item';
const handleText = document.createElement('div');
handleText.className = 'handle-text';
handleText.textContent = '@' + handle;
item.appendChild(handleText);
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-handle';
removeBtn.textContent = '×';
removeBtn.title = 'Remove from list';
item.appendChild(removeBtn);
listElement.appendChild(item);
});
}
function refreshModelsUI() {
const modelSelectContainer = document.getElementById('model-select-container');
const imageModelSelectContainer = document.getElementById('image-model-select-container');
listedModels = [...availableModels];
if (!showFreeModels) {
listedModels = listedModels.filter(model => !model.slug.endsWith(':free'));
}
const sortDirection = browserGet('sortDirection', 'default');
const sortOrder = browserGet('modelSortOrder', 'throughput-high-to-low');
const toggleBtn = document.getElementById('sort-direction');
if (toggleBtn) {
switch (sortOrder) {
case 'latency-low-to-high':
toggleBtn.textContent = sortDirection === 'default' ? 'High-Low' : 'Low-High';
if (sortDirection === 'reverse') listedModels.reverse();
break;
case '': // Age
toggleBtn.textContent = sortDirection === 'default' ? 'New-Old' : 'Old-New';
if (sortDirection === 'reverse') listedModels.reverse();
break;
case 'top-weekly':
toggleBtn.textContent = sortDirection === 'default' ? 'Most Popular' : 'Least Popular';
if (sortDirection === 'reverse') listedModels.reverse();
break;
default:
toggleBtn.textContent = sortDirection === 'default' ? 'High-Low' : 'Low-High';
if (sortDirection === 'reverse') listedModels.reverse();
}
}
if (modelSelectContainer) {
modelSelectContainer.innerHTML = '';
createCustomSelect(
modelSelectContainer,
'model-selector',
listedModels.map(model => ({ value: model.slug || model.id, label: formatModelLabel(model) })),
selectedModel,
(newValue) => {
selectedModel = newValue;
browserSet('selectedModel', selectedModel);
showStatus('Rating model updated');
},
'Search rating models...'
);
}
if (imageModelSelectContainer) {
const visionModels = listedModels.filter(model =>
model.input_modalities?.includes('image') ||
model.architecture?.input_modalities?.includes('image') ||
model.architecture?.modality?.includes('image')
);
imageModelSelectContainer.innerHTML = '';
createCustomSelect(
imageModelSelectContainer,
'image-model-selector',
visionModels.map(model => ({ value: model.slug || model.id, label: formatModelLabel(model) })),
selectedImageModel,
(newValue) => {
selectedImageModel = newValue;
browserSet('selectedImageModel', selectedImageModel);
showStatus('Image model updated');
},
'Search vision models...'
);
}
}
function formatModelLabel(model) {
let label = model.slug || model.id || model.name || 'Unknown Model';
let pricingInfo = '';
const pricing = model.endpoint?.pricing || model.pricing;
if (pricing) {
const promptPrice = parseFloat(pricing.prompt);
const completionPrice = parseFloat(pricing.completion);
if (!isNaN(promptPrice)) {
pricingInfo += ` - $${(promptPrice * 1e6).toFixed(4)}/mil. tok.-in`;
if (!isNaN(completionPrice) && completionPrice !== promptPrice) {
pricingInfo += ` $${(completionPrice * 1e6).toFixed(4)}/mil. tok.-out`;
}
} else if (!isNaN(completionPrice)) {
pricingInfo += ` - $${(completionPrice * 1e6).toFixed(4)}/mil. tok.-out`;
}
}
const isVision = model.input_modalities?.includes('image') ||
model.architecture?.input_modalities?.includes('image') ||
model.architecture?.modality?.includes('image');
if (isVision) {
label = '🖼️ ' + label;
}
return label + pricingInfo;
}
function createCustomSelect(container, id, options, initialSelectedValue, onChange, searchPlaceholder) {
let currentSelectedValue = initialSelectedValue;
const customSelect = document.createElement('div');
customSelect.className = 'custom-select';
customSelect.id = id;
const selectSelected = document.createElement('div');
selectSelected.className = 'select-selected';
const selectItems = document.createElement('div');
selectItems.className = 'select-items';
selectItems.style.display = 'none';
const searchField = document.createElement('div');
searchField.className = 'search-field';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'search-input';
searchInput.placeholder = searchPlaceholder || 'Search...';
searchField.appendChild(searchInput);
selectItems.appendChild(searchField);
function renderOptions(filter = '') {
while (selectItems.childNodes.length > 1) {
selectItems.removeChild(selectItems.lastChild);
}
const filteredOptions = options.filter(opt =>
opt.label.toLowerCase().includes(filter.toLowerCase())
);
if (filteredOptions.length === 0) {
const noResults = document.createElement('div');
noResults.textContent = 'No matches found';
noResults.style.cssText = 'opacity: 0.7; font-style: italic; padding: 10px; text-align: center; cursor: default;';
selectItems.appendChild(noResults);
}
filteredOptions.forEach(option => {
const optionDiv = document.createElement('div');
optionDiv.textContent = option.label;
optionDiv.dataset.value = option.value;
if (option.value === currentSelectedValue) {
optionDiv.classList.add('same-as-selected');
}
optionDiv.addEventListener('click', (e) => {
e.stopPropagation();
currentSelectedValue = option.value;
selectSelected.textContent = option.label;
selectItems.style.display = 'none';
selectSelected.classList.remove('select-arrow-active');
selectItems.querySelectorAll('div[data-value]').forEach(div => {
div.classList.toggle('same-as-selected', div.dataset.value === currentSelectedValue);
});
onChange(currentSelectedValue);
});
selectItems.appendChild(optionDiv);
});
}
const initialOption = options.find(opt => opt.value === currentSelectedValue);
selectSelected.textContent = initialOption ? initialOption.label : 'Select an option';
customSelect.appendChild(selectSelected);
customSelect.appendChild(selectItems);
container.appendChild(customSelect);
renderOptions();
searchInput.addEventListener('input', () => renderOptions(searchInput.value));
searchInput.addEventListener('click', e => e.stopPropagation());
selectSelected.addEventListener('click', (e) => {
e.stopPropagation();
closeAllSelectBoxes(customSelect);
const isHidden = selectItems.style.display === 'none';
selectItems.style.display = isHidden ? 'block' : 'none';
selectSelected.classList.toggle('select-arrow-active', isHidden);
if (isHidden) {
searchInput.focus();
searchInput.select();
renderOptions();
}
});
}
function closeAllSelectBoxes(exceptThisOne = null) {
document.querySelectorAll('.custom-select').forEach(select => {
if (select === exceptThisOne) return;
const items = select.querySelector('.select-items');
const selected = select.querySelector('.select-selected');
if (items) items.style.display = 'none';
if (selected) selected.classList.remove('select-arrow-active');
});
}
function resetSettings(noconfirm = false) {
if (noconfirm || confirm('Are you sure you want to reset all settings to their default values? This will not clear your cached ratings, blacklisted handles, or instruction history.')) {
tweetCache.clear();
const defaults = {
selectedModel: 'openai/gpt-4.1-nano',
selectedImageModel: 'openai/gpt-4.1-nano',
enableImageDescriptions: false,
enableStreaming: true,
modelTemperature: 0.5,
modelTopP: 0.9,
imageModelTemperature: 0.5,
imageModelTopP: 0.9,
maxTokens: 0,
filterThreshold: 5,
userDefinedInstructions: 'Rate the tweet on a scale from 1 to 10 based on its clarity, insight, creativity, and overall quality.',
modelSortOrder: 'throughput-high-to-low'
};
for (const key in defaults) {
if (window[key] !== undefined) {
window[key] = defaults[key];
}
browserSet(key, defaults[key]);
}
refreshSettingsUI();
fetchAvailableModels();
showStatus('Settings reset to defaults');
}
}
function addHandleToBlacklist(handle) {
handle = handle.trim().replace(/^@/, '');
if (handle === '' || blacklistedHandles.includes(handle)) {
showStatus(handle === '' ? 'Handle cannot be empty.' : `@${handle} is already on the list.`);
return;
}
blacklistedHandles.push(handle);
browserSet('blacklistedHandles', blacklistedHandles.join('\n'));
refreshHandleList(document.getElementById('handle-list'));
showStatus(`Added @${handle} to auto-rate list.`);
}
function removeHandleFromBlacklist(handle) {
const index = blacklistedHandles.indexOf(handle);
if (index > -1) {
blacklistedHandles.splice(index, 1);
browserSet('blacklistedHandles', blacklistedHandles.join('\n'));
refreshHandleList(document.getElementById('handle-list'));
showStatus(`Removed @${handle} from auto-rate list.`);
} else console.warn(`Attempted to remove non-existent handle: ${handle}`);
}
function initialiseUI() {
const uiContainer = injectUI();
if (!uiContainer) return;
initializeEventListeners(uiContainer);
refreshSettingsUI();
fetchAvailableModels();
initializeFloatingCacheStats();
setInterval(updateCacheStatsUI, 3000);
if (!window.activeStreamingRequests) window.activeStreamingRequests = {};
}
function initializeFloatingCacheStats() {
const statsBadge = document.getElementById('tweet-filter-stats-badge');
if (!statsBadge) return;
statsBadge.title = 'Click to open settings';
statsBadge.addEventListener('click', () => {
const settingsToggle = document.getElementById('settings-toggle');
if (settingsToggle) {
settingsToggle.click();
}
});
let fadeTimeout;
const resetFadeTimeout = () => {
clearTimeout(fadeTimeout);
statsBadge.style.opacity = '1';
fadeTimeout = setTimeout(() => {
statsBadge.style.opacity = '0.3';
}, 5000);
};
statsBadge.addEventListener('mouseenter', () => {
statsBadge.style.opacity = '1';
clearTimeout(fadeTimeout);
});
statsBadge.addEventListener('mouseleave', resetFadeTimeout);
resetFadeTimeout();
updateCacheStatsUI();
}
// ----- ratingEngine.js -----
function filterSingleTweet(tweetArticle) {
const cell = tweetArticle.closest('div[data-testid="cellInnerDiv"]');
if (!cell) {
return;
}
const handles = getUserHandles(tweetArticle);
const authorHandle = handles.length > 0 ? handles[0] : '';
if (authorHandle && adAuthorCache.has(authorHandle)) {
const tweetId = getTweetID(tweetArticle);
if (tweetId) {
ScoreIndicatorRegistry.get(tweetId)?.destroy();
}
cell.innerHTML = '';
cell.dataset.filtered = 'true';
cell.dataset.isAd = 'true';
return;
}
const score = parseInt(tweetArticle.dataset.sloppinessScore || '9', 10);
const tweetId = getTweetID(tweetArticle);
const indicatorInstance = ScoreIndicatorRegistry.get(tweetId);
indicatorInstance?.ensureIndicatorAttached();
const currentFilterThreshold = parseInt(browserGet('filterThreshold', '1'));
const ratingStatus = tweetArticle.dataset.ratingStatus;
if (ratingStatus === 'pending' || ratingStatus === 'streaming') {
delete cell.dataset.filtered;
} else if (isNaN(score) || score < currentFilterThreshold) {
if (tweetId) {
ScoreIndicatorRegistry.get(tweetId)?.destroy();
}
cell.innerHTML = '';
cell.dataset.filtered = 'true';
}
}
async function applyTweetCachedRating(tweetArticle) {
const tweetId = getTweetID(tweetArticle);
const handles = getUserHandles(tweetArticle);
const userHandle = handles.length > 0 ? handles[0] : '';
if (userHandle && isUserBlacklisted(userHandle)) {
const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
if (indicatorInstance) {
const tweetText = getElementText(tweetArticle.querySelector(TWEET_TEXT_SELECTOR)) || "[Tweet text not found]";
const mediaUrls = await extractMediaLinks(tweetArticle);
const blacklistResponse = `
This user is on the blacklist. Tweets from this user are not rated by the AI and are always shown.
SCORE_10
Q_1. Rate this tweet anyway.
Q_2. N/A
Q_3. N/A
`;
indicatorInstance.updateInitialReviewAndBuildHistory({
fullContext: tweetText,
mediaUrls: mediaUrls,
apiResponseContent: blacklistResponse,
reviewSystemPrompt: REVIEW_SYSTEM_PROMPT, // Assumed global
followUpSystemPrompt: FOLLOW_UP_SYSTEM_PROMPT // Assumed global
});
tweetCache.set(tweetId, {
score: 10,
description: indicatorInstance.description,
reasoning: "",
questions: indicatorInstance.questions,
lastAnswer: "",
tweetContent: tweetText,
mediaUrls: mediaUrls,
streaming: false,
blacklisted: true,
timestamp: Date.now(),
qaConversationHistory: indicatorInstance.qaConversationHistory
});
} else {
tweetArticle.dataset.sloppinessScore = '10';
tweetArticle.dataset.blacklisted = 'true';
tweetArticle.dataset.ratingStatus = 'blacklisted';
tweetArticle.dataset.ratingDescription = 'User is blacklisted';
}
filterSingleTweet(tweetArticle);
return true;
}
const cachedRating = tweetCache.get(tweetId);
if (cachedRating) {
if (cachedRating.streaming === true &&
(cachedRating.score === undefined || cachedRating.score === null)) {
return false;
}
if (cachedRating.score !== undefined && cachedRating.score !== null) {
const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
if (indicatorInstance) {
indicatorInstance.rehydrateFromCache(cachedRating);
} else {
return false;
}
filterSingleTweet(tweetArticle);
return true;
} else if (!cachedRating.streaming) {
tweetCache.delete(tweetId);
return false;
}
}
return false;
}
function isUserBlacklisted(handle) {
if (!handle) return false;
handle = handle.toLowerCase().trim();
return blacklistedHandles.some(h => h.toLowerCase().trim() === handle);
}
const VALID_FINAL_STATES = ['rated', 'cached', 'blacklisted'];
const VALID_INTERIM_STATES = ['pending', 'streaming'];
function isValidFinalState(status) {
return VALID_FINAL_STATES.includes(status);
}
function isValidInterimState(status) {
return VALID_INTERIM_STATES.includes(status);
}
async function delayedProcessTweet(tweetArticle, tweetId, authorHandle) {
let processingSuccessful = false;
try {
const apiKey = browserGet('openrouter-api-key', '');
if (!apiKey) {
tweetArticle.dataset.ratingStatus = 'error';
tweetArticle.dataset.ratingDescription = "No API key";
ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
status: 'error',
score: 9,
description: "No API key",
questions: [],
lastAnswer: ""
});
filterSingleTweet(tweetArticle);
return;
}
if (authorHandle && adAuthorCache.has(authorHandle)) {
tweetArticle.dataset.ratingStatus = 'rated';
tweetArticle.dataset.ratingDescription = "Advertisement";
tweetArticle.dataset.sloppinessScore = '0';
ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
status: 'rated',
score: 0,
description: "Advertisement from known ad author",
questions: [],
lastAnswer: ""
});
filterSingleTweet(tweetArticle);
processingSuccessful = true;
return;
}
if (isAd(tweetArticle)) {
if (authorHandle) {
adAuthorCache.add(authorHandle);
}
tweetArticle.dataset.ratingStatus = 'rated';
tweetArticle.dataset.ratingDescription = "Advertisement";
tweetArticle.dataset.sloppinessScore = '0';
ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
status: 'rated',
score: 0,
description: "Advertisement",
questions: [],
lastAnswer: ""
});
filterSingleTweet(tweetArticle);
processingSuccessful = true;
return;
}
let score = 5;
let description = "";
let reasoning = "";
let questions = [];
let lastAnswer = "";
try {
const cachedRating = tweetCache.get(tweetId);
if (cachedRating) {
if (cachedRating.streaming === true &&
(cachedRating.score === undefined || cachedRating.score === null)) {
}
else if (!cachedRating.streaming && (cachedRating.score === undefined || cachedRating.score === null)) {
tweetCache.delete(tweetId);
}
}
const fullContextWithImageDescription = await getFullContext(tweetArticle, tweetId, apiKey);
if (!fullContextWithImageDescription) {
throw new Error("Failed to get tweet context");
}
let mediaURLs = [];
if (document.querySelector('div[aria-label="Timeline: Conversation"]')) {
const replyInfo = getTweetReplyInfo(tweetId);
if (replyInfo && replyInfo.replyTo) {
if (!tweetCache.has(tweetId)) {
tweetCache.set(tweetId, {});
}
if (!tweetCache.get(tweetId).threadContext) {
tweetCache.get(tweetId).threadContext = {
replyTo: replyInfo.to,
replyToId: replyInfo.replyTo,
isRoot: false
};
}
}
}
const mediaMatches1 = fullContextWithImageDescription.matchAll(/(?:\[MEDIA_URLS\]:\s*\n)(.*?)(?:\n|$)/g);
const mediaMatches2 = fullContextWithImageDescription.matchAll(/(?:\[QUOTED_TWEET_MEDIA_URLS\]:\s*\n)(.*?)(?:\n|$)/g);
for (const match of mediaMatches1) {
if (match[1]) {
mediaURLs.push(...match[1].split(', ').filter(url => url.trim()));
}
}
for (const match of mediaMatches2) {
if (match[1]) {
mediaURLs.push(...match[1].split(', ').filter(url => url.trim()));
}
}
mediaURLs = [...new Set(mediaURLs.filter(url => url.trim()))];
if (fullContextWithImageDescription) {
try {
const currentCache = tweetCache.get(tweetId);
const isCached = currentCache &&
!currentCache.streaming &&
currentCache.score !== undefined &&
currentCache.score !== null;
if (isCached) {
score = currentCache.score;
description = currentCache.description || "";
reasoning = currentCache.reasoning || "";
questions = currentCache.questions || [];
lastAnswer = currentCache.lastAnswer || "";
const mediaUrls = currentCache.mediaUrls || [];
processingSuccessful = true;
ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
status: currentCache.fromStorage ? 'cached' : 'rated',
score: score,
description: description,
reasoning: reasoning,
questions: questions,
lastAnswer: lastAnswer,
metadata: currentCache.metadata || null,
mediaUrls: mediaUrls // Pass mediaUrls to indicator
});
filterSingleTweet(tweetArticle);
return;
}
const rating = await rateTweetWithOpenRouter(fullContextWithImageDescription, tweetId, apiKey, mediaURLs, 3, tweetArticle, authorHandle);
score = rating.score;
description = rating.content;
reasoning = rating.reasoning || '';
questions = rating.questions || [];
lastAnswer = "";
let finalStatus = rating.error ? 'error' : 'rated';
if (!rating.error) {
const cacheEntry = tweetCache.get(tweetId);
if (cacheEntry && cacheEntry.fromStorage) {
finalStatus = 'cached';
} else if (rating.cached) {
finalStatus = 'cached';
}
}
tweetArticle.dataset.ratingStatus = finalStatus;
tweetArticle.dataset.ratingDescription = description || "not available";
tweetArticle.dataset.sloppinessScore = score?.toString() || '';
tweetArticle.dataset.ratingReasoning = reasoning;
ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
status: finalStatus,
score: score,
description: description,
reasoning: reasoning,
questions: questions,
lastAnswer: lastAnswer,
metadata: rating.data?.id ? { generationId: rating.data.id } : null, // Pass metadata
mediaUrls: mediaURLs // Pass mediaUrls to indicator
});
processingSuccessful = !rating.error;
filterSingleTweet(tweetArticle);
return;
} catch (apiError) {
score = 5;
description = `API Error: ${apiError.message}`;
reasoning = '';
questions = [];
lastAnswer = '';
processingSuccessful = false;
ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
status: 'error',
score: score,
description: description,
questions: [],
lastAnswer: ""
});
const errorCacheEntry = tweetCache.get(tweetId) || {};
errorCacheEntry.score = score;
errorCacheEntry.description = description;
errorCacheEntry.reasoning = reasoning;
errorCacheEntry.questions = questions;
errorCacheEntry.lastAnswer = lastAnswer;
errorCacheEntry.error = true;
errorCacheEntry.streaming = false;
tweetCache.set(tweetId, errorCacheEntry);
filterSingleTweet(tweetArticle);
return;
}
}
filterSingleTweet(tweetArticle);
} catch (error) {
ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
status: 'error',
score: 5,
description: "Error during processing: " + error.message,
questions: [],
lastAnswer: ""
});
filterSingleTweet(tweetArticle);
processingSuccessful = false;
} finally {
if (!processingSuccessful) {
processedTweets.delete(tweetId);
}
}
} catch (error) {
const indicatorInstance = ScoreIndicatorRegistry.get(tweetId);
if (indicatorInstance) {
indicatorInstance.update({
status: 'error',
score: 5,
description: "Error during processing: " + error.message,
questions: [],
lastAnswer: ""
});
}
filterSingleTweet(tweetArticle);
processingSuccessful = false;
} finally {
if (!processingSuccessful) {
processedTweets.delete(tweetId);
const indicatorInstance = ScoreIndicatorRegistry.get(tweetId);
if (indicatorInstance && !isValidFinalState(indicatorInstance.status)) {
setTimeout(() => {
if (!isValidFinalState(ScoreIndicatorRegistry.get(tweetId)?.status)) {
scheduleTweetProcessing(tweetArticle);
}
}, PROCESSING_DELAY_MS * 2);
}
}
}
}
async function scheduleTweetProcessing(tweetArticle) {
const tweetId = getTweetID(tweetArticle);
if (!tweetId) {
return;
}
if (window.activeStreamingRequests && window.activeStreamingRequests[tweetId]) {
return;
}
const handles = getUserHandles(tweetArticle);
const authorHandle = handles.length > 0 ? handles[0] : '';
if (authorHandle && adAuthorCache.has(authorHandle)) {
filterSingleTweet(tweetArticle);
return;
}
if (isAd(tweetArticle)) {
if (authorHandle) {
adAuthorCache.add(authorHandle);
}
filterSingleTweet(tweetArticle);
return;
}
const existingInstance = ScoreIndicatorRegistry.get(tweetId);
if (existingInstance) {
existingInstance.ensureIndicatorAttached();
if (isValidFinalState(existingInstance.status)) {
filterSingleTweet(tweetArticle);
return;
}
if (isValidInterimState(existingInstance.status) && processedTweets.has(tweetId)) {
filterSingleTweet(tweetArticle);
return;
}
processedTweets.delete(tweetId);
}
if (authorHandle && isUserBlacklisted(authorHandle)) {
const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
if (indicatorInstance) {
const tweetText = getElementText(tweetArticle.querySelector(TWEET_TEXT_SELECTOR)) || "[Tweet text not found]";
const mediaUrls = await extractMediaLinks(tweetArticle);
const blacklistResponse = `
This user is on the blacklist. Tweets from this user are not rated by the AI and are always shown.
SCORE_10
Q_1. Rate this tweet anyway.
Q_2. N/A
Q_3. N/A
`;
indicatorInstance.updateInitialReviewAndBuildHistory({
fullContext: tweetText,
mediaUrls: mediaUrls,
apiResponseContent: blacklistResponse,
reviewSystemPrompt: REVIEW_SYSTEM_PROMPT, // Assumed global
followUpSystemPrompt: FOLLOW_UP_SYSTEM_PROMPT // Assumed global
});
tweetCache.set(tweetId, {
score: 10,
description: indicatorInstance.description,
reasoning: "",
questions: indicatorInstance.questions,
lastAnswer: "",
tweetContent: tweetText,
mediaUrls: mediaUrls,
streaming: false,
blacklisted: true,
timestamp: Date.now(),
qaConversationHistory: indicatorInstance.qaConversationHistory
});
} else {
tweetArticle.dataset.sloppinessScore = '10';
tweetArticle.dataset.blacklisted = 'true';
tweetArticle.dataset.ratingStatus = 'blacklisted';
tweetArticle.dataset.ratingDescription = 'User is blacklisted';
}
filterSingleTweet(tweetArticle);
return;
}
if (tweetCache.has(tweetId)) {
const isIncompleteStreaming =
tweetCache.get(tweetId).streaming === true &&
(tweetCache.get(tweetId).score === undefined || tweetCache.get(tweetId).score === null);
if (!isIncompleteStreaming) {
const wasApplied = await applyTweetCachedRating(tweetArticle);
if (wasApplied) {
return;
}
}
}
if (processedTweets.has(tweetId)) {
const instance = ScoreIndicatorRegistry.get(tweetId);
if (instance) {
instance.ensureIndicatorAttached();
if (instance.status === 'pending' || instance.status === 'streaming') {
filterSingleTweet(tweetArticle);
return;
}
}
processedTweets.delete(tweetId);
}
const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
if (indicatorInstance) {
if (indicatorInstance.status !== 'blacklisted' &&
indicatorInstance.status !== 'cached' &&
indicatorInstance.status !== 'rated') {
indicatorInstance.update({ status: 'pending', score: null, description: 'Rating scheduled...', questions: [], lastAnswer: "" });
} else {
indicatorInstance.ensureIndicatorAttached();
filterSingleTweet(tweetArticle);
return;
}
} else {
}
if (!processedTweets.has(tweetId)) {
processedTweets.add(tweetId);
}
setTimeout(() => {
try {
delayedProcessTweet(tweetArticle, tweetId, authorHandle);
} catch (e) {
processedTweets.delete(tweetId);
}
}, PROCESSING_DELAY_MS);
}
let threadRelationships = {};
const THREAD_CHECK_INTERVAL = 2500;
const SWEEP_INTERVAL = 1000;
let threadMappingInProgress = false;
function loadThreadRelationships() {
try {
const savedRelationships = browserGet('threadRelationships', '{}');
threadRelationships = JSON.parse(savedRelationships);
} catch (e) {
threadRelationships = {};
}
}
function saveThreadRelationships() {
try {
const relationshipCount = Object.keys(threadRelationships).length;
if (relationshipCount > 1000) {
const entries = Object.entries(threadRelationships);
entries.sort((a, b) => (b[1].timestamp || 0) - (a[1].timestamp || 0));
const recent = entries.slice(0, 500);
threadRelationships = Object.fromEntries(recent);
}
browserSet('threadRelationships', JSON.stringify(threadRelationships));
} catch (e) {
}
}
loadThreadRelationships();
async function buildReplyChain(tweetId, maxDepth = 5) {
if (!tweetId || maxDepth <= 0) return [];
const chain = [];
let currentId = tweetId;
let depth = 0;
while (currentId && depth < maxDepth) {
const replyInfo = threadRelationships[currentId];
if (!replyInfo || !replyInfo.replyTo) break;
chain.push({
fromId: currentId,
toId: replyInfo.replyTo,
from: replyInfo.from,
to: replyInfo.to
});
currentId = replyInfo.replyTo;
depth++;
}
return chain;
}
async function getFullContext(tweetArticle, tweetId, apiKey) {
const handles = getUserHandles(tweetArticle);
const userHandle = handles.length > 0 ? handles[0] : '';
const quotedHandle = handles.length > 1 ? handles[1] : '';
const mainText = getElementText(tweetArticle.querySelector(TWEET_TEXT_SELECTOR));
let allMediaLinks = await extractMediaLinks(tweetArticle);
let quotedText = "";
let quotedMediaLinks = [];
let quotedTweetId = null;
const quoteContainer = tweetArticle.querySelector(QUOTE_CONTAINER_SELECTOR);
if (quoteContainer) {
const quotedLink = quoteContainer.querySelector('a[href*="/status/"]');
if (quotedLink) {
const href = quotedLink.getAttribute('href');
const match = href.match(/\/status\/(\d+)/);
if (match && match[1]) {
quotedTweetId = match[1];
}
}
quotedText = getElementText(quoteContainer.querySelector(TWEET_TEXT_SELECTOR)) || "";
quotedMediaLinks = await extractMediaLinks(quoteContainer);
}
const conversation = document.querySelector('div[aria-label="Timeline: Conversation"]') ||
document.querySelector('div[aria-label^="Timeline: Conversation"]');
let threadMediaUrls = [];
if (conversation && conversation.dataset.threadMapping && tweetCache.has(tweetId) && tweetCache.get(tweetId).threadContext?.threadMediaUrls) {
threadMediaUrls = tweetCache.get(tweetId).threadContext.threadMediaUrls || [];
} else if (conversation && conversation.dataset.threadMediaUrls) {
try {
const allMediaUrls = JSON.parse(conversation.dataset.threadMediaUrls);
threadMediaUrls = Array.isArray(allMediaUrls) ? allMediaUrls : [];
} catch (e) {
}
}
let allAvailableMediaLinks = [...allMediaLinks];
let mainMediaLinks = allAvailableMediaLinks.filter(link => !quotedMediaLinks.includes(link));
let engagementStats = "";
const engagementDiv = tweetArticle.querySelector('div[role="group"][aria-label$=" views"]');
if (engagementDiv) {
engagementStats = engagementDiv.getAttribute('aria-label')?.trim() || "";
}
let fullContextWithImageDescription = `[TWEET ${tweetId}]
Author:@${userHandle}:
` + mainText;
if (mainMediaLinks.length > 0) {
if (enableImageDescriptions = browserGet('enableImageDescriptions', false)) {
let mainMediaLinksDescription = await getImageDescription(mainMediaLinks, apiKey, tweetId, userHandle);
fullContextWithImageDescription += `
[MEDIA_DESCRIPTION]:
${mainMediaLinksDescription}`;
}
fullContextWithImageDescription += `
[MEDIA_URLS]:
${mainMediaLinks.join(", ")}`;
}
if (engagementStats) {
fullContextWithImageDescription += `
[ENGAGEMENT_STATS]:
${engagementStats}`;
}
if (!isOriginalTweet(tweetArticle) && threadMediaUrls.length > 0) {
const uniqueThreadMediaUrls = threadMediaUrls.filter(url =>
!mainMediaLinks.includes(url) && !quotedMediaLinks.includes(url));
if (uniqueThreadMediaUrls.length > 0) {
fullContextWithImageDescription += `
[THREAD_MEDIA_URLS]:
${uniqueThreadMediaUrls.join(", ")}`;
}
}
if (quotedText || quotedMediaLinks.length > 0) {
fullContextWithImageDescription += `
[QUOTED_TWEET${quotedTweetId ? ' ' + quotedTweetId : ''}]:
Author:@${quotedHandle}:
${quotedText}`;
if (quotedMediaLinks.length > 0) {
if (enableImageDescriptions) {
let quotedMediaLinksDescription = await getImageDescription(quotedMediaLinks, apiKey, tweetId, userHandle);
fullContextWithImageDescription += `
[QUOTED_TWEET_MEDIA_DESCRIPTION]:
${quotedMediaLinksDescription}`;
}
fullContextWithImageDescription += `
[QUOTED_TWEET_MEDIA_URLS]:
${quotedMediaLinks.join(", ")}`;
}
}
if (document.querySelector('div[aria-label="Timeline: Conversation"]', 'div[aria-label^="Timeline: Conversation"]')) {
const replyChain = await buildReplyChain(tweetId);
let threadHistoryIncluded = false;
if (conversation && conversation.dataset.threadHist) {
if (!isOriginalTweet(tweetArticle)) {
fullContextWithImageDescription = conversation.dataset.threadHist + `
[REPLY]
` + fullContextWithImageDescription;
threadHistoryIncluded = true;
}
}
if (replyChain.length > 0 && !threadHistoryIncluded) {
let parentContexts = "";
for (let i = replyChain.length - 1; i >= 0; i--) {
const link = replyChain[i];
const parentId = link.toId;
const parentCache = tweetCache.get(parentId);
const parentContent = parentCache?.tweetContent;
if (parentContent) {
parentContexts = parentContent + "\n[REPLY]\n" + parentContexts;
} else {
parentContexts = `[CONTEXT UNAVAILABLE FOR TWEET ${parentId} @${link.to || 'unknown'}]\n[REPLY]\n` + parentContexts;
}
}
fullContextWithImageDescription = parentContexts + fullContextWithImageDescription;
}
const replyInfo = getTweetReplyInfo(tweetId);
if (replyInfo && replyInfo.replyTo && !threadHistoryIncluded && replyChain.length === 0) {
fullContextWithImageDescription = `[REPLY TO TWEET ${replyInfo.replyTo}]\n` + fullContextWithImageDescription;
}
}
tweetArticle.dataset.fullContext = fullContextWithImageDescription;
return fullContextWithImageDescription;
}
function applyFilteringToAll() {
if (!observedTargetNode) return;
const tweets = observedTargetNode.querySelectorAll(TWEET_ARTICLE_SELECTOR);
tweets.forEach(filterSingleTweet);
}
function ensureAllTweetsRated() {
if (!observedTargetNode) return;
const tweets = observedTargetNode.querySelectorAll(TWEET_ARTICLE_SELECTOR);
if (tweets.length > 0) {
tweets.forEach(tweet => {
const tweetId = getTweetID(tweet);
if (!tweetId) return;
const indicatorInstance = ScoreIndicatorRegistry.get(tweetId);
const needsProcessing = !indicatorInstance ||
!indicatorInstance.status ||
indicatorInstance.status === 'error' ||
(!isValidFinalState(indicatorInstance.status) && !isValidInterimState(indicatorInstance.status)) ||
(processedTweets.has(tweetId) && !isValidFinalState(indicatorInstance.status) && !isValidInterimState(indicatorInstance.status));
if (needsProcessing) {
if (processedTweets.has(tweetId)) {
processedTweets.delete(tweetId);
}
scheduleTweetProcessing(tweet);
} else if (indicatorInstance && !isValidInterimState(indicatorInstance.status)) {
filterSingleTweet(tweet);
}
});
}
}
async function handleThreads() {
try {
let conversation = document.querySelector('div[aria-label="Timeline: Conversation"]');
if (!conversation) {
conversation = document.querySelector('div[aria-label^="Timeline: Conversation"]');
}
if (!conversation) return;
if (threadMappingInProgress || conversation.dataset.threadHist === "pending") {
return;
}
if (conversation.dataset.threadMappedAt) {
const lastMappedTime = parseInt(conversation.dataset.threadMappedAt, 10);
if (Date.now() - lastMappedTime < 10000) {
return;
}
}
const match = location.pathname.match(/status\/(\d+)/);
const pageTweetId = match ? match[1] : null;
if (!pageTweetId) return;
let rootTweetId = pageTweetId;
while (threadRelationships[rootTweetId] && threadRelationships[rootTweetId].replyTo) {
rootTweetId = threadRelationships[rootTweetId].replyTo;
}
if (conversation.dataset.threadHist === undefined) {
threadHist = "";
const rootArticle = Array.from(conversation.querySelectorAll('article[data-testid="tweet"]'))
.find(el => getTweetID(el) === rootTweetId)
|| document.querySelector('article[data-testid="tweet"]');
if (rootArticle) {
conversation.dataset.threadHist = 'pending';
threadMappingInProgress = true;
try {
const tweetId = getTweetID(rootArticle);
if (!tweetId) {
throw new Error("Failed to get tweet ID from first article");
}
const apiKey = browserGet('openrouter-api-key', '');
const fullcxt = await getFullContext(rootArticle, tweetId, apiKey);
if (!fullcxt) {
throw new Error("Failed to get full context for root tweet");
}
threadHist = fullcxt;
conversation.dataset.threadHist = threadHist;
if (conversation.firstChild) {
conversation.firstChild.dataset.canary = "true";
}
if (!processedTweets.has(tweetId)) {
scheduleTweetProcessing(rootArticle);
}
setTimeout(() => {
mapThreadStructure(conversation, rootTweetId);
}, 10);
} catch (error) {
threadMappingInProgress = false;
delete conversation.dataset.threadHist;
}
return;
}
} else if (conversation.dataset.threadHist !== "pending" &&
conversation.firstChild &&
conversation.firstChild.dataset.canary === undefined) {
if (conversation.firstChild) {
conversation.firstChild.dataset.canary = "pending";
}
threadMappingInProgress = true;
try {
const nextArticle = document.querySelector('article[data-testid="tweet"]:has(~ div[data-testid="inline_reply_offscreen"])');
if (nextArticle) {
const tweetId = getTweetID(nextArticle);
if (!tweetId) {
throw new Error("Failed to get tweet ID from next article");
}
if (tweetCache.has(tweetId) && tweetCache.get(tweetId).tweetContent) {
threadHist = threadHist + "\n[REPLY]\n" + tweetCache.get(tweetId).tweetContent;
} else {
const apiKey = browserGet('openrouter-api-key', '');
await new Promise(resolve => setTimeout(resolve, 10));
const newContext = await getFullContext(nextArticle, tweetId, apiKey);
if (!newContext) {
throw new Error("Failed to get context for next article");
}
threadHist = threadHist + "\n[REPLY]\n" + newContext;
}
conversation.dataset.threadHist = threadHist;
}
setTimeout(() => {
mapThreadStructure(conversation, rootTweetId);
}, 500);
} catch (error) {
threadMappingInProgress = false;
if (conversation.firstChild) {
delete conversation.firstChild.dataset.canary;
}
}
} else if (!threadMappingInProgress && !conversation.dataset.threadMappingInProgress) {
threadMappingInProgress = true;
setTimeout(() => {
mapThreadStructure(conversation, rootTweetId);
}, 250);
}
} catch (error) {
threadMappingInProgress = false;
}
}
async function mapThreadStructure(conversation, localRootTweetId) {
conversation.dataset.threadMappingInProgress = "true";
conversation.dataset.threadMappedAt = Date.now().toString();
threadMappingInProgress = true;
try {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Thread mapping timed out')), 5000)
);
const mapping = async () => {
let cellDivs = Array.from(document.querySelectorAll('div[data-testid="cellInnerDiv"]'));
if (!cellDivs.length) {
delete conversation.dataset.threadMappingInProgress;
threadMappingInProgress = false;
return;
}
let tweetCells = [];
let processedCount = 0;
for (let idx = 0; idx < cellDivs.length; idx++) {
const cell = cellDivs[idx];
const article = cell.querySelector('article[data-testid="tweet"]');
if (!article) continue;
try {
let tweetId = getTweetID(article);
if (!tweetId) {
let tweetLink = article.querySelector('a[href*="/status/"]');
if (tweetLink) {
let match = tweetLink.href.match(/status\/(\d+)/);
if (match) tweetId = match[1];
}
}
if (!tweetId) continue;
const handles = getUserHandles(article);
let username = handles.length > 0 ? handles[0] : null;
if (!username) continue;
let tweetTextSpan = article.querySelector('[data-testid="tweetText"]');
let text = tweetTextSpan ? tweetTextSpan.innerText.trim().replace(/\n+/g, ' ⏎ ') : '';
let mediaLinks = await extractMediaLinks(article);
let quotedMediaLinks = [];
const quoteContainer = article.querySelector(QUOTE_CONTAINER_SELECTOR);
if (quoteContainer) {
quotedMediaLinks = await extractMediaLinks(quoteContainer);
}
let prevCell = cellDivs[idx - 1] || null;
let isReplyToRoot = false;
if (prevCell && prevCell.childElementCount === 1) {
let onlyChild = prevCell.children[0];
if (onlyChild && onlyChild.children.length === 0 && onlyChild.innerHTML.trim() === '') {
isReplyToRoot = true;
}
}
tweetCells.push({
tweetNode: article,
username,
tweetId,
text,
mediaLinks,
quotedMediaLinks,
cellIndex: idx,
isReplyToRoot,
cellDiv: cell,
index: processedCount++
});
if (!processedTweets.has(tweetId)) {
scheduleTweetProcessing(article);
}
} catch (err) {
continue;
}
}
if (tweetCells.length === 0) {
delete conversation.dataset.threadMappingInProgress;
threadMappingInProgress = false;
return;
}
for (let i = 0; i < tweetCells.length; ++i) {
let tw = tweetCells[i];
const persistentRelation = threadRelationships[tw.tweetId];
if (tw.tweetId === localRootTweetId) {
tw.replyTo = null;
tw.replyToId = null;
tw.isRoot = true;
} else if (persistentRelation && persistentRelation.replyTo) {
tw.replyTo = persistentRelation.to;
tw.replyToId = persistentRelation.replyTo;
tw.isRoot = false;
} else if (tw.isReplyToRoot) {
let root = tweetCells.find(tk => tk.tweetId === localRootTweetId);
tw.replyTo = root ? root.username : null;
tw.replyToId = root ? root.tweetId : null;
tw.isRoot = false;
} else if (i > 0) {
tw.replyTo = tweetCells[i - 1].username;
tw.replyToId = tweetCells[i - 1].tweetId;
tw.isRoot = false;
} else {
tw.replyTo = null;
tw.replyToId = null;
tw.isRoot = false;
}
}
const replyDocs = tweetCells.map(tw => ({
from: tw.username,
tweetId: tw.tweetId,
to: tw.replyTo,
toId: tw.replyToId,
isRoot: tw.isRoot === true,
text: tw.text,
mediaLinks: tw.mediaLinks || [],
quotedMediaLinks: tw.quotedMediaLinks || []
}));
for (let tw of tweetCells) {
if (!tw.replyToId && !tw.isRoot && threadRelationships[tw.tweetId]?.replyTo) {
tw.replyToId = threadRelationships[tw.tweetId].replyTo;
tw.replyTo = threadRelationships[tw.tweetId].to;
const doc = replyDocs.find(d => d.tweetId === tw.tweetId);
if (doc) {
doc.toId = tw.replyToId;
doc.to = tw.replyTo;
}
}
}
conversation.dataset.threadMapping = JSON.stringify(replyDocs);
const timestamp = Date.now();
replyDocs.forEach(doc => {
if (doc.tweetId && doc.toId) {
threadRelationships[doc.tweetId] = {
replyTo: doc.toId,
from: doc.from,
to: doc.to,
isRoot: false,
timestamp
};
} else if (doc.tweetId && doc.isRoot) {
threadRelationships[doc.tweetId] = {
replyTo: null,
from: doc.from,
isRoot: true,
timestamp
};
}
});
saveThreadRelationships();
let completeThreadHistory = "";
const rootTweet = replyDocs.find(t => t.isRoot === true);
if (rootTweet && rootTweet.tweetId) {
const rootTweetElement = tweetCells.find(t => t.tweetId === rootTweet.tweetId)?.tweetNode;
if (rootTweetElement) {
try {
const apiKey = browserGet('openrouter-api-key', '');
const rootContext = await getFullContext(rootTweetElement, rootTweet.tweetId, apiKey);
if (rootContext) {
completeThreadHistory = rootContext;
conversation.dataset.threadHist = completeThreadHistory;
const allMediaUrls = [];
replyDocs.forEach(doc => {
if (doc.mediaLinks && doc.mediaLinks.length) {
allMediaUrls.push(...doc.mediaLinks);
}
if (doc.quotedMediaLinks && doc.quotedMediaLinks.length) {
allMediaUrls.push(...doc.quotedMediaLinks);
}
});
if (allMediaUrls.length > 0) {
conversation.dataset.threadMediaUrls = JSON.stringify(allMediaUrls);
}
}
} catch (error) {
}
}
}
const batchSize = 10;
for (let i = 0; i < replyDocs.length; i += batchSize) {
const batch = replyDocs.slice(i, i + batchSize);
batch.forEach(doc => {
if (doc.tweetId && tweetCache.has(doc.tweetId)) {
tweetCache.get(doc.tweetId).threadContext = {
replyTo: doc.to,
replyToId: doc.toId,
isRoot: doc.isRoot,
threadMediaUrls: doc.isRoot ? [] : getAllPreviousMediaUrls(doc.tweetId, replyDocs)
};
if (doc.tweetId && processedTweets.has(doc.tweetId)) {
const tweetCell = tweetCells.find(tc => tc.tweetId === doc.tweetId);
if (tweetCell && tweetCell.tweetNode) {
const isStreaming = tweetCell.tweetNode.dataset.ratingStatus === 'streaming' ||
(tweetCache.has(doc.tweetId) && tweetCache.get(doc.tweetId).streaming === true);
if (!isStreaming) {
processedTweets.delete(doc.tweetId);
scheduleTweetProcessing(tweetCell.tweetNode);
}
}
}
}
});
if (i + batchSize < replyDocs.length) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
delete conversation.dataset.threadMappingInProgress;
threadMappingInProgress = false;
};
function getAllPreviousMediaUrls(tweetId, replyDocs) {
const allMediaUrls = [];
const index = replyDocs.findIndex(doc => doc.tweetId === tweetId);
if (index > 0) {
for (let i = 0; i < index; i++) {
if (replyDocs[i].mediaLinks && replyDocs[i].mediaLinks.length) {
allMediaUrls.push(...replyDocs[i].mediaLinks);
}
if (replyDocs[i].quotedMediaLinks && replyDocs[i].quotedMediaLinks.length) {
allMediaUrls.push(...replyDocs[i].quotedMediaLinks);
}
}
}
return allMediaUrls;
}
await Promise.race([mapping(), timeout]);
} catch (error) {
delete conversation.dataset.threadMappedAt;
delete conversation.dataset.threadMappingInProgress;
threadMappingInProgress = false;
}
}
function getTweetReplyInfo(tweetId) {
if (threadRelationships[tweetId]) {
return threadRelationships[tweetId];
}
return null;
}
setInterval(handleThreads, THREAD_CHECK_INTERVAL);
setInterval(ensureAllTweetsRated, SWEEP_INTERVAL);
setInterval(applyFilteringToAll, SWEEP_INTERVAL);
// ----- api/api_requests.js -----
async function getCompletion(request, apiKey, timeout = 30000) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "POST",
url: "https://openrouter.ai/api/v1/chat/completions",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
"HTTP-Referer": "https://greasyfork.org/en/scripts/532459-tweetfilter-ai",
"X-Title": "TweetFilter-AI"
},
data: JSON.stringify(request),
timeout: timeout,
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
if (data.content==="") {
resolve({
error: true,
message: `No content returned${data.choices[0].native_finish_reason=="SAFETY"?" (SAFETY FILTER)":""}`,
data: data
});
}
resolve({
error: false,
message: "Request successful",
data: data
});
} catch (error) {
resolve({
error: true,
message: `Failed to parse response: ${error.message}`,
data: null
});
}
} else {
resolve({
error: true,
message: `Request failed with status ${response.status}: ${response.responseText}`,
data: null
});
}
},
onerror: function (error) {
resolve({
error: true,
message: `Request error: ${error.toString()}`,
data: null
});
},
ontimeout: function () {
resolve({
error: true,
message: `Request timed out after ${timeout}ms`,
data: null
});
}
});
});
}
function getCompletionStreaming(request, apiKey, onChunk, onComplete, onError, timeout = 90000, tweetId = null) {
const streamingRequest = {
...request,
stream: true
};
let fullResponse = "";
let content = "";
let reasoning = "";
let responseObj = null;
let streamComplete = false;
const reqObj = GM_xmlhttpRequest({
method: "POST",
url: "https://openrouter.ai/api/v1/chat/completions",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
"HTTP-Referer": "https://greasyfork.org/en/scripts/532459-tweetfilter-ai",
"X-Title": "TweetFilter-AI"
},
data: JSON.stringify(streamingRequest),
timeout: timeout,
responseType: "stream",
onloadstart: function(response) {
const reader = response.response.getReader();
const resetStreamTimeout = () => {
if (streamTimeout) clearTimeout(streamTimeout);
streamTimeout = setTimeout(() => {
if (!streamComplete) {
streamComplete = true;
onComplete({
content: content,
reasoning: reasoning,
fullResponse: fullResponse,
data: responseObj,
timedOut: true
});
}
}, 30000);
};
let streamTimeout = null;
const processStream = async () => {
try {
resetStreamTimeout()
let isDone = false;
let emptyChunksCount = 0;
while (!isDone && !streamComplete) {
const { done, value } = await reader.read();
if (done) {
isDone = true;
break;
}
const chunk = new TextDecoder().decode(value);
clearTimeout(streamTimeout);
resetStreamTimeout();
if (chunk.trim() === '') {
emptyChunksCount++;
if (emptyChunksCount >= 3) {
isDone = true;
break;
}
continue;
}
emptyChunksCount = 0;
fullResponse += chunk;
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.substring(6);
if (data === "[DONE]") {
isDone = true;
break;
}
try {
const parsed = JSON.parse(data);
responseObj = parsed;
if (parsed.choices && parsed.choices[0]) {
if (parsed.choices[0].delta && parsed.choices[0].delta.content !== undefined) {
const delta = parsed.choices[0].delta.content || "";
content += delta;
}
if (parsed.choices[0].delta && parsed.choices[0].delta.reasoning !== undefined) {
const reasoningDelta = parsed.choices[0].delta.reasoning || "";
reasoning += reasoningDelta;
}
onChunk({
chunk: parsed.choices[0].delta?.content || "",
reasoningChunk: parsed.choices[0].delta?.reasoning || "",
content: content,
reasoning: reasoning,
data: parsed
});
}
} catch (e) {
}
}
}
}
if (!streamComplete) {
streamComplete = true;
if (streamTimeout) clearTimeout(streamTimeout);
if (tweetId && window.activeStreamingRequests) {
delete window.activeStreamingRequests[tweetId];
}
onComplete({
content: content,
reasoning: reasoning,
fullResponse: fullResponse,
data: responseObj
});
}
} catch (error) {
if (streamTimeout) clearTimeout(streamTimeout);
if (!streamComplete) {
streamComplete = true;
if (tweetId && window.activeStreamingRequests) {
delete window.activeStreamingRequests[tweetId];
}
onError({
error: true,
message: `Stream processing error: ${error.toString()}`,
data: null
});
}
}
};
processStream().catch(error => {
if (streamTimeout) clearTimeout(streamTimeout);
if (!streamComplete) {
streamComplete = true;
if (tweetId && window.activeStreamingRequests) {
delete window.activeStreamingRequests[tweetId];
}
onError({
error: true,
message: `Unhandled stream error: ${error.toString()}`,
data: null
});
}
});
},
onerror: function(error) {
if (tweetId && window.activeStreamingRequests) {
delete window.activeStreamingRequests[tweetId];
}
onError({
error: true,
message: `Request error: ${error.toString()}`,
data: null
});
},
ontimeout: function() {
if (tweetId && window.activeStreamingRequests) {
delete window.activeStreamingRequests[tweetId];
}
onError({
error: true,
message: `Request timed out after ${timeout}ms`,
data: null
});
}
});
const streamingRequestObj = {
abort: function() {
streamComplete = true;
pendingRequests--;
try {
reqObj.abort();
} catch (e) {
}
if (tweetId && window.activeStreamingRequests) {
delete window.activeStreamingRequests[tweetId];
}
if (tweetId && tweetCache.has(tweetId)) {
const entry = tweetCache.get(tweetId);
if (entry.streaming && (entry.score === undefined || entry.score === null)) {
tweetCache.delete(tweetId);
}
}
}
};
if (tweetId && window.activeStreamingRequests) {
window.activeStreamingRequests[tweetId] = streamingRequestObj;
}
return streamingRequestObj;
}
function fetchAvailableModels() {
const apiKey = browserGet('openrouter-api-key', '');
if (!apiKey) {
showStatus('Please enter your OpenRouter API key');
return;
}
showStatus('Fetching available models...');
const sortOrder = browserGet('modelSortOrder', 'throughput-high-to-low');
GM_xmlhttpRequest({
method: "GET",
url: `https://openrouter.ai/api/frontend/models/find?order=${sortOrder}`,
headers: {
"Authorization": `Bearer ${apiKey}`,
"HTTP-Referer": "https://greasyfork.org/en/scripts/532182-twitter-x-ai-tweet-filter",
"X-Title": "Tweet Rating Tool"
},
onload: function (response) {
try {
const data = JSON.parse(response.responseText);
if (data.data && data.data.models) {
let filteredModels = data.data.models.filter(model => model.endpoint && model.endpoint !== null);
filteredModels.forEach(model => {
const endpointPricing = model.endpoint?.pricing;
const isFree = !endpointPricing || (
(endpointPricing.completion == null || parseFloat(endpointPricing.completion) === 0) &&
(endpointPricing.prompt == null || parseFloat(endpointPricing.prompt) === 0)
);
if (isFree && model.slug && !model.slug.endsWith(':free')) {
model.slug += ':free';
}
});
if (sortOrder === 'latency-low-to-high'|| sortOrder === 'pricing-low-to-high') {
filteredModels.reverse();
}
availableModels = filteredModels || [];
listedModels = [...availableModels];
refreshModelsUI();
showStatus('Models updated!');
}
} catch (error) {
showStatus('Error parsing models list');
}
},
onerror: function (error) {
showStatus('Error fetching models!');
}
});
}
async function getImageDescription(urls, apiKey, tweetId, userHandle) {
if (!urls?.length || !enableImageDescriptions) {
return !enableImageDescriptions ? '[Image descriptions disabled]' : '';
}
let descriptions = [];
for (const url of urls) {
const request = {
model: selectedImageModel,
messages: [{
role: "user",
content: [
{
type: "text",
text: "Describe what you see in this image in a concise way, focusing on the main elements and any text visible. Keep the description under 100 words."
},
{
type: "image_url",
image_url: { url }
}
]
}],
temperature: imageModelTemperature,
top_p: imageModelTopP,
max_tokens: maxTokens,
};
if (selectedImageModel.includes('gemini')) {
request.config = {
safetySettings: safetySettings,
}
}
if (providerSort) {
request.provider = {
sort: providerSort,
allow_fallbacks: true
};
}
const result = await getCompletion(request, apiKey);
if (!result.error && result.data?.choices?.[0]?.message?.content) {
descriptions.push(result.data.choices[0].message.content);
} else {
descriptions.push('[Error getting image description]');
}
}
return descriptions.map((desc, i) => `[IMAGE ${i + 1}]: ${desc}`).join('\n');
}
async function getGenerationMetadata(generationId, apiKey, timeout = 10000) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://openrouter.ai/api/v1/generation?id=${generationId}`,
headers: {
"Authorization": `Bearer ${apiKey}`,
"HTTP-Referer": "https://greasyfork.org/en/scripts/532459-tweetfilter-ai", // Use your script's URL
"X-Title": "TweetFilter-AI" // Replace with your script's name
},
timeout: timeout,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
resolve({
error: false,
message: "Metadata fetched successfully",
data: data // The structure is { data: { ...metadata... } }
});
} catch (error) {
resolve({
error: true,
message: `Failed to parse metadata response: ${error.message}`,
data: null
});
}
} else if (response.status === 404) {
resolve({
error: true,
status: 404, // Indicate not found specifically for retry logic
message: `Generation metadata not found (404): ${response.responseText}`,
data: null
});
} else {
resolve({
error: true,
status: response.status,
message: `Metadata request failed with status ${response.status}: ${response.responseText}`,
data: null
});
}
},
onerror: function(error) {
resolve({
error: true,
message: `Metadata request error: ${error.toString()}`,
data: null
});
},
ontimeout: function() {
resolve({
error: true,
message: `Metadata request timed out after ${timeout}ms`,
data: null
});
}
});
});
}
// ----- api/api.js -----
const safetySettings = [
{
category: "HARM_CATEGORY_HARASSMENT",
threshold: "BLOCK_NONE",
},
{
category: "HARM_CATEGORY_HATE_SPEECH",
threshold: "BLOCK_NONE",
},
{
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
threshold: "BLOCK_NONE",
},
{
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
threshold: "BLOCK_NONE",
},
{
category: "HARM_CATEGORY_CIVIC_INTEGRITY",
threshold: "BLOCK_NONE",
},
];
function extractFollowUpQuestions(content) {
if (!content) return [];
const questions = [];
const q1Marker = "Q_1.";
const q2Marker = "Q_2.";
const q3Marker = "Q_3.";
const q1Start = content.indexOf(q1Marker);
const q2Start = content.indexOf(q2Marker);
const q3Start = content.indexOf(q3Marker);
if (q1Start !== -1 && q2Start > q1Start && q3Start > q2Start) {
const q1Text = content.substring(q1Start + q1Marker.length, q2Start).trim();
questions.push(q1Text);
const q2Text = content.substring(q2Start + q2Marker.length, q3Start).trim();
questions.push(q2Text);
let q3Text = content.substring(q3Start + q3Marker.length).trim();
const endMarker = "";
if (q3Text.endsWith(endMarker)) {
q3Text = q3Text.substring(0, q3Text.length - endMarker.length).trim();
}
questions.push(q3Text);
if (questions.every(q => q.length > 0)) {
return questions;
}
}
return [];
}
async function rateTweetWithOpenRouter(tweetText, tweetId, apiKey, mediaUrls, maxRetries = 3, tweetArticle = null, authorHandle="") {
const cleanupRequest = () => {
pendingRequests = Math.max(0, pendingRequests - 1);
showStatus(`Rating tweet... (${pendingRequests} pending)`);
};
const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
if (!indicatorInstance) {
return {
score: 5, // Default error score
content: "Failed to initialize UI components for rating.",
reasoning: "",
questions: [],
lastAnswer: "",
error: true,
cached: false,
data: null,
qaConversationHistory: [] // Empty history
};
}
if (adAuthorCache.has(authorHandle)) {
indicatorInstance.updateInitialReviewAndBuildHistory({
fullContext: tweetText, // or a specific ad message
mediaUrls: [],
apiResponseContent: "