Fix: Improve AI Studio message sending robustness and handle tab updates

This commit addresses multiple issues affecting message delivery
to AI Studio:

1.  Resolved a ReferenceError in `AIStudioProvider.sendChatMessage`
    where `sendButton` was used before definition, preventing message
    submission. The logging has been corrected.

2.  Implemented logic in `background.js` to detect if an active
    chat request to AI Studio is interrupted by a tab update
    (e.g., navigation or reload). If an interruption is suspected,
    the command is re-queued and re-sent to the content script
    after the tab and debugger have re-initialized. This aims to
    prevent requests from being lost due to page refreshes.

3.  Ensured `currentRequestTargetTabId` is correctly managed and
    nulled when request processing completes or fails, supporting
    the resend logic.

These changes significantly improve the reliability of sending
messages to AI Studio, particularly in dynamic page environments.
This commit is contained in:
MasuRii マス リ 2025-06-03 12:00:29 +08:00
parent 0cd4b7833e
commit fc7b28a737
3 changed files with 1179 additions and 1402 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,84 +1,45 @@
/*
* Chat Relay: Relay for AI Chat Interfaces
* Copyright (C) 2025 Jamison Moore
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
// AI Chat Relay - AI Studio Provider
class AIStudioProvider {
constructor() {
// --- START OF CONFIGURABLE PROPERTIES ---
// Method for response capture: "debugger" or "dom"
this.captureMethod = "debugger";
// URL pattern for debugger to intercept if captureMethod is "debugger". Ensure this is specific.
this.debuggerUrlPattern = "*MakerSuiteService/GenerateContent*"; // VERIFY THIS PATTERN
// Whether to include "thinking" process in the message or just the final answer.
// If true, parseDebuggerResponse returns a JSON string: { "thinking": "...", "answer": "..." }
// If false, parseDebuggerResponse returns a string: "answer"
this.debuggerUrlPattern = "*MakerSuiteService/GenerateContent*";
this.includeThinkingInMessage = false;
// Option to enable AI Studio function calling on load
// ENABLE_AISTUDIO_FUNCTION_CALLING: true or false
this.ENABLE_AISTUDIO_FUNCTION_CALLING = true;
// --- END OF CONFIGURABLE PROPERTIES ---
this.name = "AIStudioProvider"; // Updated name
this.name = "AIStudioProvider";
this.supportedDomains = ["aistudio.google.com"];
// Selectors for the AI Studio interface
this.inputSelector = 'textarea.textarea, textarea.gmat-body-medium, textarea[aria-label="Type something or pick one from prompt gallery"]';
// The send button selector
this.sendButtonSelector = 'button.run-button, button[aria-label="Run"], button.mat-mdc-tooltip-trigger.run-button';
// Updated response selectors based on the actual elements
this.responseSelector = '.response-container, .response-text, .model-response, .model-response-container, ms-chat-turn, ms-prompt-chunk, ms-text-chunk, .very-large-text-container, .cmark-node';
// Thinking indicator selector
this.thinkingIndicatorSelector = '.thinking-indicator, .loading-indicator, .typing-indicator, .response-loading, loading-indicator';
// Fallback selectors
this.responseSelectorForDOMFallback = '.response-container, .model-response-text'; // Placeholder, adjust as needed
this.thinkingIndicatorSelectorForDOM = '.thinking-indicator, .spinner'; // Placeholder, adjust as needed
// Last sent message to avoid capturing it as a response
this.responseSelectorForDOMFallback = '.response-container, .model-response-text';
this.thinkingIndicatorSelectorForDOM = '.thinking-indicator, .spinner';
this.lastSentMessage = '';
// Initialize pendingResponseCallbacks
this.pendingResponseCallbacks = new Map();
// Call the method to ensure function calling is enabled on initial load
this.ensureFunctionCallingEnabled();
// Listen for SPA navigation events to re-trigger the check
if (window.navigation) {
window.navigation.addEventListener('navigate', (event) => {
// We are interested in same-document navigations, common in SPAs
if (!event.canIntercept || event.hashChange || event.downloadRequest !== null) {
return;
}
// Check if the navigation is within the same origin and path structure of AI Studio
const currentUrl = new URL(window.location.href);
const destinationUrl = new URL(event.destination.url);
if (currentUrl.origin === destinationUrl.origin && destinationUrl.pathname.startsWith("/prompts/")) {
console.log(`[${this.name}] Detected SPA navigation to: ${event.destination.url}. Re-checking function calling toggle.`);
// Use a timeout to allow the new view's DOM to settle
setTimeout(() => {
this.ensureFunctionCallingEnabled();
}, 1000); // Delay to allow DOM update
}, 1000);
}
});
} else {
@ -92,18 +53,17 @@ class AIStudioProvider {
return;
}
const checkInterval = 500; // ms
const maxDuration = 7000; // ms
const checkInterval = 500;
const maxDuration = 7000;
let elapsedTime = 0;
const providerName = this.name;
// Clear any existing timer for this specific functionality to avoid multiple polling loops
if (this.functionCallingPollTimer) {
clearTimeout(this.functionCallingPollTimer);
this.functionCallingPollTimer = null;
console.log(`[${providerName}] Cleared previous function calling poll timer.`);
clearTimeout(this.functionCallingPollTimer);
this.functionCallingPollTimer = null;
console.log(`[${providerName}] Cleared previous function calling poll timer.`);
}
console.log(`[${providerName}] Ensuring function calling is enabled (polling up to ${maxDuration / 1000}s).`);
const tryEnableFunctionCalling = () => {
@ -115,7 +75,6 @@ class AIStudioProvider {
if (!isChecked) {
console.log(`[${providerName}] Function calling toggle found and is NOT checked. Attempting to enable...`);
functionCallingToggle.click();
// Verify after a short delay if the click was successful
setTimeout(() => {
const stillChecked = functionCallingToggle.getAttribute('aria-checked') === 'true';
if (stillChecked) {
@ -127,7 +86,7 @@ class AIStudioProvider {
} else {
console.log(`[${providerName}] Function calling toggle found and is already enabled.`);
}
this.functionCallingPollTimer = null; // Clear timer once action is taken or element found
this.functionCallingPollTimer = null;
} else {
elapsedTime += checkInterval;
if (elapsedTime < maxDuration) {
@ -135,35 +94,29 @@ class AIStudioProvider {
this.functionCallingPollTimer = setTimeout(tryEnableFunctionCalling, checkInterval);
} else {
console.warn(`[${providerName}] Function calling toggle button (selector: 'button[aria-label="Function calling"]') not found after ${maxDuration}ms. It might not be available on this page/view or selector is incorrect.`);
this.functionCallingPollTimer = null; // Clear timer
this.functionCallingPollTimer = null;
}
}
};
// Start the first attempt after a brief initial delay
this.functionCallingPollTimer = setTimeout(tryEnableFunctionCalling, 500);
}
// Send a message to the chat interface
async sendChatMessage(messageContent) {
console.log(`[${this.name}] sendChatMessage called with content type:`, typeof messageContent, Array.isArray(messageContent) ? `Array length: ${messageContent.length}` : '');
async sendChatMessage(messageContent, requestId) {
console.log(`[${this.name} REQ-${requestId}] sendChatMessage called with content type:`, typeof messageContent, Array.isArray(messageContent) ? `Array length: ${messageContent.length}` : '');
const inputField = document.querySelector(this.inputSelector);
const sendButton = document.querySelector(this.sendButtonSelector);
if (!inputField || !sendButton) {
console.error(`[${this.name}] Missing input field or send button. Input: ${this.inputSelector}, Button: ${this.sendButtonSelector}`);
if (!inputField) {
console.error(`[${this.name} REQ-${requestId}] Missing input field. Input selector: ${this.inputSelector}`);
return false;
}
console.log(`[${this.name}] Attempting to send message to AI Studio with:`, {
inputField: inputField.className,
sendButton: sendButton.getAttribute('aria-label') || sendButton.className
});
// Deferred sendButton logging moved to after it's defined
try {
let textToInput = "";
let blobToPaste = null;
let blobMimeType = "image/png"; // Default MIME type
let blobMimeType = "image/png";
if (typeof messageContent === 'string') {
textToInput = messageContent;
@ -181,7 +134,7 @@ class AIStudioProvider {
textToInput += (textToInput ? "\n" : "") + part.text;
console.log(`[${this.name}] Added text part:`, part.text.substring(0, 50) + "...");
} else if (part.type === "image_url" && part.image_url && typeof part.image_url.url === 'string') {
if (!blobToPaste) { // Prioritize the first image found
if (!blobToPaste) {
try {
const response = await fetch(part.image_url.url);
blobToPaste = await response.blob();
@ -195,25 +148,20 @@ class AIStudioProvider {
}
}
}
this.lastSentMessage = `Array content (Text: "${textToInput.substring(0,50)}...", Image: ${blobToPaste ? 'Yes' : 'No'})`;
this.lastSentMessage = `Array content (Text: "${textToInput.substring(0, 50)}...", Image: ${blobToPaste ? 'Yes' : 'No'})`;
} else {
console.error(`[${this.name}] Unhandled message content type: ${typeof messageContent}. Cannot send.`);
this.lastSentMessage = `Unhandled data type: ${typeof messageContent}`;
return false;
}
// Set text input if any
if (textToInput) {
inputField.value = textToInput;
inputField.dispatchEvent(new Event('input', { bubbles: true }));
console.log(`[${this.name}] Set input field value with accumulated text.`);
} else {
// If there's no text but an image, ensure the input field is clear if AI Studio requires it
// inputField.value = "";
// inputField.dispatchEvent(new Event('input', { bubbles: true }));
}
// Paste blob if any
if (blobToPaste) {
const dataTransfer = new DataTransfer();
const file = new File([blobToPaste], "pasted_image." + (blobMimeType.split('/')[1] || 'png'), { type: blobMimeType });
@ -226,50 +174,65 @@ class AIStudioProvider {
inputField.dispatchEvent(pasteEvent);
console.log(`[${this.name}] Dispatched paste event with Blob data.`);
}
inputField.focus();
await new Promise(resolve => setTimeout(resolve, 100));
let attempts = 0;
const maxAttempts = 60; // Try up to 60 times (5 minutes total)
const retryDelay = 5000; // 5 seconds delay between attempts
const maxAttempts = 60;
const retryDelay = 5000;
while (attempts < maxAttempts) {
const sendButton = document.querySelector(this.sendButtonSelector);
if (attempts === 0) {
console.log(`[${this.name} REQ-${requestId}] Initial attempt to send. Input field class: ${inputField.className}. Send button details:`,
sendButton ? {
ariaLabel: sendButton.getAttribute('aria-label'),
className: sendButton.className,
selector: this.sendButtonSelector
} : "NOT FOUND");
}
if (!sendButton) {
console.warn(`[${this.name} REQ-${requestId}] Send button not found (attempt ${attempts + 1}). Will retry.`);
attempts++;
if (attempts >= maxAttempts) {
console.error(`[${this.name} REQ-${requestId}] Send button not found after ${maxAttempts} attempts.`);
return false;
}
await new Promise(resolve => setTimeout(resolve, retryDelay));
continue;
}
const isDisabled = sendButton.disabled ||
sendButton.getAttribute('aria-disabled') === 'true' ||
sendButton.classList.contains('disabled');
sendButton.getAttribute('aria-disabled') === 'true' ||
sendButton.classList.contains('disabled');
if (!isDisabled) {
// Removed check for input field content matching lastSentMessage
// as it can cause issues when there are multiple messages waiting to be sent
console.log(`[${this.name}] Send button is enabled. Clicking send button (attempt ${attempts + 1}).`);
console.log(`[${this.name} REQ-${requestId}] Send button is enabled. Clicking send button (attempt ${attempts + 1}).`);
sendButton.click();
return true; // Successfully clicked
return true;
}
attempts++;
if (attempts >= maxAttempts) {
console.error(`[${this.name}] Send button remained disabled after ${maxAttempts} attempts. Failed to send message.`);
return false; // Failed to send
return false;
}
console.log(`[${this.name}] Send button is disabled (attempt ${attempts}). Trying to enable and will retry in ${retryDelay}ms.`);
// Attempt to trigger UI updates that might enable the button
inputField.dispatchEvent(new Event('input', { bubbles: true })); // Re-dispatch input
inputField.dispatchEvent(new Event('input', { bubbles: true }));
inputField.dispatchEvent(new Event('change', { bubbles: true }));
inputField.dispatchEvent(new Event('blur', { bubbles: true }));
// Focusing and bluring input sometimes helps enable send buttons
inputField.focus();
await new Promise(resolve => setTimeout(resolve, 50)); // Short delay for focus
await new Promise(resolve => setTimeout(resolve, 50));
inputField.blur();
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
// Should not be reached if logic is correct, but as a fallback:
console.error(`[${this.name}] Exited send button check loop unexpectedly.`);
return false;
} catch (error) {
console.error(`[${this.name}] Error sending message to AI Studio:`, error);
console.error(`[${this.name} REQ-${requestId}] Error sending message to AI Studio:`, error);
return false;
}
}
@ -282,16 +245,16 @@ class AIStudioProvider {
} else if (this.captureMethod === "dom") {
console.log(`[${this.name}] Starting DOM monitoring for requestId: ${requestId}`);
this.pendingResponseCallbacks.set(requestId, responseCallback);
this._stopDOMMonitoring();
this._startDOMMonitoring(requestId);
this._stopDOMMonitoring();
this._startDOMMonitoring(requestId);
} else {
console.error(`[${this.name}] Unknown capture method: ${this.captureMethod}`);
responseCallback(requestId, `[Error: Unknown capture method '${this.captureMethod}' in provider]`, true);
this.pendingResponseCallbacks.delete(requestId);
responseCallback(requestId, `[Error: Unknown capture method '${this.captureMethod}' in provider]`, true);
this.pendingResponseCallbacks.delete(requestId);
}
}
handleDebuggerData(requestId, rawData, isFinalFromBackground) { // Renamed isFinal to isFinalFromBackground for clarity
handleDebuggerData(requestId, rawData, isFinalFromBackground) {
console.log(`[${this.name}] handleDebuggerData called for requestId: ${requestId}. Raw data length: ${rawData ? rawData.length : 'null'}. isFinalFromBackground: ${isFinalFromBackground}`);
const callback = this.pendingResponseCallbacks.get(requestId);
if (!callback) {
@ -303,48 +266,41 @@ class AIStudioProvider {
let contentHasInternalFinalMarker = false;
if (rawData && rawData.trim() !== "") {
const parseOutput = this.parseDebuggerResponse(rawData);
parsedText = parseOutput.text;
contentHasInternalFinalMarker = parseOutput.isFinalResponse; // Use the parser's determination
console.log(`[${this.name}] Debugger data parsed for requestId: ${requestId}. Parsed text (first 100 chars): '${(parsedText || "").substring(0,100)}', Type: ${typeof parsedText}, ChunkHasFinalMarkerFromParser: ${contentHasInternalFinalMarker}`);
const parseOutput = this.parseDebuggerResponse(rawData);
parsedText = parseOutput.text;
contentHasInternalFinalMarker = parseOutput.isFinalResponse;
console.log(`[${this.name}] Debugger data parsed for requestId: ${requestId}. Parsed text (first 100 chars): '${(parsedText || "").substring(0, 100)}', Type: ${typeof parsedText}, ChunkHasFinalMarkerFromParser: ${contentHasInternalFinalMarker}`);
} else {
console.log(`[${this.name}] Received empty rawData from debugger for requestId: ${requestId}. isFinalFromBackground: ${isFinalFromBackground}`);
// If rawData is empty, text remains empty.
// If background says it's final, but data is empty, it's still final.
}
// The response is considered final for the callback if:
// 1. The background script explicitly states this is the final debugger event for the request OR
// 2. The provider's own parsing of the current chunk's content indicates it's the end of the AI's message.
const isFinalForCallback = isFinalFromBackground || contentHasInternalFinalMarker;
console.log(`[${this.name}] Calling callback for requestId ${requestId} with text (first 100): '${(parsedText || "").substring(0,100)}', isFinalForCallback: ${isFinalForCallback} (isFinalFromBackground: ${isFinalFromBackground}, contentHasInternalFinalMarker: ${contentHasInternalFinalMarker})`);
console.log(`[${this.name}] Calling callback for requestId ${requestId} with text (first 100): '${(parsedText || "").substring(0, 100)}', isFinalForCallback: ${isFinalForCallback} (isFinalFromBackground: ${isFinalFromBackground}, contentHasInternalFinalMarker: ${contentHasInternalFinalMarker})`);
callback(requestId, parsedText, isFinalForCallback);
// If the callback was told this is the final response, then clean up.
if (isFinalForCallback) {
console.log(`[${this.name}] Final event processed for requestId: ${requestId} (isFinalForCallback was true). Removing callback.`);
this.pendingResponseCallbacks.delete(requestId);
}
}
// --- Internal DOM Capture Logic (largely unchanged but kept for completeness) ---
_captureResponseDOM(element = null) {
_captureResponseDOM(element = null) {
console.log(`[${this.name}] _captureResponseDOM (DOM method) called with element:`, element);
if (!element && this.captureMethod === "dom") {
const elements = document.querySelectorAll(this.responseSelector);
if (elements.length > 0) {
element = elements[elements.length - 1];
console.log(`[${this.name}] _captureResponseDOM: Found element via querySelector during polling.`);
}
if (!element && this.captureMethod === "dom") {
const elements = document.querySelectorAll(this.responseSelector);
if (elements.length > 0) {
element = elements[elements.length - 1];
console.log(`[${this.name}] _captureResponseDOM: Found element via querySelector during polling.`);
}
}
if (!element) {
console.log(`[${this.name}] _captureResponseDOM: No element provided or found.`);
return { found: false, text: '' };
return { found: false, text: '' };
}
if (this._isResponseStillGeneratingDOM()) {
if (this._isResponseStillGeneratingDOM()) {
console.log(`[${this.name}] Response is still being generated (_isResponseStillGeneratingDOM check), waiting for completion`);
return { found: false, text: '' };
return { found: false, text: '' };
}
console.log(`[${this.name}] Attempting to capture DOM response from AI Studio...`);
let responseText = "";
@ -355,10 +311,9 @@ class AIStudioProvider {
console.log("AISTUDIO: Element has text content");
responseText = element.textContent.trim();
if (responseText &&
// Removed check for responseText !== this.lastSentMessage
!responseText.includes("Loading") &&
!responseText.includes("Thinking") &&
!responseText.includes("Expand to view model thoughts")) {
!responseText.includes("Loading") &&
!responseText.includes("Thinking") &&
!responseText.includes("Expand to view model thoughts")) {
console.log("AISTUDIO: Found response in element:", responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
foundResponse = true;
} else {
@ -384,9 +339,8 @@ class AIStudioProvider {
if (responseSpan) {
console.log("AISTUDIO: Found response span in last ms-text-chunk");
const text = responseSpan.textContent.trim();
if (text &&
// Removed check for text !== this.lastSentMessage
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
if (text &&
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
responseText = text;
console.log("AISTUDIO: Found response in span:", responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
foundResponse = true;
@ -394,9 +348,8 @@ class AIStudioProvider {
} else {
console.log("AISTUDIO: No response span found, getting text directly from ms-text-chunk");
const text = lastTextChunk.textContent.trim();
if (text &&
// Removed check for text !== this.lastSentMessage
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
if (text &&
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
responseText = text;
console.log("AISTUDIO: Found response in ms-text-chunk:", responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
foundResponse = true;
@ -412,9 +365,8 @@ class AIStudioProvider {
const isInThoughtChunk = p.closest('ms-thought-chunk');
if (!isInThoughtChunk) {
const text = p.textContent.trim();
if (text &&
// Removed check for text !== this.lastSentMessage
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
if (text &&
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
combinedText += text + "\n";
}
}
@ -438,9 +390,8 @@ class AIStudioProvider {
let combinedTextFallback = "";
paragraphsFallback.forEach((p) => {
const text = p.textContent.trim();
if (text &&
// Removed check for text !== this.lastSentMessage
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
if (text &&
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
combinedTextFallback += text + "\n";
}
});
@ -451,9 +402,8 @@ class AIStudioProvider {
}
if (!foundResponse) {
const textFallback = lastChatTurnFallback.textContent.trim();
if (textFallback &&
// Removed check for textFallback !== this.lastSentMessage
!textFallback.includes("Loading") && !textFallback.includes("Thinking") && !textFallback.includes("Expand to view model thoughts")) {
if (textFallback &&
!textFallback.includes("Loading") && !textFallback.includes("Thinking") && !textFallback.includes("Expand to view model thoughts")) {
responseText = textFallback;
foundResponse = true;
}
@ -467,9 +417,8 @@ class AIStudioProvider {
for (let i = textContainers.length - 1; i >= 0; i--) {
const textContainer = textContainers[i];
const text = textContainer.textContent.trim();
if (text &&
// Removed check for text !== this.lastSentMessage
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
if (text &&
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
responseText = text;
foundResponse = true;
break;
@ -487,9 +436,8 @@ class AIStudioProvider {
const isUserChunk = paragraph.closest('.user-chunk');
if (isUserChunk) continue;
const text = paragraph.textContent.trim();
if (text &&
// Removed check for text !== this.lastSentMessage
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
if (text &&
!text.includes("Loading") && !text.includes("Thinking") && !text.includes("Expand to view model thoughts")) {
combinedTextDoc = text + "\n" + combinedTextDoc;
if (text.startsWith("Hello") || text.includes("I'm doing") || text.includes("How can I assist")) break;
}
@ -513,126 +461,123 @@ class AIStudioProvider {
.replace(/\n{3,}/g, '\n\n')
.trim();
}
return {
return {
found: foundResponse && !!responseText.trim(),
text: responseText
};
}
// --- START OF CORRECTED DEBUGGER PARSING LOGIC ---
parseDebuggerResponse(jsonString) {
console.log(`[${this.name}] Parsing debugger response (AI Studio specific)... Input jsonString (first 200):`, jsonString ? jsonString.substring(0,200) : "null", "Type:", typeof jsonString);
console.log(`[${this.name}] Parsing debugger response (AI Studio specific)... Input jsonString (first 200):`, jsonString ? jsonString.substring(0, 200) : "null", "Type:", typeof jsonString);
if (!jsonString || jsonString.trim() === "") {
console.warn(`[${this.name}] parseDebuggerResponse called with empty or null jsonString.`);
return { text: "", isFinalResponse: false };
console.warn(`[${this.name}] parseDebuggerResponse called with empty or null jsonString.`);
return { text: "", isFinalResponse: false };
}
let thinkingAndProcessText = "";
let actualResponseText = "";
let overallMarkerFound = false;
let overallMarkerFound = false;
function findEndOfUnitMarker(data) {
if (Array.isArray(data)) {
if (data.length >= 2 && data[data.length - 1] === 1 && data[data.length - 2] === "model") {
return true;
}
for (const item of data) {
if (findEndOfUnitMarker(item)) {
return true;
}
}
if (Array.isArray(data)) {
if (data.length >= 2 && data[data.length - 1] === 1 && data[data.length - 2] === "model") {
return true;
}
return false;
for (const item of data) {
if (findEndOfUnitMarker(item)) {
return true;
}
}
}
return false;
}
function extractTextSegments(data, segments = []) {
if (Array.isArray(data)) {
if (data.length > 1 && data[0] === null && typeof data[1] === 'string') {
segments.push(data[1]);
} else {
for (const item of data) {
extractTextSegments(item, segments);
}
}
if (Array.isArray(data)) {
if (data.length > 1 && data[0] === null && typeof data[1] === 'string') {
segments.push(data[1]);
} else {
for (const item of data) {
extractTextSegments(item, segments);
}
}
return segments;
}
return segments;
}
try {
const parsedJson = JSON.parse(jsonString);
if (Array.isArray(parsedJson)) {
for (let i = 0; i < parsedJson.length; i++) {
const chunk = parsedJson[i];
const textSegmentsInChunk = extractTextSegments(chunk);
if (textSegmentsInChunk.length > 0) {
actualResponseText += textSegmentsInChunk.join("");
}
if (findEndOfUnitMarker(chunk)) {
overallMarkerFound = true;
}
if (this.includeThinkingInMessage) {
if (Array.isArray(chunk) && chunk[0] && Array.isArray(chunk[0][0]) && chunk[0][0][2]) {
const potentialThinkingBlock = chunk[0][0][2];
const thinkingSegments = extractTextSegments(potentialThinkingBlock);
const thinkingBlockText = thinkingSegments.join("").trim();
if (thinkingBlockText && !actualResponseText.includes(thinkingBlockText)) {
thinkingAndProcessText += thinkingBlockText + "\n";
}
}
}
}
} else {
if (typeof parsedJson === 'string') {
actualResponseText = parsedJson;
overallMarkerFound = true;
} else {
console.warn(`[${this.name}] Parsed JSON is not an array as expected. Type: ${typeof parsedJson}. Content (first 100): ${JSON.stringify(parsedJson).substring(0,100)}`);
const genericText = extractTextSegments(parsedJson).join("");
if (genericText) {
actualResponseText = genericText;
overallMarkerFound = true;
} else {
actualResponseText = "[Error: Unexpected JSON structure from AI Studio]";
overallMarkerFound = true;
}
const parsedJson = JSON.parse(jsonString);
if (Array.isArray(parsedJson)) {
for (let i = 0; i < parsedJson.length; i++) {
const chunk = parsedJson[i];
const textSegmentsInChunk = extractTextSegments(chunk);
if (textSegmentsInChunk.length > 0) {
actualResponseText += textSegmentsInChunk.join("");
}
if (findEndOfUnitMarker(chunk)) {
overallMarkerFound = true;
}
if (this.includeThinkingInMessage) {
if (Array.isArray(chunk) && chunk[0] && Array.isArray(chunk[0][0]) && chunk[0][0][2]) {
const potentialThinkingBlock = chunk[0][0][2];
const thinkingSegments = extractTextSegments(potentialThinkingBlock);
const thinkingBlockText = thinkingSegments.join("").trim();
if (thinkingBlockText && !actualResponseText.includes(thinkingBlockText)) {
thinkingAndProcessText += thinkingBlockText + "\n";
}
}
}
}
actualResponseText = actualResponseText.replace(/\\n/g, "\n").replace(/\n\s*\n/g, '\n').trim();
thinkingAndProcessText = thinkingAndProcessText.replace(/\\n/g, "\n").replace(/\n\s*\n/g, '\n').trim();
} else {
if (typeof parsedJson === 'string') {
actualResponseText = parsedJson;
overallMarkerFound = true;
} else {
console.warn(`[${this.name}] Parsed JSON is not an array as expected. Type: ${typeof parsedJson}. Content (first 100): ${JSON.stringify(parsedJson).substring(0, 100)}`);
const genericText = extractTextSegments(parsedJson).join("");
if (genericText) {
actualResponseText = genericText;
overallMarkerFound = true;
} else {
actualResponseText = "[Error: Unexpected JSON structure from AI Studio]";
overallMarkerFound = true;
}
}
}
actualResponseText = actualResponseText.replace(/\\n/g, "\n").replace(/\n\s*\n/g, '\n').trim();
thinkingAndProcessText = thinkingAndProcessText.replace(/\\n/g, "\n").replace(/\n\s*\n/g, '\n').trim();
} catch (e) {
console.error(`[${this.name}] Error parsing AI Studio debugger response JSON:`, e, "Original string (first 200 chars):", jsonString.substring(0, 200));
const formattedFallback = this.formatOutput("", jsonString);
return { text: formattedFallback, isFinalResponse: true };
console.error(`[${this.name}] Error parsing AI Studio debugger response JSON:`, e, "Original string (first 200 chars):", jsonString.substring(0, 200));
const formattedFallback = this.formatOutput("", jsonString);
return { text: formattedFallback, isFinalResponse: true };
}
const formattedOutput = this.formatOutput(thinkingAndProcessText, actualResponseText);
if (formattedOutput.trim() === "" && overallMarkerFound) {
return { text: "", isFinalResponse: true };
return { text: "", isFinalResponse: true };
}
return { text: formattedOutput, isFinalResponse: overallMarkerFound };
}
formatOutput(thinkingText, answerText) {
if (this.includeThinkingInMessage && thinkingText && thinkingText.trim() !== "") {
try {
const result = {
thinking: thinkingText.trim(),
answer: (answerText || "").trim()
};
return JSON.stringify(result);
} catch (e) {
console.error(`[${this.name}] Error stringifying thinking/answer object:`, e);
return (answerText || "").trim();
}
try {
const result = {
thinking: thinkingText.trim(),
answer: (answerText || "").trim()
};
return JSON.stringify(result);
} catch (e) {
console.error(`[${this.name}] Error stringifying thinking/answer object:`, e);
return (answerText || "").trim();
}
}
return (answerText || "").trim();
return (answerText || "").trim();
}
// --- END OF CORRECTED DEBUGGER PARSING LOGIC ---
// --- Other methods (DOM fallback, etc. - largely unchanged but included for completeness) ---
_findResponseElementDOM(container) {
console.log(`[${this.name}] _findResponseElementDOM called on container:`, container);
if (!container) return null;
@ -641,7 +586,6 @@ class AIStudioProvider {
if (elements.length > 0) {
const lastElement = elements[elements.length - 1];
console.log(`[${this.name}] Found last response element via DOM:`, lastElement);
// Add checks to ensure it's not the user's input or an old response
if (lastElement.textContent && lastElement.textContent.trim() !== this.lastSentMessage) {
return lastElement;
}
@ -651,21 +595,14 @@ class AIStudioProvider {
}
shouldSkipResponseMonitoring() {
// Example: if a provider indicates via a specific property or method
// For AIStudio, if using debugger, we don't need DOM monitoring.
// This method is more for providers that might sometimes use DOM, sometimes not.
// console.log(`[${this.name}] shouldSkipResponseMonitoring called. Capture method: ${this.captureMethod}`);
return this.captureMethod === "debugger";
}
_isResponseStillGeneratingDOM() {
// This is for the DOM fallback method
const thinkingIndicator = document.querySelector(this.thinkingIndicatorSelectorForDOM);
if (thinkingIndicator) {
// console.log(`[${this.name}] DOM Fallback: Thinking indicator found.`);
return true;
}
// console.log(`[${this.name}] DOM Fallback: No thinking indicator found.`);
return false;
}
@ -681,7 +618,7 @@ class AIStudioProvider {
_startDOMMonitoring(requestId) {
console.log(`[${this.name}] DOM Fallback: _startDOMMonitoring for requestId: ${requestId}`);
this._stopDOMMonitoring(); // Stop any existing observer
this._stopDOMMonitoring();
const callback = this.pendingResponseCallbacks.get(requestId);
if (!callback) {
@ -690,24 +627,24 @@ class AIStudioProvider {
}
let attempts = 0;
const maxAttempts = 15; // Try for ~15 seconds
const maxAttempts = 15;
const interval = 1000;
this.domMonitorTimer = setInterval(() => {
console.log(`[${this.name}] DOM Fallback: Polling attempt ${attempts + 1}/${maxAttempts} for requestId: ${requestId}`);
const responseData = this._captureResponseDOM(); // Will use this.responseSelectorForDOMFallback
const responseData = this._captureResponseDOM();
if (responseData.found && responseData.text.trim() !== "") {
console.log(`[${this.name}] DOM Fallback: Response captured for requestId ${requestId}. Text (first 100): ${responseData.text.substring(0,100)}`);
console.log(`[${this.name}] DOM Fallback: Response captured for requestId ${requestId}. Text (first 100): ${responseData.text.substring(0, 100)}`);
this._stopDOMMonitoring();
callback(requestId, responseData.text, true); // Assume final for DOM capture
callback(requestId, responseData.text, true);
this.pendingResponseCallbacks.delete(requestId);
} else {
attempts++;
if (attempts >= maxAttempts) {
console.warn(`[${this.name}] DOM Fallback: Max attempts reached for requestId ${requestId}. No response captured.`);
this._stopDOMMonitoring();
callback(requestId, "[Error: Timed out waiting for DOM response]", true); // Error, final
callback(requestId, "[Error: Timed out waiting for DOM response]", true);
this.pendingResponseCallbacks.delete(requestId);
}
}
@ -724,7 +661,6 @@ class AIStudioProvider {
}
}
// Ensure the provider is available on the window for the content script
if (window.providerUtils) {
const providerInstance = new AIStudioProvider();
window.providerUtils.registerProvider(