Last active
December 9, 2025 23:44
-
-
Save msabramo/af3e7ecb44cd762fca80ba5911fa2d1b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Avalon Image Renderer</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| background: white; | |
| min-height: 100vh; | |
| padding: 0; | |
| margin: 0; | |
| overflow-y: auto; | |
| } | |
| .container { | |
| max-width: 100%; | |
| width: 100%; | |
| margin: 0; | |
| background: white; | |
| overflow: visible; | |
| min-height: 100vh; | |
| } | |
| .header { | |
| display: none; | |
| } | |
| .content { | |
| padding: 2px; | |
| } | |
| .image-container { | |
| background: #f8f9fa; | |
| border-radius: 3px; | |
| padding: 2px; | |
| margin-bottom: 2px; | |
| border: 1px solid #e9ecef; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .image-container img { | |
| max-width: 100%; | |
| max-height: 350px; | |
| width: auto; | |
| height: auto; | |
| border-radius: 3px; | |
| box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); | |
| display: block; | |
| } | |
| .metadata { | |
| background: #f8f9fa; | |
| border-radius: 2px; | |
| padding: 3px; | |
| margin-bottom: 2px; | |
| } | |
| .metadata-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); | |
| gap: 3px; | |
| } | |
| .metadata-item { | |
| background: white; | |
| padding: 3px 5px; | |
| border-radius: 2px; | |
| border-left: 2px solid #667eea; | |
| } | |
| .metadata-item .label { | |
| font-size: 8px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.1px; | |
| color: #6c757d; | |
| font-weight: 600; | |
| margin-bottom: 1px; | |
| } | |
| .metadata-item .value { | |
| font-size: 10px; | |
| font-weight: 500; | |
| color: #212529; | |
| } | |
| .prompt-section { | |
| display: none; | |
| } | |
| .description-section { | |
| background: #d1ecf1; | |
| border: 1px solid #17a2b8; | |
| border-radius: 2px; | |
| padding: 3px; | |
| margin-bottom: 2px; | |
| } | |
| .description-section h3 { | |
| font-size: 8px; | |
| font-weight: 600; | |
| color: #0c5460; | |
| margin-bottom: 2px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.1px; | |
| } | |
| .description-section .description-text { | |
| font-size: 10px; | |
| line-height: 1.2; | |
| color: #212529; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 60px 20px; | |
| color: #6c757d; | |
| } | |
| .loading-spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 4px solid #e9ecef; | |
| border-top-color: #667eea; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 20px; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .error { | |
| background: #f8d7da; | |
| border: 2px solid #dc3545; | |
| border-radius: 12px; | |
| padding: 20px; | |
| color: #721c24; | |
| } | |
| .error h3 { | |
| font-size: 16px; | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| } | |
| .json-fallback { | |
| background: white; | |
| padding: 4px; | |
| text-align: left; | |
| } | |
| .json-fallback-header { | |
| background: #f8f9fa; | |
| padding: 10px 12px; | |
| border-bottom: 1px solid #e9ecef; | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: #495057; | |
| text-align: left; | |
| } | |
| .json-container { | |
| padding: 12px; | |
| overflow: auto; | |
| max-height: 600px; | |
| text-align: left; | |
| } | |
| .json-container pre { | |
| margin: 0; | |
| font-family: 'Monaco', 'Menlo', 'Consolas', monospace; | |
| font-size: 13px; | |
| line-height: 1.6; | |
| color: #212529; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| text-align: left; | |
| } | |
| .json-key { | |
| color: #0066cc; | |
| font-weight: 500; | |
| } | |
| .json-string { | |
| color: #008000; | |
| } | |
| .json-number { | |
| color: #cc6600; | |
| } | |
| .json-boolean { | |
| color: #0000cc; | |
| font-weight: 600; | |
| } | |
| .json-null { | |
| color: #999; | |
| font-style: italic; | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 4px 12px; | |
| border-radius: 12px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .badge-gemini { | |
| background: #e3f2fd; | |
| color: #1565c0; | |
| } | |
| .badge-nano { | |
| background: #fff9c4; | |
| color: #f57f17; | |
| } | |
| .text-content { | |
| background: white; | |
| padding: 16px; | |
| overflow-y: auto; | |
| max-height: 90vh; | |
| } | |
| .text-content-header { | |
| background: #f8f9fa; | |
| padding: 8px 12px; | |
| border-bottom: 1px solid #e9ecef; | |
| font-size: 11px; | |
| font-weight: 600; | |
| color: #495057; | |
| text-align: left; | |
| } | |
| .text-content-body { | |
| padding: 16px; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| font-size: 14px; | |
| line-height: 1.6; | |
| color: #212529; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="content"> | |
| <div id="output" class="loading"> | |
| <div class="loading-spinner"></div> | |
| <div>Waiting for image data from LangSmith...</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| function extractTextContent(data) { | |
| // Check if the response contains plain text content | |
| // Try LangSmith generations wrapper | |
| if (data && data.generations && Array.isArray(data.generations)) { | |
| const firstGen = data.generations[0]; | |
| if (Array.isArray(firstGen) && firstGen.length > 0) { | |
| const genData = firstGen[0]; | |
| // Check message.kwargs.content (LangChain AIMessage format) | |
| if (genData.message?.kwargs?.content) { | |
| return genData.message.kwargs.content; | |
| } | |
| // Check text field | |
| if (genData.text && typeof genData.text === 'string') { | |
| return genData.text; | |
| } | |
| } | |
| } | |
| // Check direct text field | |
| if (data && typeof data.text === 'string') { | |
| return data.text; | |
| } | |
| return null; | |
| } | |
| function extractHtmlContent(data) { | |
| // Check if the response contains HTML content | |
| const textContent = extractTextContent(data); | |
| if (textContent) { | |
| const trimmed = textContent.trim(); | |
| if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html')) { | |
| console.log('✓ Detected HTML content in response'); | |
| return trimmed; | |
| } | |
| } | |
| return null; | |
| } | |
| function extractImageData(data) { | |
| // Handle different response formats | |
| // Try LangSmith generations wrapper (most common from LangSmith postMessage) | |
| if (data && data.generations && Array.isArray(data.generations)) { | |
| const firstGen = data.generations[0]; | |
| if (Array.isArray(firstGen) && firstGen.length > 0) { | |
| const genData = firstGen[0]; | |
| // FIRST: Try generation_info.response_data (most reliable) | |
| if (genData.generation_info && genData.generation_info.response_data) { | |
| console.log('✓ Found response_data in generation_info'); | |
| const result = extractImageData(genData.generation_info.response_data); | |
| if (result) { | |
| console.log('✓ Successfully extracted image from generation_info'); | |
| return result; | |
| } | |
| } | |
| // FALLBACK: Try parsing the text field | |
| if (genData.text) { | |
| try { | |
| // Try parsing as-is first (proper JSON) | |
| let parsed; | |
| try { | |
| parsed = JSON.parse(genData.text); | |
| } catch (e1) { | |
| // If that fails, try replacing single quotes (Python dict format) | |
| console.log('First parse failed, trying single quote replacement:', e1); | |
| parsed = JSON.parse(genData.text.replace(/'/g, '"')); | |
| } | |
| console.log('Successfully parsed generation text:', parsed); | |
| const result = extractImageData(parsed); | |
| if (result) { | |
| console.log('Extracted image data from parsed text:', result); | |
| return result; | |
| } | |
| } catch (e) { | |
| console.error('Could not parse generation text as JSON:', e); | |
| console.error('Text was:', genData.text.substring(0, 200)); | |
| } | |
| } | |
| // Try the generation data directly | |
| const result = extractImageData(genData); | |
| if (result) return result; | |
| } | |
| } | |
| // Try direct result object (from provider response) | |
| if (data && data.result && data.result.url) { | |
| return { | |
| imageUrl: data.result.url, | |
| metadata: data.result.metadata || {}, | |
| description: data.result.description || null | |
| }; | |
| } | |
| // Try choices format (from DMG response) | |
| if (data && data.choices && data.choices.length > 0) { | |
| console.log('Found choices array, checking for images...'); | |
| const message = data.choices[0].message; | |
| console.log('Message structure:', message); | |
| // Check images array | |
| if (message && message.images && message.images.length > 0) { | |
| console.log('Found images array with', message.images.length, 'images'); | |
| const firstImage = message.images[0]; | |
| console.log('First image structure:', firstImage); | |
| if (firstImage.image_url && firstImage.image_url.url) { | |
| console.log('✓ Successfully extracted image URL'); | |
| return { | |
| imageUrl: firstImage.image_url.url, | |
| metadata: data.usage || {}, | |
| description: message.content || null, | |
| model: data.model | |
| }; | |
| } else { | |
| console.warn('image_url or url field not found in image object'); | |
| } | |
| } else { | |
| console.warn('No images array found in message'); | |
| } | |
| } | |
| // Try nested response_data (from generation_info) | |
| if (data && data.response_data) { | |
| return extractImageData(data.response_data); | |
| } | |
| return null; | |
| } | |
| function getProviderBadge(model) { | |
| if (!model) return ''; | |
| if (model.includes('gemini-2.0')) { | |
| return '<span class="badge badge-gemini">Gemini 2.0 Flash</span>'; | |
| } else if (model.includes('gemini-2.5')) { | |
| return '<span class="badge badge-nano">Nano Banana (Gemini 2.5)</span>'; | |
| } | |
| return `<span class="badge">${model}</span>`; | |
| } | |
| function formatBytes(bytes) { | |
| if (!bytes) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; | |
| } | |
| function gcd(a, b) { | |
| return b === 0 ? a : gcd(b, a % b); | |
| } | |
| function getAspectRatio(width, height) { | |
| const divisor = gcd(width, height); | |
| return `${width / divisor}:${height / divisor}`; | |
| } | |
| function getImageDimensions(imageUrl) { | |
| return new Promise((resolve) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| const aspectRatio = getAspectRatio(img.width, img.height); | |
| resolve({ | |
| width: img.width, | |
| height: img.height, | |
| aspectRatio: aspectRatio, | |
| formatted: `${img.width} × ${img.height} (${aspectRatio})` | |
| }); | |
| }; | |
| img.onerror = () => { | |
| resolve({ formatted: 'Unable to load image' }); | |
| }; | |
| img.src = imageUrl; | |
| }); | |
| } | |
| function extractPromptFromInputs(inputs) { | |
| // Handle different input formats | |
| if (!inputs) return null; | |
| // Direct prompt field | |
| if (inputs.prompt) return inputs.prompt; | |
| if (inputs.content) return inputs.content; | |
| // Messages array (LangChain format) | |
| if (inputs.messages && Array.isArray(inputs.messages)) { | |
| // LangSmith serializes messages as nested arrays: messages[0][0] | |
| // Check if first element is an array (nested structure) | |
| let messageArray = inputs.messages; | |
| if (messageArray.length > 0 && Array.isArray(messageArray[0])) { | |
| // Flatten nested array structure | |
| messageArray = messageArray[0]; | |
| } | |
| // Find the last user message | |
| for (let i = messageArray.length - 1; i >= 0; i--) { | |
| const msg = messageArray[i]; | |
| // Handle LangChain serialized format with kwargs | |
| if (msg.kwargs) { | |
| const msgType = msg.kwargs.type || ''; | |
| if (msgType === 'human' || msgType === 'user') { | |
| return msg.kwargs.content || msg.kwargs.text; | |
| } | |
| } | |
| // Check various message type formats (direct format) | |
| const msgType = msg.type || msg.role || ''; | |
| if (msgType === 'human' || msgType === 'user' || msgType === 'HumanMessage') { | |
| return msg.content || msg.text || msg.data?.content; | |
| } | |
| } | |
| // Fallback to last message | |
| if (messageArray.length > 0) { | |
| const lastMsg = messageArray[messageArray.length - 1]; | |
| // Try kwargs first | |
| if (lastMsg.kwargs?.content) return lastMsg.kwargs.content; | |
| return lastMsg.content || lastMsg.text || lastMsg.data?.content; | |
| } | |
| } | |
| // Try input field | |
| if (inputs.input) return inputs.input; | |
| // Try kwargs | |
| if (inputs.kwargs?.messages) { | |
| return extractPromptFromInputs(inputs.kwargs); | |
| } | |
| return null; | |
| } | |
| function extractPromptFromMessageData(messageData) { | |
| // Try multiple locations where prompt might be | |
| // 1. Try metadata.inputs | |
| let prompt = extractPromptFromInputs(messageData.metadata?.inputs); | |
| if (prompt) return prompt; | |
| // 2. Try metadata.prompt directly | |
| if (messageData.metadata?.prompt) return messageData.metadata.prompt; | |
| // 3. Try metadata.input | |
| if (messageData.metadata?.input) return messageData.metadata.input; | |
| // 4. Try inputs at top level | |
| if (messageData.inputs) { | |
| prompt = extractPromptFromInputs(messageData.inputs); | |
| if (prompt) return prompt; | |
| } | |
| // 5. Try data.inputs | |
| if (messageData.data?.inputs) { | |
| prompt = extractPromptFromInputs(messageData.data.inputs); | |
| if (prompt) return prompt; | |
| } | |
| // 6. Try to extract from the generation data itself | |
| if (messageData.data?.generations) { | |
| const gen = messageData.data.generations[0]?.[0]; | |
| if (gen?.inputs) { | |
| prompt = extractPromptFromInputs(gen.inputs); | |
| if (prompt) return prompt; | |
| } | |
| } | |
| return null; | |
| } | |
| function formatJsonWithSyntaxHighlight(obj) { | |
| const json = JSON.stringify(obj, null, 2); | |
| // Simple syntax highlighting by replacing patterns | |
| return json | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/("(?:\\.|[^"\\])*")\s*:/g, '<span class="json-key">$1</span>:') | |
| .replace(/:\s*("(?:\\.|[^"\\])*")/g, ': <span class="json-string">$1</span>') | |
| .replace(/:\s*(-?\d+\.?\d*)/g, ': <span class="json-number">$1</span>') | |
| .replace(/:\s*(true|false)/g, ': <span class="json-boolean">$1</span>') | |
| .replace(/:\s*(null)/g, ': <span class="json-null">$1</span>'); | |
| } | |
| async function renderImage(messageData) { | |
| // Debug logging | |
| console.log('Full messageData structure:', messageData); | |
| console.log('Metadata:', messageData.metadata); | |
| console.log('Inputs:', messageData.metadata?.inputs); | |
| // FIRST: Check if this is HTML content | |
| const htmlContent = extractHtmlContent(messageData.data); | |
| if (htmlContent) { | |
| console.log('✓ Rendering HTML content in iframe'); | |
| // Create a blob URL for the HTML content | |
| const blob = new Blob([htmlContent], { type: 'text/html' }); | |
| const blobUrl = URL.createObjectURL(blob); | |
| document.getElementById('output').innerHTML = ` | |
| <div style="width: 100%; height: calc(100vh - 8px); padding: 2px;"> | |
| <iframe | |
| src="${blobUrl}" | |
| style="width: 100%; height: 100%; border: 1px solid #e9ecef; border-radius: 4px;" | |
| sandbox="allow-scripts allow-same-origin" | |
| ></iframe> | |
| </div> | |
| `; | |
| return; | |
| } | |
| // SECOND: Try to extract image data | |
| const imageData = extractImageData(messageData.data); | |
| if (imageData && imageData.imageUrl) { | |
| // Render the image (existing code continues below) | |
| } else { | |
| // THIRD: Check for plain text content | |
| const textContent = extractTextContent(messageData.data); | |
| if (textContent && !htmlContent) { | |
| console.log('✓ Rendering plain text content'); | |
| const escapedText = textContent | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>'); | |
| document.getElementById('output').innerHTML = ` | |
| <div class="text-content"> | |
| <div class="text-content-header"> | |
| 📝 AI Output | |
| </div> | |
| <div class="text-content-body">${escapedText}</div> | |
| </div> | |
| `; | |
| return; | |
| } | |
| // FOURTH: Fall back to JSON display | |
| const formattedJson = formatJsonWithSyntaxHighlight(messageData); | |
| document.getElementById('output').innerHTML = ` | |
| <div class="json-fallback"> | |
| <div class="json-fallback-header"> | |
| ⚠️ No image, HTML, or text found in response - showing raw output data | |
| </div> | |
| <div class="json-container"> | |
| <pre>${formattedJson}</pre> | |
| </div> | |
| </div> | |
| `; | |
| return; | |
| } | |
| // Continue with image rendering if we got here | |
| const prompt = extractPromptFromMessageData(messageData); | |
| console.log('Extracted prompt:', prompt); | |
| const promptDisplay = prompt || 'No prompt available (check console for data structure)'; | |
| // If no prompt found, log where we looked | |
| if (!prompt) { | |
| console.warn('❌ Could not find prompt. Checked locations:', { | |
| 'metadata.inputs': messageData.metadata?.inputs, | |
| 'metadata.prompt': messageData.metadata?.prompt, | |
| 'metadata.input': messageData.metadata?.input, | |
| 'inputs': messageData.inputs, | |
| 'data.inputs': messageData.data?.inputs | |
| }); | |
| } | |
| const model = imageData.model || | |
| messageData.data?.model || | |
| messageData.metadata?.inputs?.model || | |
| messageData.metadata?.model || | |
| 'Unknown model'; | |
| const tokens = imageData.metadata?.tokens_used || | |
| imageData.metadata?.total_tokens || | |
| 'N/A'; | |
| const imageSize = imageData.imageUrl ? formatBytes(imageData.imageUrl.length) : 'N/A'; | |
| // Get image dimensions asynchronously | |
| const dimensions = await getImageDimensions(imageData.imageUrl); | |
| console.log('Image dimensions:', dimensions); | |
| let html = ''; | |
| // Image display | |
| html += ` | |
| <div class="image-container"> | |
| <img src="${imageData.imageUrl}" alt="Generated image" onerror="this.onerror=null; this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22400%22 height=%22300%22><rect width=%22400%22 height=%22300%22 fill=%22%23f0f0f0%22/><text x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 fill=%22%23999%22>Image failed to load</text></svg>';"> | |
| </div> | |
| `; | |
| // AI Description (if available) | |
| if (imageData.description) { | |
| html += ` | |
| <div class="description-section"> | |
| <h3>🤖 AI Description</h3> | |
| <div class="description-text">${imageData.description}</div> | |
| </div> | |
| `; | |
| } | |
| // Metadata | |
| html += ` | |
| <div class="metadata"> | |
| <div class="metadata-grid"> | |
| <div class="metadata-item"> | |
| <div class="label">Model</div> | |
| <div class="value">${getProviderBadge(model)}</div> | |
| </div> | |
| <div class="metadata-item"> | |
| <div class="label">Dimensions</div> | |
| <div class="value">${dimensions.formatted}</div> | |
| </div> | |
| <div class="metadata-item"> | |
| <div class="label">Tokens Used</div> | |
| <div class="value">${tokens}</div> | |
| </div> | |
| <div class="metadata-item"> | |
| <div class="label">Data Size</div> | |
| <div class="value">${imageSize}</div> | |
| </div> | |
| <div class="metadata-item"> | |
| <div class="label">Type</div> | |
| <div class="value">${messageData.type === 'reference' ? 'Reference' : 'Output'}</div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| document.getElementById('output').innerHTML = html; | |
| } | |
| // Listen for messages from LangSmith | |
| let messageReceived = false; | |
| window.addEventListener('message', async (event) => { | |
| if (messageReceived) return; // Only process first message | |
| console.log('Received message from LangSmith:', event.data); | |
| try { | |
| await renderImage(event.data); | |
| messageReceived = true; | |
| } catch (error) { | |
| console.error('Error rendering image:', error); | |
| document.getElementById('output').innerHTML = ` | |
| <div class="error"> | |
| <h3>⚠️ Rendering Error</h3> | |
| <p>${error.message}</p> | |
| <pre style="margin-top: 12px; font-size: 12px;">${error.stack}</pre> | |
| </div> | |
| `; | |
| } | |
| }); | |
| // Timeout after 10 seconds | |
| setTimeout(() => { | |
| if (!messageReceived) { | |
| document.getElementById('output').innerHTML = ` | |
| <div class="error"> | |
| <h3>⏱️ Timeout</h3> | |
| <p>No message received from LangSmith after 10 seconds.</p> | |
| <p style="margin-top: 8px; font-size: 14px;">Make sure this page is configured correctly in your LangSmith settings.</p> | |
| </div> | |
| `; | |
| } | |
| }, 10000); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment