Spaces:
Running
Running
console.log("Script execution started."); | |
// --- Sidebar Functionality --- | |
function initializeSidebar() { | |
const sidebar = document.getElementById('sidebar'); | |
const sidebarToggle = document.getElementById('sidebar-toggle'); | |
const sidebarTitle = document.getElementById('sidebar-title'); | |
const folderHeaders = document.querySelectorAll('.folder-header'); | |
if (sidebarToggle) { // Ensure element exists before adding listener | |
sidebarToggle.addEventListener('click', function() { | |
sidebar.classList.toggle('sidebar-collapsed'); | |
const icon = sidebarToggle.querySelector('.material-icons-round'); | |
if (sidebar.classList.contains('sidebar-collapsed')) { | |
icon.textContent = 'chevron_right'; | |
if(sidebarTitle) sidebarTitle.style.display = 'none'; | |
document.querySelectorAll('.folder-name, .file-name').forEach(el => el.style.display = 'none'); | |
} else { | |
icon.textContent = 'chevron_left'; | |
if(sidebarTitle) sidebarTitle.style.display = 'inline'; | |
document.querySelectorAll('.folder-name, .file-name').forEach(el => el.style.display = 'inline'); | |
} | |
}); | |
} | |
folderHeaders.forEach(header => { | |
header.addEventListener('click', function() { | |
const folder = this.parentElement; | |
folder.classList.toggle('expanded'); | |
}); | |
}); | |
const files = document.querySelectorAll('.file'); | |
files.forEach(file => { | |
file.addEventListener('click', function() { | |
files.forEach(f => f.classList.remove('active')); | |
this.classList.add('active'); | |
// Add logic to load file content here if needed | |
}); | |
}); | |
} | |
// --- Module Imports --- | |
let GoogleGenerativeAI, marked, DOMPurify; | |
try { | |
const gaModule = await import('https://esm.sh/@google/generative-ai'); | |
GoogleGenerativeAI = gaModule.GoogleGenerativeAI; | |
const markedModule = await import('https://esm.sh/marked'); | |
marked = markedModule.marked; | |
const domPurifyModule = await import('https://esm.sh/dompurify'); | |
DOMPurify = domPurifyModule.default; | |
if (!GoogleGenerativeAI || !marked || !DOMPurify || typeof Chart === 'undefined' || typeof pdfjsLib === 'undefined' || typeof Babel === 'undefined') { | |
let missing = []; | |
if(!GoogleGenerativeAI) missing.push("GoogleGenerativeAI"); | |
if(!marked) missing.push("marked"); | |
if(!DOMPurify) missing.push("DOMPurify"); | |
if(typeof Chart === 'undefined') missing.push("Chart.js"); | |
if(typeof pdfjsLib === 'undefined') missing.push("pdf.js"); | |
if(typeof Babel === 'undefined') missing.push("Babel"); // Added Babel to check | |
if (missing.length > 0) throw new Error(`Critical modules failed to load: ${missing.join(', ')}.`); | |
} | |
console.log("Core & external libraries seem available."); | |
} catch (error) { | |
console.error("Fatal Error: Could not load core dependencies:", error); | |
const errorMsgEl = document.getElementById('init-error-message') || { textContent: "" }; | |
const sdkErrorOverlayEl = document.getElementById('sdk-error-overlay') || { classList: { add: () => {} }}; | |
errorMsgEl.textContent = `Failed to load critical libraries: ${error.message}. Check network, console, and ensure all script tags are correct.`; | |
sdkErrorOverlayEl.classList.add('visible'); | |
throw error; // Stop further execution | |
} | |
// --- UI Element References --- | |
const codeInputArea = document.getElementById('code-input-area'); | |
const mainInput = document.getElementById('main-input'); | |
const sendBtn = document.getElementById('send-btn'); | |
const researchBtn = document.getElementById('research-btn'); | |
const stopBtn = document.getElementById('stop-btn'); | |
const uploadBtnLabel = document.getElementById('upload-btn-label'); | |
const fileInput = document.getElementById('file-input'); | |
const fileInfoEl = document.getElementById('file-info'); | |
const fileNameEl = document.getElementById('file-name'); | |
const urlInfoEl = document.getElementById('url-info'); | |
const urlInfoTextEl = document.getElementById('url-info-text'); | |
const fetchUrlBtn = document.getElementById('fetch-url-btn'); | |
const aiOverlay = document.getElementById('ai-overlay'); | |
const aiStatusText = document.getElementById('ai-status-text'); | |
const initStatusText = document.getElementById('init-status-text'); | |
const sdkErrorOverlay = document.getElementById('sdk-error-overlay'); | |
const reloadButton = document.getElementById('reload-button'); | |
const outputFrame = document.getElementById('output-frame'); | |
const fullCodeContent = document.getElementById('full-code-content'); | |
const researchProgressEl = document.getElementById('research-progress'); | |
const languageSelector = document.getElementById('language-selector'); | |
const tabButtons = document.querySelectorAll('.tab-button'); | |
const videoStreamPanel = document.getElementById('video-stream-panel'); // Panel for screen share | |
const videoElement = document.getElementById('video-element'); // Displays screen share | |
const videoCanvas = document.getElementById('video-canvas'); // Used for capturing frame | |
const startStreamBtn = document.getElementById('start-stream-btn'); // Should be "Start Screen Share" | |
const stopStreamBtn = document.getElementById('stop-stream-btn'); // Should be "Stop Screen Share" | |
// --- Application State --- | |
let genAI; | |
let model; | |
let generationInProgress = false; | |
let stopGenerationFlag = false; | |
let generatedCode = ''; | |
let fileContentForAI = ""; | |
let pyodide; | |
let pyodideLoadingPromise = null; | |
let currentLanguage = "HTML/CSS/JS (Web Page)"; | |
window.currentAiChart = null; | |
let screenStream = null; // Changed from localStream | |
let currentScreenFrameDataUrl = ""; // To store the captured screen frame | |
const API_KEY = "AIzaSyBCURZf72ZWwMN2SHZ7XCQoNgExV4WMX8E"; // Ensure this key has access to the model | |
const MODEL_NAME = "gemini-2.0-flash-thinking-exp-1219"; // IMPORTANT: MUST be a multimodal model (e.g., gemini-pro-vision, gemini-1.5-flash) for screen analysis | |
const CORS_PROXY_URL_PREFIX = "https://api.allorigins.win/raw?url="; | |
const SUPPORTED_LANGUAGES = [ | |
"HTML/CSS/JS (Web Page)", "JavaScript (Standalone)", "TypeScript", | |
"React Component", "Next.js Page (Client-Side)", "Python (Pyodide)", | |
"Java (Code Display)", "Go (Code Display)", "Node.js (Code Display)", | |
"Flutter (Dart Code Display)", "Unity (C# Script Display)", "Data Analysis (from text/file/URL/screen)" | |
]; | |
const DATA_ANALYSIS_LANG = "Data Analysis (from text/file/URL/screen)"; | |
// --- Helper Functions --- | |
function showError(message) { | |
console.error("showError called:", message); | |
const errorMsgEl = document.getElementById('init-error-message'); | |
if (errorMsgEl) errorMsgEl.textContent = message; | |
if (sdkErrorOverlay) sdkErrorOverlay.classList.add('visible'); | |
else alert("A critical error occurred: " + message); | |
} | |
function updateButtonStates(isGenerating) { | |
sendBtn.disabled = isGenerating; | |
researchBtn.disabled = isGenerating; | |
fetchUrlBtn.disabled = isGenerating; | |
fileInput.disabled = isGenerating; | |
uploadBtnLabel.style.cursor = isGenerating ? 'not-allowed' : 'pointer'; | |
uploadBtnLabel.style.opacity = isGenerating ? 0.5 : 1; | |
startStreamBtn.disabled = isGenerating || screenStream !== null; // Uses screenStream | |
stopBtn.style.display = isGenerating ? 'block' : 'none'; | |
if (!isGenerating) stopBtn.disabled = false; | |
} | |
function showAiOverlay(visible, statusText = "Thinking...") { | |
aiStatusText.textContent = statusText; | |
aiOverlay.classList.toggle('visible', visible); | |
const isResearchingWithProgress = statusText.toLowerCase().includes("researching") && researchProgressEl.dataset.showProgress === "true"; | |
researchProgressEl.classList.toggle('hidden', !visible || !isResearchingWithProgress); | |
if(isResearchingWithProgress) researchProgressEl.classList.remove('hidden'); else researchProgressEl.classList.add('hidden'); | |
} | |
function clearInputSourceInfo() { | |
if (fileInfoEl) fileInfoEl.style.display = 'none'; | |
if (fileNameEl) fileNameEl.textContent = ''; | |
if (urlInfoEl) urlInfoEl.style.display = 'none'; | |
if (urlInfoTextEl) urlInfoTextEl.textContent = ''; | |
fileContentForAI = ""; | |
currentScreenFrameDataUrl = ""; // Clear screen frame data | |
if (fileInput) fileInput.value = null; | |
} | |
// --- Initialization --- | |
async function initializeApp() { | |
console.log("initializeApp started"); | |
try { | |
initializeSidebar(); | |
if(initStatusText) initStatusText.textContent = "Initializing UI..."; | |
setupLanguageSelector(); | |
setupEventListeners(); | |
clearInputSourceInfo(); | |
if(initStatusText) initStatusText.textContent = "Initializing Gemini..."; | |
if (!GoogleGenerativeAI) throw new Error("GoogleGenerativeAI SDK is not loaded."); | |
genAI = new GoogleGenerativeAI(API_KEY); | |
model = genAI.getGenerativeModel({ model: MODEL_NAME }); | |
if(codeInputArea) codeInputArea.disabled = false; | |
if(mainInput) mainInput.disabled = false; | |
updateButtonStates(false); | |
if(mainInput) mainInput.placeholder = `Enter task, paste URL, use screen share, or upload file...`; | |
if(codeInputArea) codeInputArea.placeholder = "Results will appear here..."; | |
if(initStatusText) initStatusText.textContent = "Gemini Ready. Loading Pyodide..."; | |
pyodideLoadingPromise = loadPyodideInstance().then(() => { | |
if(initStatusText) { | |
if (initStatusText.textContent.includes("Loading Pyodide...")) { | |
initStatusText.textContent = "Ready (Gemini, Pyodide)"; | |
} else { | |
initStatusText.textContent = initStatusText.textContent.replace("Gemini Ready.", "Gemini, Pyodide Ready."); | |
} | |
} | |
}).catch(err => { | |
console.warn("Pyodide background load failed:", err); | |
if(initStatusText) { | |
if (initStatusText.textContent.includes("Loading Pyodide...")) { | |
initStatusText.textContent = "Ready (Gemini; Pyodide failed)"; | |
} else { | |
initStatusText.textContent = initStatusText.textContent.replace("Gemini Ready.", "Ready (Gemini; Pyodide failed)."); | |
} | |
} | |
}); | |
// No Tesseract initialization needed here | |
} catch (error) { | |
console.error("Initialization error in initializeApp:", error); | |
showError(`Failed to initialize application: ${error.message}`); | |
} | |
} | |
function setupLanguageSelector() { | |
if(!languageSelector) return; | |
SUPPORTED_LANGUAGES.forEach(lang => { | |
const option = document.createElement('option'); | |
option.value = lang; option.textContent = lang; | |
languageSelector.appendChild(option); | |
}); | |
languageSelector.value = DATA_ANALYSIS_LANG; | |
currentLanguage = DATA_ANALYSIS_LANG; | |
if(mainInput) mainInput.placeholder = `Describe task for content (file, URL, screen)...`; | |
} | |
async function loadPyodideInstance() { | |
if (typeof loadPyodide === "undefined") throw new Error("Pyodide main script not loaded."); | |
console.log("Attempting to load Pyodide..."); | |
try { | |
const inst = await loadPyodide({}); await inst.loadPackage(["micropip"]); pyodide = inst; | |
console.log("Pyodide instance loaded successfully."); return pyodide; | |
} catch (error) { console.error("Pyodide loading error:", error); pyodide = null; throw error; } | |
} | |
// --- Event Listeners Setup --- | |
function setupEventListeners() { | |
console.log("Setting up event listeners."); | |
if(reloadButton) reloadButton.addEventListener('click', () => window.location.reload()); | |
if(tabButtons) { | |
tabButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
tabButtons.forEach(btn => btn.classList.remove('active')); | |
button.classList.add('active'); | |
const tabName = button.dataset.tab; | |
document.getElementById('output-tab').classList.toggle('hidden', tabName !== 'output'); | |
document.getElementById('output-tab').classList.toggle('active', tabName === 'output'); | |
document.getElementById('code-tab').classList.toggle('hidden', tabName !== 'code'); | |
document.getElementById('code-tab').classList.toggle('active', tabName === 'code'); | |
// Assuming 'video-stream-panel' is the ID for the screen share tab's content panel | |
const screenSharePanel = document.getElementById('video-stream-panel'); | |
if (screenSharePanel) { | |
screenSharePanel.classList.toggle('hidden', tabName !== 'video'); // 'video' is likely the data-tab value for screen share | |
screenSharePanel.classList.toggle('active', tabName === 'video'); | |
if (tabName === 'video' && mainInput) { | |
mainInput.placeholder = "Describe task for the screen content..."; | |
} else if (mainInput && currentLanguage === DATA_ANALYSIS_LANG) { | |
mainInput.placeholder = `Describe task for content (file, URL, screen)...`; | |
} else if (mainInput) { | |
mainInput.placeholder = `Enter task for ${currentLanguage.split('(')[0].trim()}...`; | |
} | |
} | |
}); | |
}); | |
} | |
if(fileInput) fileInput.addEventListener('change', handleFileChange); | |
if(fetchUrlBtn) fetchUrlBtn.addEventListener('click', handleFetchUrl); | |
if(languageSelector) languageSelector.addEventListener('change', handleLanguageChange); | |
if(sendBtn) sendBtn.addEventListener('click', handleSendRequest); | |
if(researchBtn) { | |
researchBtn.addEventListener('click', () => { | |
const query = mainInput.value.trim() || fileContentForAI /* No currentFrameOcrText */; | |
if (query) { | |
performResearch(query); | |
} else { | |
alert("Please enter a topic in the input field, or provide content to research."); | |
} | |
}); | |
} | |
if(mainInput) { | |
mainInput.addEventListener('keypress', (e) => { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
// Check if videoStreamPanel (screen share panel) is active for URL handling change | |
const screenSharePanelActive = videoStreamPanel && videoStreamPanel.classList.contains('active'); | |
if (isValidURL(mainInput.value.trim()) && !screenSharePanelActive) { | |
handleFetchUrl(); | |
} else { | |
handleSendRequest(); | |
} | |
} | |
}); | |
} | |
if(stopBtn) stopBtn.addEventListener('click', handleStopGeneration); | |
if(startStreamBtn) startStreamBtn.addEventListener('click', startScreenStream); | |
if(stopStreamBtn) stopStreamBtn.addEventListener('click', stopScreenStream); | |
} | |
// --- File, URL Processing (largely unchanged) --- | |
async function handleFileChange(event) { | |
const file = event.target.files[0]; if (!file) { clearInputSourceInfo(); return; } | |
clearInputSourceInfo(); | |
if(fileNameEl) fileNameEl.textContent = file.name; | |
if(fileInfoEl) fileInfoEl.style.display = 'flex'; | |
if(mainInput) mainInput.value = ""; | |
showAiOverlay(true, `Processing ${file.name}...`); | |
try { | |
let textContent = ""; const fileType = file.name.split('.').pop().toLowerCase(); | |
if (fileType === 'pdf') { textContent = await readPdfFile(file); | |
} else if (['txt', 'md', 'csv'].includes(fileType)) { textContent = await readGenericTextFile(file); | |
} else if (fileType === 'docx') { | |
if (typeof mammoth !== 'undefined') textContent = await readDocxFileWithMammoth(file); | |
else { textContent = `(mammoth.js not loaded for .docx: ${file.name})`; alert("Enable mammoth.js for .docx.");} | |
} else if (fileType === 'doc') { textContent = `(.doc not supported: ${file.name})`; alert(".doc not supported."); | |
} else { textContent = `(Unsupported type .${fileType}: ${file.name})`; alert(`Unsupported type: .${fileType}`);} | |
fileContentForAI = textContent.substring(0, 30000); | |
if(languageSelector) languageSelector.value = DATA_ANALYSIS_LANG; | |
currentLanguage = DATA_ANALYSIS_LANG; | |
if(mainInput) { | |
mainInput.placeholder = `Describe task for ${file.name}...`; mainInput.value = `File: ${file.name}\nTask: Summarize.`; mainInput.focus(); | |
} | |
} catch (error) { | |
console.error("File processing error:", error); | |
if(mainInput) mainInput.value = `Error processing ${file.name}: ${error.message}.`; | |
fileContentForAI = ""; alert(`Error: ${error.message}`); | |
} finally { showAiOverlay(false); } | |
} | |
function readGenericTextFile(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = event => resolve(event.target.result); reader.onerror = error => reject(error); reader.readAsText(file); }); } | |
async function readPdfFile(file) { | |
if (typeof pdfjsLib === 'undefined') throw new Error("pdf.js library is not loaded."); | |
if (!pdfjsLib.GlobalWorkerOptions.workerSrc && typeof pdfjsLib !== 'undefined') { | |
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js'; | |
} | |
const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument({data: arrayBuffer}).promise; | |
let fullText = ""; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const textContent = await page.getTextContent(); fullText += textContent.items.map(item => item.str).join(" ") + "\n"; } return fullText; | |
} | |
async function readDocxFileWithMammoth(file) { if (typeof mammoth === 'undefined') throw new Error("mammoth.js not loaded."); const arrayBuffer = await file.arrayBuffer(); const result = await mammoth.extractRawText({ arrayBuffer: arrayBuffer }); return result.value; } | |
function isValidURL(string) { try { new URL(string); return true; } catch (_) { return false; }} | |
async function handleFetchUrl() { | |
const url = mainInput.value.trim(); if (!isValidURL(url)) { alert("Invalid URL. Please enter a full URL including http:// or https://"); return; } | |
clearInputSourceInfo(); | |
if(urlInfoTextEl) urlInfoTextEl.textContent = url.length > 20 ? url.substring(0,17)+"..." : url; | |
if(urlInfoEl) urlInfoEl.style.display = 'flex'; | |
showAiOverlay(true, `Fetching ${url.substring(0,30)}...`); | |
try { | |
const response = await fetch(`${CORS_PROXY_URL_PREFIX}${encodeURIComponent(url)}`); | |
if (!response.ok) { | |
const errorText = await response.text(); | |
console.error("Fetch error response:", errorText); | |
throw new Error(`Fetch failed: ${response.status} ${response.statusText}. Proxy or target server issue. Details: ${errorText.substring(0,100)}`); | |
} | |
const html = await response.text(); | |
const doc = new DOMParser().parseFromString(html, "text/html"); | |
const article = doc.querySelector('article, main, [role="main"], .main, #main, .content, #content, body'); | |
let text = ""; | |
if (article) { | |
article.querySelectorAll('script, style, noscript, iframe, header, footer, nav, aside').forEach(el => el.remove()); | |
text = article.textContent || ""; | |
} else { | |
text = doc.body.textContent || ""; | |
} | |
fileContentForAI = text.replace(/\s\s+/g, ' ').trim().substring(0, 30000); | |
if (!fileContentForAI.trim()) { | |
console.warn("Extracted text from URL is empty."); | |
} | |
if(languageSelector) languageSelector.value = DATA_ANALYSIS_LANG; | |
currentLanguage = DATA_ANALYSIS_LANG; | |
if(mainInput) { | |
mainInput.placeholder = `Task for URL content...`; mainInput.value = `URL: ${url}\nTask: Summarize.`; mainInput.focus(); | |
} | |
} catch (error) { | |
console.error("URL fetch error:", error); | |
if(mainInput) mainInput.value = `Error fetching ${url}: ${error.message}.`; | |
fileContentForAI = ""; | |
alert(`URL Error: ${error.message}. This could be due to the CORS proxy, the target website blocking requests, or network issues.`); | |
} finally { showAiOverlay(false); } | |
} | |
// --- Screen Share Functions --- | |
async function startScreenStream() { | |
if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { | |
try { | |
screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }); | |
if(videoElement) videoElement.srcObject = screenStream; | |
if(startStreamBtn) startStreamBtn.classList.add('hidden'); | |
if(stopStreamBtn) stopStreamBtn.classList.remove('hidden'); | |
// Optional: Update some status element to indicate screen sharing is active | |
// e.g., if(someStatusElement) someStatusElement.textContent = "Screen sharing active. Capture frame on send."; | |
console.log("Screen sharing started."); | |
// Handle stream ending (e.g., user clicks browser's "Stop sharing" button) | |
screenStream.getVideoTracks()[0].onended = () => { | |
console.log("Screen sharing stopped by user or browser."); | |
stopScreenStream(false); // Pass false to avoid recursive calls if any | |
}; | |
} catch (err) { | |
console.error("Screen share access error: ", err); | |
alert("Screen share access error: " + err.message + "\nMake sure you select a screen/window to share."); | |
if (screenStream) stopScreenStream(false); // Clean up if partial start | |
} | |
} else { | |
alert("Screen Sharing (getDisplayMedia) not supported by your browser!"); | |
} | |
} | |
function stopScreenStream(notifyUser = true) { | |
if (screenStream) { | |
screenStream.getTracks().forEach(track => track.stop()); | |
if(videoElement) videoElement.srcObject = null; | |
screenStream = null; | |
if(startStreamBtn) startStreamBtn.classList.remove('hidden'); | |
if(stopStreamBtn) stopStreamBtn.classList.add('hidden'); | |
currentScreenFrameDataUrl = ""; // Clear any captured frame | |
if (notifyUser) console.log("Screen sharing stopped."); | |
// Optional: Update status element | |
} | |
updateButtonStates(generationInProgress); // Reset button states based on generation status | |
} | |
async function captureScreenFrameAsDataURL() { | |
if (!screenStream || !videoElement.srcObject) { | |
console.warn("Screen stream not active or video element not ready for capture."); | |
return null; | |
} | |
// Ensure video element has dimensions and is ready | |
if (videoCanvas && videoElement && videoElement.readyState >= videoElement.HAVE_METADATA && videoElement.videoWidth > 0 && videoElement.videoHeight > 0) { | |
videoCanvas.width = videoElement.videoWidth; | |
videoCanvas.height = videoElement.videoHeight; | |
const ctx = videoCanvas.getContext('2d'); | |
ctx.drawImage(videoElement, 0, 0, videoCanvas.width, videoCanvas.height); | |
try { | |
return videoCanvas.toDataURL('image/jpeg', 0.85); // Use JPEG for smaller size, 0.85 quality | |
} catch (e) { | |
console.error("Error converting canvas to Data URL:", e); | |
alert("Could not capture screen frame. This can happen if the content being shared is protected or from a different origin in a way that taints the canvas."); | |
return null; | |
} | |
} else { | |
console.warn("Video element not ready for capture (no metadata/dimensions)."); | |
if (videoElement.videoWidth === 0 || videoElement.videoHeight === 0) { | |
alert("Cannot capture screen: Video dimensions are zero. Ensure content is visible in the stream."); | |
} | |
return null; | |
} | |
} | |
function handleLanguageChange(e) { | |
currentLanguage = e.target.value; | |
const screenSharePanelActive = videoStreamPanel && videoStreamPanel.classList.contains('active'); | |
if(mainInput) { | |
if (screenSharePanelActive) { | |
mainInput.placeholder = "Describe task for the screen content..."; | |
} else if (currentLanguage === DATA_ANALYSIS_LANG) { | |
mainInput.placeholder = `Describe task for content (file, URL, screen)...`; | |
} else { | |
mainInput.placeholder = `Enter task for ${currentLanguage.split('(')[0].trim()}...`; | |
} | |
} | |
if (currentLanguage === "Python (Pyodide)" && !pyodide && !pyodideLoadingPromise) { | |
if(initStatusText) initStatusText.textContent = initStatusText.textContent.replace("Ready", "Loading Pyodide for Python tasks..."); | |
pyodideLoadingPromise = loadPyodideInstance().then(() => { | |
if(initStatusText) initStatusText.textContent = initStatusText.textContent.replace("Loading Pyodide for Python tasks...", "Pyodide Ready."); | |
}).catch(err => { | |
if(initStatusText) initStatusText.textContent = initStatusText.textContent.replace("Loading Pyodide for Python tasks...", "Pyodide Failed."); | |
}); | |
} | |
} | |
function handleStopGeneration() { if (generationInProgress) { stopGenerationFlag = true; if(stopBtn) stopBtn.disabled = true; if(aiStatusText) aiStatusText.textContent = "Stopping...";}} | |
async function runPythonCode(pythonCode) { | |
if (!pyodide) { | |
if(initStatusText && !initStatusText.textContent.includes("Pyodide not loaded")) initStatusText.textContent += " Pyodide not loaded. Waiting..."; | |
try { | |
await pyodideLoadingPromise; // Wait for it if loading | |
if (!pyodide) throw new Error("Pyodide failed to load previously."); | |
if(initStatusText && initStatusText.textContent.includes("Pyodide not loaded")) initStatusText.textContent = initStatusText.textContent.replace("Pyodide not loaded. Waiting...", "Pyodide ready. Retrying Python."); | |
} catch (error) { | |
console.error("Pyodide not available for Python execution:", error); | |
if(outputFrame) outputFrame.srcdoc = `<div style='color:red;padding:20px'>Error: Pyodide (Python runtime) is not available. Cannot run Python code. ${error.message}</div>`; | |
return; | |
} | |
} | |
if(outputFrame) outputFrame.srcdoc = `<div style="padding:20px;font-family:monospace;white-space:pre-wrap;background:#f0f0f0;border-radius:8px;">Running Python...</div>`; | |
try { | |
await pyodide.loadPackagesFromImports(pythonCode); | |
let capturedOutput = ""; | |
pyodide.setStdout({batched: (str) => capturedOutput += str + "\n"}); | |
pyodide.setStderr({batched: (str) => capturedOutput += "Error: " + str + "\n"}); | |
await pyodide.runPythonAsync(pythonCode); | |
if(outputFrame) outputFrame.srcdoc = `<div style="padding:20px;font-family:monospace;white-space:pre-wrap;background:#f0f0f0;border-radius:8px;">${DOMPurify.sanitize(capturedOutput) || "Python code executed. No explicit print output."}</div>`; | |
} catch (error) { | |
console.error("Python execution error:", error); | |
if(outputFrame) outputFrame.srcdoc = `<div style='color:red;padding:20px;font-family:monospace;white-space:pre-wrap;'>Python Execution Error:<br>${DOMPurify.sanitize(error.message)}</div>`; | |
} | |
} | |
function transpileToRunnableJS(code, lang) { | |
let displayLang = lang; | |
if (lang === "TypeScript" && typeof Babel !== 'undefined') { | |
try { | |
const { code: jsCode } = Babel.transform(code, { presets: ['typescript'], filename: 'component.tsx' }); | |
code = `// Transpiled from TypeScript:\n${jsCode}`; | |
displayLang = "JavaScript (from TypeScript)"; | |
} catch (e) { | |
console.warn("TypeScript transpilation failed:", e); | |
code = `// TypeScript (transpilation failed: ${e.message})\n${code}`; | |
} | |
} else if ((lang === "React Component" || lang === "Next.js Page (Client-Side)") && typeof Babel !== 'undefined') { | |
try { | |
const { code: jsCode } = Babel.transform(code, { presets: ['react'], filename: 'component.jsx' }); | |
code = `// Transpiled from JSX (React/Next.js):\n${jsCode}\n\n/* Note: This is a basic transpile for syntax. Full React/Next.js functionality requires a proper build environment. */`; | |
displayLang = "JavaScript (from JSX)"; | |
} catch (e) { | |
console.warn("JSX transpilation failed:", e); | |
code = `// JSX (transpilation failed: ${e.message})\n${code}`; | |
} | |
} | |
return `<div style="padding:15px; background-color: #282c34; color: #abb2bf; font-family: 'Courier New', Courier, monospace; font-size: 0.9em; border-radius: 8px; overflow:auto; height:100%;box-sizing:border-box;"> | |
<h4 style="color:#61afef; margin-top:0; margin-bottom:10px; border-bottom: 1px solid #3f434a; padding-bottom:5px;">${displayLang} Preview</h4> | |
<pre style="margin:0; white-space:pre-wrap; word-break:break-all;">${DOMPurify.sanitize(code)}</pre> | |
</div>`; | |
} | |
// --- Main Actions (Send Request, Research) --- | |
async function handleSendRequest() { | |
const taskDescription = mainInput ? mainInput.value.trim() : ""; | |
let capturedScreenImageDataUrl = null; | |
const isScreenShareTabActive = videoStreamPanel && videoStreamPanel.classList.contains('active'); | |
if (isScreenShareTabActive && screenStream) { | |
showAiOverlay(true, `Capturing screen frame...`); | |
updateButtonStates(true); // Disable buttons during capture | |
capturedScreenImageDataUrl = await captureScreenFrameAsDataURL(); | |
if (!capturedScreenImageDataUrl) { | |
console.warn("Failed to capture screen frame. Proceeding without image."); | |
// Potentially alert user or handle this state | |
showAiOverlay(false); // Hide overlay if capture fails and we don't proceed | |
updateButtonStates(false); | |
alert("Could not capture screen frame. Please try again or ensure screen content is shareable."); | |
return; | |
} | |
// Keep overlay: Processing request with screen image... | |
showAiOverlay(true, `Processing request with screen image...`); | |
} | |
if (!taskDescription && !fileContentForAI && !capturedScreenImageDataUrl) { | |
alert("Please enter a task or provide content (file, URL, or active screen share)."); | |
updateButtonStates(false); // Ensure buttons are re-enabled if nothing to send | |
return; | |
} | |
generationInProgress = true; // Set true before any async UI work not covered by above capture | |
stopGenerationFlag = false; | |
updateButtonStates(true); // Ensure buttons are disabled now that we are proceeding | |
if (!aiOverlay.classList.contains('visible')) { // Only show if not already shown by capture | |
showAiOverlay(true, `Processing request...`); | |
} | |
if(codeInputArea) codeInputArea.value = ""; | |
if(outputFrame) outputFrame.srcdoc = `<p style="padding:20px;">Generating response...</p>`; | |
if(fullCodeContent) fullCodeContent.textContent = ""; | |
if (window.currentAiChart) { window.currentAiChart.destroy(); window.currentAiChart = null; } | |
try { | |
let requestContent; // This will be passed to model.generateContent() | |
let currentRequestMode = "code_generation"; // Default | |
const chartCanvasHTML = `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:10px;box-sizing:border-box;"><div style="width:95%;max-width:700px;height:95%;max-height:500px;"><canvas id="aiChart"></canvas></div></div>`; | |
if (capturedScreenImageDataUrl) { | |
currentRequestMode = "screen_analysis"; | |
const base64ImageData = capturedScreenImageDataUrl.split(',')[1]; | |
const mimeType = capturedScreenImageDataUrl.substring(capturedScreenImageDataUrl.indexOf(':') + 1, capturedScreenImageDataUrl.indexOf(';')); | |
const textPromptPart = `You are an AI assistant. An image of a user's screen is provided. | |
User's task or question about this screen image: "${taskDescription || "Describe what you see on this screen in detail."}" | |
Analyze the screen image and provide a helpful, concise response in Markdown format. | |
Focus ONLY on what is visible in the image. Do not speculate about hidden content or actions not depicted.`; | |
requestContent = [ | |
textPromptPart, | |
{ inlineData: { mimeType: mimeType, data: base64ImageData } } | |
]; | |
console.log(`Mode: ${currentRequestMode}. Task: "${taskDescription || "Describe screen"}". Image MIME: ${mimeType}, Data length: ${base64ImageData.length}`); | |
} else if (fileContentForAI || currentLanguage === DATA_ANALYSIS_LANG) { | |
currentRequestMode = "data_analysis"; | |
const dataAnalysisPrompt = ` | |
You are an AI data and content analysis assistant. | |
You have been provided with the following context (e.g., from a file, URL), which may be empty if no content was loaded: | |
Content Context (first 30,000 characters): | |
""" | |
${fileContentForAI || "No explicit content was provided apart from the task."} | |
""" | |
The user's task is: "${taskDescription}" | |
Based on the provided Content Context and the user's task, please choose the MOST APPROPRIATE response format from the following options: | |
1. **HTML TABLE:** If the task involves presenting structured data or comparing items, and the content supports it, generate a semantic HTML table. | |
2. **Chart.js JSON Configuration:** If the task is best represented by a visual chart (e.g., trends, distributions, proportions), and the data allows for it, provide a VALID JSON object for Chart.js. The JSON should be structured as: \`{"type": "chartjs", "config": {type: "bar", data: {...}, options: {...}}}\`. Ensure the config is complete and directly usable by Chart.js. | |
3. **Markdown Text Analysis:** If the task requires a textual summary, explanation, interpretation, or if the data is unsuitable for a table or chart, provide a comprehensive analysis in Markdown format. | |
CRITICAL INSTRUCTIONS: | |
- **ONLY ONE RESPONSE TYPE:** You MUST output *only* the chosen format (HTML table, Chart.js JSON, or Markdown). Do NOT include any conversational preamble, explanations about your choice, or markdown specifiers like \`\`\`html or \`\`\`json unless it's part of the Markdown text itself. | |
- **Chart.js JSON must be raw JSON:** If you choose Chart.js, the entire response must be *only* the JSON object, starting with \`{\` and ending with \`}\`. | |
- **HTML Table must be raw HTML:** If you choose HTML table, the entire response must be *only* the HTML code, starting with \`<table\` and ending with \`</table>\`. | |
- **Insufficiency:** If the provided content is insufficient or inappropriate for creating a meaningful table or chart as requested, default to a Markdown text analysis explaining why and what might be needed.`; | |
requestContent = dataAnalysisPrompt; | |
console.log(`Mode: ${currentRequestMode}. Prompt (start):`, requestContent.substring(0, 250) + "..."); | |
} else { // Default to code generation if no screen image and not data analysis | |
currentRequestMode = "code_generation"; | |
const baseP = `You are an expert AI code generation assistant. Your primary goal is to generate clean, correct, and complete code according to the user's request.`; | |
let specI = ""; | |
switch(currentLanguage){ | |
case "HTML/CSS/JS (Web Page)": specI = `Generate a single, self-contained HTML file. All CSS must be within \`<style>\` tags in the \`<head>\`, and all JavaScript must be within \`<script>\` tags, preferably at the end of the \`<body>\`. Do not use external file links for CSS or JS unless specifically asked. The HTML should be well-structured and semantic.`; break; | |
case "Python (Pyodide)": specI = `Generate Python code intended to be run in a Pyodide environment. Use \`print()\` for any output that should be displayed. If graphics are requested, explain that Pyodide typically needs integration with JavaScript for complex UIs, or use simple text-based representations if possible. Assume standard Python libraries are available. For plotting, prefer generating data that could be used with a library like Matplotlib, but output the data or a description if direct plotting in Pyodide is complex for the request.`; break; | |
case "JavaScript (Standalone)": specI = `Generate standalone JavaScript code. If it's for a browser environment, it might manipulate a hypothetical DOM or use \`console.log()\` for output. If it's for Node.js, assume a Node environment. Be clear about execution context if ambiguous.`; break; | |
case "TypeScript": specI = `Generate TypeScript code. Include type annotations. The code should be transpilable to JavaScript.`; break; | |
case "React Component": specI = `Generate a single React functional component as a self-contained JSX snippet. Include necessary imports (e.g., \`import React from 'react';\`). Focus on the component's structure and logic.`; break; | |
case "Next.js Page (Client-Side)": specI = `Generate code for a Next.js page component, focusing on client-side rendering aspects. This would typically be a React component. Assume it's a file within the 'pages' directory. If server-side logic (getServerSideProps, etc.) is implied, structure it accordingly but prioritize the client-side component code.`; break; | |
default: specI = `Generate code for ${currentLanguage.replace(' (Code Display)','').trim()}. Provide any necessary comments or context for the code.`; | |
} | |
const codeGenPrompt = `${baseP}\nUser Task: "${taskDescription}"\nTarget Language/Framework: ${currentLanguage}\n\nSpecific Instructions for ${currentLanguage}:\n${specI}\n\nCRITICAL: Output ONLY the raw code as a single block. Do NOT use Markdown backticks (e.g., \`\`\`language) to wrap the code. No explanations, no conversational text, just the code itself.`; | |
requestContent = codeGenPrompt; | |
console.log(`Mode: ${currentRequestMode}. Prompt (start):`, requestContent.substring(0, 250) + "..."); | |
} | |
const result = await model.generateContent(requestContent); | |
if (stopGenerationFlag) throw new Error("Stopped by user."); | |
const response = await result.response; let rawRspTxt = ""; | |
if (response && typeof response.text === 'function') rawRspTxt = response.text(); | |
else throw new Error("No text from Gemini response."); | |
generatedCode = rawRspTxt.replace(/^```(?:[a-zA-Z0-9_ .-]+)?\s*\n?([\s\S]*?)\n?```$/gm, '$1').trim(); | |
if (currentRequestMode === "screen_analysis" || (currentRequestMode === "data_analysis" && !(generatedCode.startsWith("{") && generatedCode.endsWith("}")) && !generatedCode.toLowerCase().includes("<table"))) { | |
const htmlOutput = DOMPurify.sanitize(marked.parse(generatedCode)); | |
if(outputFrame) outputFrame.srcdoc = `<div style="padding:20px;background:white;border-radius:8px;max-height:100%;overflow-y:auto;font-family: Inter, sans-serif; line-height:1.6;">${htmlOutput}</div>`; | |
if(codeInputArea) codeInputArea.value = generatedCode; | |
if(fullCodeContent) fullCodeContent.textContent = generatedCode; | |
} else if (currentRequestMode === "data_analysis") { // Chart or Table from Data Analysis | |
let isChart = false, isTable = false, chartCfg; | |
if (generatedCode.startsWith("{") && generatedCode.endsWith("}")) { | |
try { const p = JSON.parse(generatedCode); if(p.type === "chartjs" && p.config && p.config.type && p.config.data){isChart=true;chartCfg=p.config;}} | |
catch(e){ console.warn("Response looked like JSON but failed to parse or was not Chart.js format:", e, generatedCode.substring(0,100)); } | |
} | |
if (!isChart && generatedCode.toLowerCase().trim().startsWith("<table") && generatedCode.toLowerCase().trim().endsWith("</table>")){isTable=true;} | |
if(isChart && outputFrame){ | |
outputFrame.srcdoc = chartCanvasHTML; | |
outputFrame.onload = () => { | |
try { | |
const chartCtx = outputFrame.contentWindow.document.getElementById('aiChart'); | |
if(chartCtx){ | |
if(window.currentAiChart) window.currentAiChart.destroy(); | |
if (outputFrame.contentWindow.Chart) { | |
window.currentAiChart = new outputFrame.contentWindow.Chart(chartCtx, chartCfg); | |
} else { | |
const script = outputFrame.contentWindow.document.createElement('script'); | |
script.src = "https://cdn.jsdelivr.net/npm/chart.js"; | |
script.onload = () => { | |
window.currentAiChart = new outputFrame.contentWindow.Chart(chartCtx, chartCfg); | |
}; | |
outputFrame.contentWindow.document.head.appendChild(script); | |
} | |
} else { throw new Error("Chart canvas element not found in iframe."); } | |
} catch(e){ | |
console.error("Chart rendering error:", e); | |
if(outputFrame.contentDocument && outputFrame.contentDocument.body) outputFrame.contentDocument.body.innerHTML=`<div style='padding:10px;color:red'>Chart Error: ${DOMPurify.sanitize(e.message)}</div><pre style='font-size:0.8em;white-space:pre-wrap;word-break:break-all;'>${DOMPurify.sanitize(JSON.stringify(chartCfg,null,2))}</pre>`; | |
} | |
outputFrame.onload=null; | |
}; | |
if(codeInputArea) codeInputArea.value = `// Chart.js Configuration:\n${JSON.stringify(chartCfg,null,2)}`; | |
if(fullCodeContent) fullCodeContent.textContent = codeInputArea.value; | |
} else if (isTable && outputFrame){ | |
outputFrame.srcdoc = `<div style="padding:10px;overflow:auto;background:white;border-radius:8px;">${DOMPurify.sanitize(generatedCode,{USE_PROFILES:{html:true}})}</div>`; | |
if(codeInputArea) codeInputArea.value=generatedCode; | |
if(fullCodeContent) fullCodeContent.textContent=generatedCode; | |
} else { // Fallback for data_analysis if not chart/table (should be Markdown already) | |
const htmlOutput = DOMPurify.sanitize(marked.parse(generatedCode)); | |
if(outputFrame) outputFrame.srcdoc = `<div style="padding:20px;background:white;border-radius:8px;max-height:100%;overflow-y:auto;font-family: Inter, sans-serif;line-height:1.6;">${htmlOutput}</div>`; | |
if(codeInputArea) codeInputArea.value=generatedCode; | |
if(fullCodeContent) fullCodeContent.textContent=generatedCode; | |
} | |
} else { // code_generation | |
if(aiStatusText) aiStatusText.textContent = "Preparing preview..."; | |
if(outputFrame) { | |
switch (currentLanguage) { | |
case "HTML/CSS/JS (Web Page)": outputFrame.srcdoc = generatedCode; break; | |
case "Python (Pyodide)": await runPythonCode(generatedCode); break; | |
case "JavaScript (Standalone)": case "TypeScript": case "React Component": case "Next.js Page (Client-Side)": | |
outputFrame.srcdoc = transpileToRunnableJS(generatedCode, currentLanguage); break; | |
default: outputFrame.srcdoc = `<div style="padding:20px;"><h4 style="color:var(--primary);">${currentLanguage.replace(' (Code Display)','')} Code</h4><p>Live preview not generally supported for this language. See 'Full Code' tab.</p></div>`; | |
} | |
} | |
if(codeInputArea) codeInputArea.value=generatedCode; | |
if(fullCodeContent) fullCodeContent.textContent=generatedCode; | |
} | |
} catch (error) { | |
console.error("AI Request/Processing Error:", error); | |
const errTxt = stopGenerationFlag ? "Stopped by user." : `Error: ${error.message}`; | |
if(codeInputArea) codeInputArea.value += `\n\n--- ERROR ---\n${DOMPurify.sanitize(errTxt)}`; | |
if(outputFrame) outputFrame.srcdoc = `<div style='color:red;padding:20px;font-family: Inter, sans-serif;'><h3>Request Failed</h3><p>${DOMPurify.sanitize(errTxt)}</p></div>`; | |
// If the error was due to a non-multimodal model, guide the user | |
if (error.message && (error.message.includes("does not support image input") || error.message.includes("multimodal"))) { | |
if(outputFrame) outputFrame.srcdoc += `<p style='color:orange;padding:0 20px 20px 20px;'>Note: The current AI model ('${MODEL_NAME}') might not support image analysis. For screen analysis, a multimodal model (e.g., 'gemini-pro-vision', 'gemini-1.5-flash') is required.</p>`; | |
} | |
} finally { | |
generationInProgress = false; stopGenerationFlag = false; | |
updateButtonStates(false); // Re-enable buttons | |
showAiOverlay(false); // Hide overlay | |
currentScreenFrameDataUrl = ""; // Clear captured frame data after use | |
console.log("handleSendRequest finished."); | |
} | |
} | |
async function performResearch(topic) { | |
if (!topic) { | |
alert("Please provide a topic to research."); | |
return; | |
} | |
if (generationInProgress) { | |
alert("Another AI task is already in progress. Please wait."); | |
return; | |
} | |
console.log(`Performing research on: ${topic}`); | |
generationInProgress = true; | |
stopGenerationFlag = false; | |
updateButtonStates(true); | |
showAiOverlay(true, `Researching: ${topic.substring(0, 50)}${topic.length > 50 ? '...' : ''}`); | |
if(codeInputArea) codeInputArea.value = ""; | |
if(outputFrame) outputFrame.srcdoc = `<p style="padding:20px;">Researching topic: "${DOMPurify.sanitize(topic)}"...</p>`; | |
if(fullCodeContent) fullCodeContent.textContent = ""; | |
if (window.currentAiChart) { window.currentAiChart.destroy(); window.currentAiChart = null; } | |
try { | |
const researchPrompt = ` | |
You are an AI research assistant. Your goal is to find and synthesize information on the given topic from your knowledge base. | |
Topic to research: "${topic}" | |
Please provide a concise yet comprehensive summary of the key information related to this topic. | |
Structure your response in Markdown format. | |
If possible and relevant, try to: | |
- Identify key facts, definitions, and concepts. | |
- Mention important figures, events, or developments. | |
- Briefly explain different perspectives or common debates if applicable. | |
- If you can cite general areas or types of sources for further reading (without making up specific URLs), that would be helpful. | |
Do not invent information. If the topic is too obscure or you lack sufficient information, please state that clearly. | |
Begin your response directly with the research findings.`; | |
console.log("Research prompt (start):", researchPrompt.substring(0, 150) + "..."); | |
const result = await model.generateContent(researchPrompt); // Research is text-only | |
if (stopGenerationFlag) { | |
throw new Error("Research stopped by user."); | |
} | |
const response = await result.response; | |
let researchText = ""; | |
if (response && typeof response.text === 'function') { | |
researchText = response.text(); | |
} else { | |
throw new Error("No text received from Gemini for research query."); | |
} | |
const cleanedResearchText = researchText.replace(/^```(?:markdown)?\s*\n?([\s\S]*?)\n?```$/gm, '$1').trim(); | |
if(codeInputArea) codeInputArea.value = `# Research on: ${topic}\n\n${cleanedResearchText}`; | |
if(fullCodeContent) fullCodeContent.textContent = codeInputArea.value; | |
const htmlOutput = DOMPurify.sanitize(marked.parse(cleanedResearchText)); | |
if(outputFrame) outputFrame.srcdoc = `<div style="padding:20px;background:white;border-radius:8px;max-height:100%;overflow-y:auto;font-family: Inter, sans-serif; line-height:1.6;"><h2>Research: ${DOMPurify.sanitize(topic)}</h2>${htmlOutput}</div>`; | |
} catch (error) { | |
console.error("Research Error:", error); | |
const errTxt = stopGenerationFlag ? "Research stopped by user." : `Research Error: ${error.message}`; | |
if(codeInputArea) codeInputArea.value += `\n\n--- RESEARCH ERROR ---\n${DOMPurify.sanitize(errTxt)}`; | |
if(outputFrame) outputFrame.srcdoc = `<div style='color:red;padding:20px;font-family: Inter, sans-serif;'><h3>Research Failed</h3><p>${DOMPurify.sanitize(errTxt)}</p></div>`; | |
} finally { | |
generationInProgress = false; | |
stopGenerationFlag = false; | |
updateButtonStates(false); | |
showAiOverlay(false); | |
console.log("performResearch finished."); | |
} | |
} | |
// --- Start Application --- | |
if (document.readyState === 'loading') { | |
document.addEventListener('DOMContentLoaded', initializeApp); | |
} else { | |
initializeApp(); | |
} | |
console.log("Script execution reached end (async ops may continue)."); | |