Skip to content

Instantly share code, notes, and snippets.

@msabramo
Last active December 9, 2025 23:44
Show Gist options
  • Select an option

  • Save msabramo/af3e7ecb44cd762fca80ba5911fa2d1b to your computer and use it in GitHub Desktop.

Select an option

Save msabramo/af3e7ecb44cd762fca80ba5911fa2d1b to your computer and use it in GitHub Desktop.
<!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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
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