From 1ed154b94e01b95afed97eec321dc94453d1358e Mon Sep 17 00:00:00 2001 From: Tyson Thomas Date: Sat, 4 Oct 2025 22:45:17 -0700 Subject: [PATCH 1/4] Add web app rendering --- front_end/panels/ai_chat/BUILD.gn | 3 + .../implementation/ConfiguredAgents.ts | 9 +- .../implementation/agents/ActionAgent.ts | 3 + .../implementation/agents/WebTaskAgent.ts | 3 + .../ai_chat/core/BaseOrchestratorAgent.ts | 12 +- .../panels/ai_chat/tools/GetWebAppDataTool.ts | 261 ++++++++++++++++++ .../panels/ai_chat/tools/RemoveWebAppTool.ts | 131 +++++++++ .../panels/ai_chat/tools/RenderWebAppTool.ts | 240 ++++++++++++++++ front_end/panels/ai_chat/tools/Tools.ts | 11 + 9 files changed, 670 insertions(+), 3 deletions(-) create mode 100644 front_end/panels/ai_chat/tools/GetWebAppDataTool.ts create mode 100644 front_end/panels/ai_chat/tools/RemoveWebAppTool.ts create mode 100644 front_end/panels/ai_chat/tools/RenderWebAppTool.ts diff --git a/front_end/panels/ai_chat/BUILD.gn b/front_end/panels/ai_chat/BUILD.gn index 464fde0fa98..8a52dabfa72 100644 --- a/front_end/panels/ai_chat/BUILD.gn +++ b/front_end/panels/ai_chat/BUILD.gn @@ -93,6 +93,9 @@ devtools_module("ai_chat") { "tools/DocumentSearchTool.ts", "tools/SequentialThinkingTool.ts", "tools/ThinkingTool.ts", + "tools/RenderWebAppTool.ts", + "tools/GetWebAppDataTool.ts", + "tools/RemoveWebAppTool.ts", "agent_framework/ConfigurableAgentTool.ts", "agent_framework/AgentRunner.ts", "agent_framework/AgentRunnerEventBus.ts", diff --git a/front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts b/front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts index ceb55c4ab36..e55dc7153ae 100644 --- a/front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts +++ b/front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts @@ -8,7 +8,7 @@ import { SchemaBasedExtractorTool } from '../../tools/SchemaBasedExtractorTool.j import { StreamlinedSchemaExtractorTool } from '../../tools/StreamlinedSchemaExtractorTool.js'; import { BookmarkStoreTool } from '../../tools/BookmarkStoreTool.js'; import { DocumentSearchTool } from '../../tools/DocumentSearchTool.js'; -import { NavigateURLTool, PerformActionTool, GetAccessibilityTreeTool, SearchContentTool, NavigateBackTool, NodeIDsToURLsTool, TakeScreenshotTool, ScrollPageTool, WaitTool } from '../../tools/Tools.js'; +import { NavigateURLTool, PerformActionTool, GetAccessibilityTreeTool, SearchContentTool, NavigateBackTool, NodeIDsToURLsTool, TakeScreenshotTool, ScrollPageTool, WaitTool, RenderWebAppTool, GetWebAppDataTool, RemoveWebAppTool } from '../../tools/Tools.js'; import { HTMLToMarkdownTool } from '../../tools/HTMLToMarkdownTool.js'; import { ConfigurableAgentTool, ToolRegistry } from '../ConfigurableAgentTool.js'; import { ThinkingTool } from '../../tools/ThinkingTool.js'; @@ -49,7 +49,12 @@ export function initializeConfiguredAgents(): void { ToolRegistry.registerToolFactory('scroll_page', () => new ScrollPageTool()); ToolRegistry.registerToolFactory('wait_for_page_load', () => new WaitTool()); ToolRegistry.registerToolFactory('thinking', () => new ThinkingTool()); - + + // Register webapp rendering tools + ToolRegistry.registerToolFactory('render_webapp', () => new RenderWebAppTool()); + ToolRegistry.registerToolFactory('get_webapp_data', () => new GetWebAppDataTool()); + ToolRegistry.registerToolFactory('remove_webapp', () => new RemoveWebAppTool()); + // Register bookmark and document search tools ToolRegistry.registerToolFactory('bookmark_store', () => new BookmarkStoreTool()); ToolRegistry.registerToolFactory('document_search', () => new DocumentSearchTool()); diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgent.ts index 7edc91fe60e..7a9dff99c3c 100644 --- a/front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgent.ts +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgent.ts @@ -91,6 +91,9 @@ Conclusion: Fix the args format and retry with proper syntax: { "method": "fill" 'node_ids_to_urls', 'scroll_page', 'take_screenshot', + 'render_webapp', + 'get_webapp_data', + 'remove_webapp', ], maxIterations: 10, modelName: MODEL_SENTINELS.USE_MINI, diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/WebTaskAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/WebTaskAgent.ts index 23b49ad328d..f9a86944bf6 100644 --- a/front_end/panels/ai_chat/agent_framework/implementation/agents/WebTaskAgent.ts +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/WebTaskAgent.ts @@ -204,6 +204,9 @@ Remember: **Plan adaptively, execute systematically, validate continuously, and 'take_screenshot', 'wait_for_page_load', 'thinking', + 'render_webapp', + 'get_webapp_data', + 'remove_webapp', ], maxIterations: 15, temperature: 0.3, diff --git a/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts b/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts index 52399d0e3f2..56a8d398611 100644 --- a/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts +++ b/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts @@ -24,7 +24,11 @@ import { NodeIDsToURLsTool, GetVisitsByDomainTool, GetVisitsByKeywordTool, - SearchVisitHistoryTool, type Tool + SearchVisitHistoryTool, + RenderWebAppTool, + GetWebAppDataTool, + RemoveWebAppTool, + type Tool } from '../tools/Tools.js'; // Imports from their own files @@ -299,6 +303,9 @@ export const AGENT_CONFIGS: {[key: string]: AgentConfig} = { ToolRegistry.getToolInstance('research_agent') || (() => { throw new Error('research_agent tool not found'); })(), new FinalizeWithCritiqueTool(), new SearchVisitHistoryTool(), + new RenderWebAppTool(), + new GetWebAppDataTool(), + new RemoveWebAppTool(), ] }, [BaseOrchestratorAgentType.DEEP_RESEARCH]: { @@ -315,6 +322,9 @@ export const AGENT_CONFIGS: {[key: string]: AgentConfig} = { ToolRegistry.getToolInstance('bookmark_store') || (() => { throw new Error('bookmark_store tool not found'); })(), ToolRegistry.getToolInstance('search_agent') || (() => { throw new Error('search_agent tool not found'); })(), new FinalizeWithCritiqueTool(), + new RenderWebAppTool(), + new GetWebAppDataTool(), + new RemoveWebAppTool(), ] }, // [BaseOrchestratorAgentType.SHOPPING]: { diff --git a/front_end/panels/ai_chat/tools/GetWebAppDataTool.ts b/front_end/panels/ai_chat/tools/GetWebAppDataTool.ts new file mode 100644 index 00000000000..3fc778c8072 --- /dev/null +++ b/front_end/panels/ai_chat/tools/GetWebAppDataTool.ts @@ -0,0 +1,261 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import * as SDK from '../../../core/sdk/sdk.js'; +import { createLogger } from '../core/Logger.js'; +import type { Tool, LLMContext, ErrorResult } from './Tools.js'; + +const logger = createLogger('GetWebAppDataTool'); + +/** + * Arguments for retrieving webapp data + */ +export interface GetWebAppDataArgs { + webappId: string; + reasoning: string; + waitForSubmit?: boolean; + timeout?: number; +} + +/** + * Result of webapp data retrieval + */ +export interface GetWebAppDataResult { + success: boolean; + formData: Record; + message: string; +} + +/** + * Tool for retrieving data from rendered webapp iframe + * Extracts values from input, select, textarea, checkbox, and radio elements + * within the webapp iframe. + */ +export class GetWebAppDataTool implements Tool { + name = 'get_webapp_data'; + description = 'Retrieves data from form elements within a previously rendered webapp iframe. Can optionally wait for form submission before retrieving data. Returns an object with field names as keys and their values. Supports text inputs, emails, selects, textareas, checkboxes, and radio buttons.'; + + async execute(args: GetWebAppDataArgs, _ctx?: LLMContext): Promise { + logger.info('Retrieving webapp data', { + webappId: args.webappId, + reasoning: args.reasoning, + waitForSubmit: args.waitForSubmit, + timeout: args.timeout + }); + + const { webappId, reasoning, waitForSubmit = false, timeout = 30000 } = args; + + // Validate required arguments + if (!webappId || typeof webappId !== 'string') { + return { error: 'webappId is required and must be a string' }; + } + + if (!reasoning || typeof reasoning !== 'string') { + return { error: 'Reasoning is required and must be a string' }; + } + + // Get the primary page target + const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); + if (!target) { + logger.error('No primary page target available'); + return { error: 'No page target available' }; + } + + try { + const runtimeAgent = target.runtimeAgent(); + + // Wait for form submission if requested + if (waitForSubmit) { + const startTime = Date.now(); + const pollInterval = 500; // Poll every 500ms + + logger.info('Waiting for webapp form submission', { webappId, timeout }); + + while (Date.now() - startTime < timeout) { + const checkResult = await runtimeAgent.invoke_evaluate({ + expression: ` + (() => { + const iframe = document.getElementById(${JSON.stringify(webappId)}); + if (!iframe) { + return { found: false }; + } + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + if (!iframeDoc) { + return { found: false }; + } + return { + found: true, + submitted: iframeDoc.body.getAttribute('data-submitted') === 'true' + }; + })() + `, + returnByValue: true, + }); + + const checkData = checkResult.result.value as { found: boolean; submitted?: boolean }; + + if (!checkData.found) { + logger.error('Webapp iframe not found while waiting for submission'); + return { error: `Webapp iframe not found with ID: ${webappId}` }; + } + + if (checkData.submitted) { + logger.info('Webapp form submission detected', { webappId }); + break; + } + + // Wait before next poll + await new Promise(resolve => setTimeout(resolve, pollInterval)); + } + + // Check if we timed out + if (Date.now() - startTime >= timeout) { + logger.warn('Timeout waiting for webapp form submission', { webappId, timeout }); + return { error: `Timeout waiting for webapp form submission after ${timeout}ms` }; + } + } + + // Execute data extraction script in page context + const result = await runtimeAgent.invoke_evaluate({ + expression: ` + (() => { + // Find the webapp iframe + const iframe = document.getElementById(${JSON.stringify(webappId)}); + if (!iframe) { + return { + success: false, + error: 'Webapp iframe not found with ID: ${webappId}' + }; + } + + // Access iframe document + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + if (!iframeDoc) { + return { + success: false, + error: 'Cannot access webapp iframe content' + }; + } + + // Collect form data + const formData = {}; + + // Find all form elements within the iframe + const inputs = iframeDoc.querySelectorAll('input, select, textarea'); + + inputs.forEach((element) => { + // Get the field identifier (prefer name, fallback to id) + const fieldName = element.name || element.id; + + // Skip elements without identifiers + if (!fieldName) { + return; + } + + // Extract value based on element type + if (element.tagName.toLowerCase() === 'input') { + const inputType = element.type ? element.type.toLowerCase() : 'text'; + + if (inputType === 'checkbox') { + // For checkboxes, check if we already have this field name + if (fieldName in formData) { + // Multiple checkboxes with same name - create array + if (!Array.isArray(formData[fieldName])) { + formData[fieldName] = [formData[fieldName]]; + } + if (element.checked) { + formData[fieldName].push(element.value || true); + } + } else { + // First checkbox with this name + formData[fieldName] = element.checked ? (element.value || true) : false; + } + } else if (inputType === 'radio') { + // For radio buttons, only store if checked + if (element.checked) { + formData[fieldName] = element.value || true; + } + } else { + // Text, email, number, password, etc. + formData[fieldName] = element.value || ''; + } + } else if (element.tagName.toLowerCase() === 'select') { + // For select elements, get selected value + if (element.multiple) { + // Multiple select - get array of selected values + const selectedOptions = Array.from(element.selectedOptions || []); + formData[fieldName] = selectedOptions.map(opt => opt.value); + } else { + // Single select + formData[fieldName] = element.value || ''; + } + } else if (element.tagName.toLowerCase() === 'textarea') { + // For textareas + formData[fieldName] = element.value || ''; + } + }); + + return { + success: true, + formData: formData, + message: 'Webapp data retrieved successfully' + }; + })() + `, + returnByValue: true, + }); + + // Check for evaluation errors + if (result.exceptionDetails) { + const errorMsg = result.exceptionDetails.text || 'Unknown evaluation error'; + logger.error('Webapp data retrieval failed with exception:', errorMsg); + return { error: `Webapp data retrieval failed: ${errorMsg}` }; + } + + // Extract result + const retrievalResult = result.result.value as GetWebAppDataResult | { success: false; error: string }; + + if (!retrievalResult.success) { + const error = 'error' in retrievalResult ? retrievalResult.error : 'Unknown error'; + logger.error('Webapp data retrieval script returned error:', error); + return { error }; + } + + logger.info('Successfully retrieved webapp data', { + webappId, + fieldCount: Object.keys((retrievalResult as GetWebAppDataResult).formData).length + }); + + return retrievalResult as GetWebAppDataResult; + + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error('Failed to retrieve webapp data:', errorMsg); + return { error: `Failed to retrieve webapp data: ${errorMsg}` }; + } + } + + schema = { + type: 'object', + properties: { + webappId: { + type: 'string', + description: 'The unique webapp ID returned from the render_webapp tool. Used to identify which webapp to retrieve data from.', + }, + reasoning: { + type: 'string', + description: 'Required explanation for why this data is being retrieved (e.g., "Collecting submitted user information")', + }, + waitForSubmit: { + type: 'boolean', + description: 'If true, waits for form submission before retrieving data. The tool will poll until the form is submitted or timeout is reached. Default: false', + }, + timeout: { + type: 'number', + description: 'Maximum time to wait for form submission in milliseconds. Only used when waitForSubmit is true. Default: 30000 (30 seconds)', + }, + }, + required: ['webappId', 'reasoning'], + }; +} diff --git a/front_end/panels/ai_chat/tools/RemoveWebAppTool.ts b/front_end/panels/ai_chat/tools/RemoveWebAppTool.ts new file mode 100644 index 00000000000..0789e55bcb1 --- /dev/null +++ b/front_end/panels/ai_chat/tools/RemoveWebAppTool.ts @@ -0,0 +1,131 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import * as SDK from '../../../core/sdk/sdk.js'; +import { createLogger } from '../core/Logger.js'; +import type { Tool, LLMContext, ErrorResult } from './Tools.js'; + +const logger = createLogger('RemoveWebAppTool'); + +/** + * Arguments for removing webapp + */ +export interface RemoveWebAppArgs { + webappId: string; + reasoning: string; +} + +/** + * Result of webapp removal + */ +export interface RemoveWebAppResult { + success: boolean; + removed: string[]; + message: string; +} + +/** + * Tool for removing a rendered webapp iframe from the page + * Cleans up the iframe element and all associated resources. + */ +export class RemoveWebAppTool implements Tool { + name = 'remove_webapp'; + description = 'Removes a previously rendered webapp iframe from the page. Cleans up the full-screen iframe and releases resources. Use this after data collection is complete or when the webapp is no longer needed.'; + + async execute(args: RemoveWebAppArgs, _ctx?: LLMContext): Promise { + logger.info('Removing webapp', { + webappId: args.webappId, + reasoning: args.reasoning + }); + + const { webappId, reasoning } = args; + + // Validate required arguments + if (!webappId || typeof webappId !== 'string') { + return { error: 'webappId is required and must be a string' }; + } + + if (!reasoning || typeof reasoning !== 'string') { + return { error: 'Reasoning is required and must be a string' }; + } + + // Get the primary page target + const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); + if (!target) { + logger.error('No primary page target available'); + return { error: 'No page target available' }; + } + + try { + const runtimeAgent = target.runtimeAgent(); + + // Execute removal script in page context + const result = await runtimeAgent.invoke_evaluate({ + expression: ` + (() => { + const removed = []; + + // Remove the webapp iframe + const iframe = document.getElementById(${JSON.stringify(webappId)}); + if (iframe) { + iframe.remove(); + removed.push('iframe'); + } + + return { + success: true, + removed: removed, + message: removed.length > 0 ? + 'Webapp removed: ' + removed.join(', ') : + 'No webapp elements found to remove' + }; + })() + `, + returnByValue: true, + }); + + // Check for evaluation errors + if (result.exceptionDetails) { + const errorMsg = result.exceptionDetails.text || 'Unknown evaluation error'; + logger.error('Webapp removal failed with exception:', errorMsg); + return { error: `Webapp removal failed: ${errorMsg}` }; + } + + // Extract result + const removalResult = result.result.value as RemoveWebAppResult; + + if (!removalResult || !removalResult.success) { + logger.error('Webapp removal script returned unsuccessful result'); + return { error: 'Webapp removal script failed to execute properly' }; + } + + logger.info('Successfully removed webapp', { + webappId, + removed: removalResult.removed + }); + + return removalResult; + + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error('Failed to remove webapp:', errorMsg); + return { error: `Failed to remove webapp: ${errorMsg}` }; + } + } + + schema = { + type: 'object', + properties: { + webappId: { + type: 'string', + description: 'The unique webapp ID returned from the render_webapp tool. Used to identify which webapp to remove.', + }, + reasoning: { + type: 'string', + description: 'Required explanation for why this webapp is being removed (e.g., "User completed form submission, cleaning up")', + }, + }, + required: ['webappId', 'reasoning'], + }; +} diff --git a/front_end/panels/ai_chat/tools/RenderWebAppTool.ts b/front_end/panels/ai_chat/tools/RenderWebAppTool.ts new file mode 100644 index 00000000000..ff00a71cc76 --- /dev/null +++ b/front_end/panels/ai_chat/tools/RenderWebAppTool.ts @@ -0,0 +1,240 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import * as SDK from '../../../core/sdk/sdk.js'; +import { createLogger } from '../core/Logger.js'; +import type { Tool, LLMContext, ErrorResult } from './Tools.js'; + +const logger = createLogger('RenderWebAppTool'); + +/** + * Arguments for webapp rendering + */ +export interface RenderWebAppArgs { + html: string; + css?: string; + js?: string; + reasoning: string; +} + +/** + * Result of webapp rendering + */ +export interface RenderWebAppResult { + success: boolean; + webappId: string; + message: string; +} + +/** + * Tool for rendering a full-screen webapp using an iframe + * This enables AI agents to render dynamic UI applications (forms, dialogs, interactive apps) + * in an isolated full-screen iframe for user interaction and data collection. + */ +export class RenderWebAppTool implements Tool { + name = 'render_webapp'; + description = 'Renders a full-screen webapp in an isolated iframe. Creates an interactive application with HTML, CSS, and JavaScript for collecting user data or showing dynamic content. The webapp runs in a sandboxed iframe with full viewport coverage. Returns a unique webappId for later data retrieval and cleanup.'; + + async execute(args: RenderWebAppArgs, _ctx?: LLMContext): Promise { + logger.info('Rendering webapp', { + htmlLength: args.html.length, + hasCss: !!args.css, + hasJs: !!args.js, + reasoning: args.reasoning + }); + + const { html, css, js, reasoning } = args; + + // Validate required arguments + if (!html || typeof html !== 'string') { + return { error: 'HTML content is required and must be a string' }; + } + + if (!reasoning || typeof reasoning !== 'string') { + return { error: 'Reasoning is required and must be a string' }; + } + + // Get the primary page target + const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); + if (!target) { + logger.error('No primary page target available'); + return { error: 'No page target available' }; + } + + // Navigate to blank page first for clean canvas + logger.info('Navigating to blank page before rendering webapp'); + const pageAgent = target.pageAgent(); + if (pageAgent) { + try { + const navResult = await pageAgent.invoke_navigate({ url: 'about:blank' }); + if (navResult.getError()) { + logger.warn(`Navigation to blank page failed: ${navResult.getError()}, continuing anyway`); + } else { + // Wait briefly for blank page to load (should be instant) + await new Promise(resolve => setTimeout(resolve, 300)); + logger.info('Navigated to blank page successfully'); + } + } catch (navError) { + logger.warn('Error navigating to blank page, continuing anyway:', navError); + } + } + + try { + const runtimeAgent = target.runtimeAgent(); + + // Execute webapp rendering script in page context + const result = await runtimeAgent.invoke_evaluate({ + expression: ` + (() => { + // Generate unique webapp ID + const webappId = 'devtools-webapp-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); + + // Create full-screen iframe + const iframe = document.createElement('iframe'); + iframe.id = webappId; + iframe.setAttribute('data-devtools-webapp', 'true'); + iframe.setAttribute('data-reasoning', ${JSON.stringify(reasoning)}); + + // Style iframe for full-screen coverage + iframe.style.position = 'fixed'; + iframe.style.top = '0'; + iframe.style.left = '0'; + iframe.style.width = '100vw'; + iframe.style.height = '100vh'; + iframe.style.border = 'none'; + iframe.style.zIndex = '999999'; + iframe.style.backgroundColor = 'white'; + + // Build complete HTML document for iframe + const fullHTML = '' + + '' + + '' + + '' + + '' + + (${JSON.stringify(css || '')} ? '' : '') + + '' + + '' + + ${JSON.stringify(html)} + + '' + + ''; + + // Set iframe content using srcdoc + iframe.srcdoc = fullHTML; + + // Append iframe to body + document.body.appendChild(iframe); + + // Wait for iframe to load, then inject JavaScript and submit detection + iframe.addEventListener('load', function() { + try { + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + + // Inject JavaScript with automatic submit detection + const script = iframeDoc.createElement('script'); + script.setAttribute('data-devtools-injected', 'true'); + + // Build script content + let scriptContent = '(function() {'; + + // Add automatic submit detection for forms + scriptContent += 'const forms = document.querySelectorAll("form");'; + scriptContent += 'forms.forEach(function(form) {'; + scriptContent += ' form.addEventListener("submit", function(e) {'; + scriptContent += ' e.preventDefault();'; + scriptContent += ' document.body.setAttribute("data-submitted", "true");'; + scriptContent += ' document.body.setAttribute("data-submit-time", Date.now().toString());'; + scriptContent += ' });'; + scriptContent += '});'; + + // Add button click detection as fallback + scriptContent += 'const buttons = document.querySelectorAll("button");'; + scriptContent += 'buttons.forEach(function(btn) {'; + scriptContent += ' if (btn.type === "submit" || btn.textContent.toLowerCase().includes("submit")) {'; + scriptContent += ' btn.addEventListener("click", function(e) {'; + scriptContent += ' setTimeout(function() {'; + scriptContent += ' document.body.setAttribute("data-submitted", "true");'; + scriptContent += ' document.body.setAttribute("data-submit-time", Date.now().toString());'; + scriptContent += ' }, 100);'; + scriptContent += ' });'; + scriptContent += ' }'; + scriptContent += '});'; + + // Add custom JavaScript if provided + if (${JSON.stringify(js || '')}) { + scriptContent += 'try {'; + scriptContent += ${JSON.stringify(js || '')}; + scriptContent += '} catch (error) {'; + scriptContent += ' console.error("Error in custom webapp script:", error);'; + scriptContent += '}'; + } + + scriptContent += '})();'; + script.textContent = scriptContent; + iframeDoc.body.appendChild(script); + } catch (scriptError) { + console.error('Failed to inject script into iframe:', scriptError); + } + }); + + return { + success: true, + webappId: webappId, + message: 'Webapp rendered successfully in full-screen iframe' + }; + })() + `, + returnByValue: true, + }); + + // Check for evaluation errors + if (result.exceptionDetails) { + const errorMsg = result.exceptionDetails.text || 'Unknown evaluation error'; + logger.error('Webapp rendering failed with exception:', errorMsg); + return { error: `Webapp rendering failed: ${errorMsg}` }; + } + + // Extract result + const renderResult = result.result.value as RenderWebAppResult; + + if (!renderResult || !renderResult.success) { + logger.error('Webapp rendering script returned unsuccessful result'); + return { error: 'Webapp rendering script failed to execute properly' }; + } + + logger.info('Successfully rendered webapp', { + webappId: renderResult.webappId + }); + + return renderResult; + + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error('Failed to render webapp:', errorMsg); + return { error: `Failed to render webapp: ${errorMsg}` }; + } + } + + schema = { + type: 'object', + properties: { + html: { + type: 'string', + description: 'The HTML content for the webapp body (e.g., form, dialog, interactive UI). Will be inserted into the iframe body.', + }, + css: { + type: 'string', + description: 'Optional CSS styles for the webapp. Will be added to the iframe head as a