diff --git a/.gitignore b/.gitignore index 4be981783..4c77ec130 100644 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1,21 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -*/node_modules -docs/node_modules -/packages/**/node_modules +/node_modules /.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions +.pnp.js +package-lock.json +*package* # testing /coverage # next.js /.next/ -sim/.next/ -sim/out/ -sim/build -docs/.next/ -docs/out/ -docs/build +/out/ # production /build -/dist -**/dist/ -**/standalone/ -sim-standalone.tar.gz # misc .DS_Store @@ -39,15 +25,10 @@ sim-standalone.tar.gz npm-debug.log* yarn-debug.log* yarn-error.log* -.pnpm-debug.log* -# env files +# local env files +.env*.local .env -*.env -.env.local -.env.development -.env.test -.env.production # vercel .vercel @@ -62,4 +43,6 @@ next-env.d.ts # docs docs/.source docs/.contentlayer -docs/.content-collections \ No newline at end of file +docs/.content-collections +.qodo + diff --git a/sim/app/api/proxy-image/route.ts b/sim/app/api/proxy-image/route.ts new file mode 100644 index 000000000..f6c24e5b6 --- /dev/null +++ b/sim/app/api/proxy-image/route.ts @@ -0,0 +1,62 @@ +import { NextResponse } from 'next/server' + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const imageUrl = searchParams.get('url') + + if (!imageUrl) { + console.error('Missing URL parameter in proxy-image request') + return new NextResponse('Missing URL parameter', { status: 400 }) + } + + console.log('Proxying image from:', imageUrl) + + // Add appropriate headers for fetching images + const response = await fetch(imageUrl, { + headers: { + 'Accept': 'image/*, */*', + 'User-Agent': 'Mozilla/5.0 (compatible; ImageProxyBot/1.0)', + }, + // Set a reasonable timeout + signal: AbortSignal.timeout(15000), + }) + + if (!response.ok) { + console.error(`Failed to fetch image from ${imageUrl}:`, response.status, response.statusText) + return new NextResponse(`Failed to fetch image: ${response.status} ${response.statusText}`, { + status: response.status + }) + } + + const contentType = response.headers.get('content-type') + console.log('Image content-type:', contentType) + + const blob = await response.blob() + console.log('Image size:', blob.size, 'bytes') + + if (blob.size === 0) { + console.error('Empty image received from source URL') + return new NextResponse('Empty image received from source', { status: 422 }) + } + + // Return the image with appropriate headers + return new NextResponse(blob, { + headers: { + 'Content-Type': contentType || 'image/png', + 'Cache-Control': 'public, max-age=31536000', // Cache for a year + 'Access-Control-Allow-Origin': '*', // CORS support + 'X-Content-Type-Options': 'nosniff', + }, + }) + } catch (error) { + // Log the full error for debugging + console.error('Error proxying image:', error) + + // Return a helpful error response + return new NextResponse( + `Internal Server Error: ${error instanceof Error ? error.message : 'Unknown error'}`, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/sim/app/w/[id]/components/panel/components/console/components/json-view/json-view.tsx b/sim/app/w/[id]/components/panel/components/console/components/json-view/json-view.tsx index 98891dd55..b99aa0d58 100644 --- a/sim/app/w/[id]/components/panel/components/console/components/json-view/json-view.tsx +++ b/sim/app/w/[id]/components/panel/components/console/components/json-view/json-view.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { Download, Image as ImageIcon } from 'lucide-react' import { Button } from '@/components/ui/button' interface JSONViewProps { @@ -39,6 +40,136 @@ const copyToClipboard = (data: any) => { navigator.clipboard.writeText(stringified) } +// Helper function to check if an object contains an image URL +const isImageData = (obj: any): boolean => { + return obj && typeof obj === 'object' && 'url' in obj && typeof obj.url === 'string' +} + +// Helper function to check if a string is likely a base64 image +const isBase64Image = (str: string): boolean => { + if (typeof str !== 'string') return false + // Check if it's a reasonably long string that could be a base64 image + // and contains only valid base64 characters + return str.length > 100 && /^[A-Za-z0-9+/=]+$/.test(str) +} + +// Check if this is a response with the new image structure +// Modified to be more flexible - content must be a URL, image is optional +const hasImageContent = (obj: any): boolean => { + return ( + obj && + typeof obj === 'object' && + 'content' in obj && + typeof obj.content === 'string' && + 'metadata' in obj && + typeof obj.metadata === 'object' && + // Either has valid image data + (('image' in obj && typeof obj.image === 'string') || + // Or at least has content that looks like a URL (more permissive check) + obj.content.startsWith('http')) + ) +} + +// Image preview component with support for both URL and base64 +const ImagePreview = ({ + imageUrl, + imageData, + isBase64 = false, +}: { + imageUrl?: string + imageData?: string + isBase64?: boolean +}) => { + const downloadImage = async () => { + try { + let blob: Blob + if (isBase64 && imageData && imageData.length > 0) { + // Convert base64 to blob + const byteString = atob(imageData) + const arrayBuffer = new ArrayBuffer(byteString.length) + const uint8Array = new Uint8Array(arrayBuffer) + for (let i = 0; i < byteString.length; i++) { + uint8Array[i] = byteString.charCodeAt(i) + } + blob = new Blob([arrayBuffer], { type: 'image/png' }) + } else if (imageUrl && imageUrl.length > 0) { + // Use proxy endpoint to fetch image + const proxyUrl = `/api/proxy-image?url=${encodeURIComponent(imageUrl)}` + const response = await fetch(proxyUrl) + if (!response.ok) { + throw new Error(`Failed to download image: ${response.statusText}`) + } + blob = await response.blob() + } else { + throw new Error('No image data or URL provided') + } + + // Create object URL and trigger download + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `generated-image-${Date.now()}.png` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + // Clean up the URL + setTimeout(() => URL.revokeObjectURL(url), 100) + } catch (error) { + console.error('Error downloading image:', error) + alert('Failed to download image. Please try again later.') + } + } + + // Only display image if we have valid data + const hasValidData = + (isBase64 && imageData && imageData.length > 0) || (imageUrl && imageUrl.length > 0) + + if (!hasValidData) { + return
Image data unavailable
+ } + + // Determine the source for the image + const imageSrc = + isBase64 && imageData && imageData.length > 0 + ? `data:image/png;base64,${imageData}` + : imageUrl || '' + + return ( +
+ Generated image { + console.error('Image failed to load:', imageSrc) + e.currentTarget.alt = 'Failed to load image' + e.currentTarget.style.height = '100px' + e.currentTarget.style.width = '100%' + e.currentTarget.style.display = 'flex' + e.currentTarget.style.alignItems = 'center' + e.currentTarget.style.justifyContent = 'center' + e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.1)' + }} + /> +
+ +
+
+ ) +} + export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONViewProps) => { const [isCollapsed, setIsCollapsed] = useState(!initiallyExpanded) const [contextMenuPosition, setContextMenuPosition] = useState<{ @@ -63,7 +194,54 @@ export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONVie } }, [contextMenuPosition]) + // Check if this is a base64 image string + const isBase64ImageString = typeof data === 'string' && isBase64Image(data) + + // Check if current object contains image URL + const hasImageUrl = isImageData(data) + + // Check if this is a response object with the new image format + const isResponseWithImage = hasImageContent(data) + + // Check if this is response.output with the new image structure + const isToolResponseWithImage = + data && typeof data === 'object' && data.output && hasImageContent(data.output) + if (data === null) return null + + // Handle base64 image strings directly + if (isBase64ImageString) { + return ( +
+ + {contextMenuPosition && ( +
+ + +
+ )} +
+ ) + } + if (typeof data !== 'object') { const stringValue = JSON.stringify(data) return ( @@ -89,6 +267,219 @@ export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONVie ) } + // Handle objects that have the new image structure + if (isResponseWithImage) { + // Get the URL from content field since that's where it should be + const imageUrl = data.content && typeof data.content === 'string' ? data.content : undefined + // Check if we have valid image data + const hasValidImage = data.image && typeof data.image === 'string' && data.image.length > 0 + + return ( +
+ { + e.stopPropagation() + setIsCollapsed(!isCollapsed) + }} + > + {isCollapsed ? '▶' : '▼'} + {'{'} + {isCollapsed ? '...' : ''} + + + {!isCollapsed && ( +
+ {Object.entries(data).map(([key, value], index) => { + const isImageKey = key === 'image' + + return ( +
+ {key}:{' '} + {isImageKey ? ( +
+ + {hasValidImage && typeof value === 'string' && value.length > 100 ? ( + + ) : ( + '""' + )} + + {/* Show image preview within the image field */} + +
+ ) : ( + + )} + {index < Object.entries(data).length - 1 && ','} +
+ ) + })} +
+ )} + + {contextMenuPosition && ( +
+ + +
+ )} + + {'}'} +
+ ) + } + + // Handle tool response objects with the new image structure in output + if (isToolResponseWithImage) { + const outputData = data.output || {} + const imageUrl = + outputData.content && typeof outputData.content === 'string' ? outputData.content : undefined + const hasValidImage = + outputData.image && typeof outputData.image === 'string' && outputData.image.length > 0 + + return ( +
+ { + e.stopPropagation() + setIsCollapsed(!isCollapsed) + }} + > + {isCollapsed ? '▶' : '▼'} + {'{'} + {isCollapsed ? '...' : ''} + + + {!isCollapsed && ( +
+ {Object.entries(data).map(([key, value]: [string, any], index) => { + const isOutputKey = key === 'output' + + return ( +
+ {key}:{' '} + {isOutputKey ? ( +
+ { + e.stopPropagation() + const nestedElem = e.currentTarget.nextElementSibling + if (nestedElem) { + nestedElem.classList.toggle('hidden') + } + }} + > + + {'{'} + +
+ {Object.entries(value).map( + ([outputKey, outputValue]: [string, any], idx) => { + const isImageSubKey = outputKey === 'image' + + return ( +
+ {outputKey}:{' '} + {isImageSubKey ? ( +
+ + {hasValidImage && outputValue.length > 100 ? ( + + ) : ( + '""' + )} + + {/* Show image preview within nested image field */} + +
+ ) : ( + + )} + {idx < Object.entries(value).length - 1 && ','} +
+ ) + } + )} +
+ {'}'} +
+ ) : ( + + )} + {index < Object.entries(data).length - 1 && ','} +
+ ) + })} +
+ )} + + {contextMenuPosition && ( +
+ + +
+ )} + + {'}'} +
+ ) + } + const isArray = Array.isArray(data) const items = isArray ? data : Object.entries(data) const isEmpty = items.length === 0 @@ -110,6 +501,10 @@ export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONVie {isArray ? '[' : '{'} {isCollapsed ? '...' : ''} + + {/* Direct image render for objects with image URLs */} + {!isCollapsed && hasImageUrl && } + {contextMenuPosition && (
Copy object + {hasImageUrl && ( + + )}
)} + {!isCollapsed && (
{isArray @@ -132,13 +541,25 @@ export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONVie {index < items.length - 1 && ','}
)) - : (items as [string, any][]).map(([key, value], index) => ( -
- {key}:{' '} - - {index < items.length - 1 && ','} -
- ))} + : (items as [string, any][]).map(([key, value], index) => { + // Handle the case where we have content (URL) and image (base64) fields + const isImageField = + key === 'image' && typeof value === 'string' && value.length > 100 + + return ( +
+ {key}:{' '} + {isImageField ? ( + + + + ) : ( + + )} + {index < items.length - 1 && ','} +
+ ) + })} )} {isArray ? ']' : '}'} diff --git a/sim/blocks/blocks/image-generator.ts b/sim/blocks/blocks/image-generator.ts new file mode 100644 index 000000000..93245d603 --- /dev/null +++ b/sim/blocks/blocks/image-generator.ts @@ -0,0 +1,128 @@ +import { AirplaneIcon, ImageIcon } from '@/components/icons' +import { DalleResponse } from '@/tools/openai/dalle' +import { BlockConfig } from '../types' + +export const ImageGeneratorBlock: BlockConfig = { + type: 'image_generator', + name: 'Image Generator', + description: 'Generate images', + longDescription: + 'Create high-quality images using DALL-E. Configure resolution, quality, style, and other parameters to get exactly the image you need.', + category: 'tools', + bgColor: '#FF6B6B', + icon: ImageIcon, + subBlocks: [ + { + id: 'provider', + title: 'Provider', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'DALL-E', id: 'dalle' }, + ], + value: () => 'dalle', + }, + { + id: 'prompt', + title: 'Prompt', + type: 'long-input', + layout: 'full', + placeholder: 'Describe the image you want to generate...', + }, + { + id: 'model', + title: 'Model', + type: 'dropdown', + layout: 'half', + options: [ + { label: 'DALL-E 2', id: 'dall-e-2' }, + { label: 'DALL-E 3', id: 'dall-e-3' }, + ], + value: () => 'dall-e-3', + }, + { + id: 'size', + title: 'Size', + type: 'dropdown', + layout: 'half', + options: [ + { label: '1024x1024', id: '1024x1024' }, + { label: '1024x1792', id: '1024x1792' }, + { label: '1792x1024', id: '1792x1024' }, + ], + value: () => '1024x1024', + }, + { + id: 'quality', + title: 'Quality', + type: 'dropdown', + layout: 'half', + options: [ + { label: 'Standard', id: 'standard' }, + { label: 'HD', id: 'hd' }, + ], + value: () => 'standard', + }, + { + id: 'style', + title: 'Style', + type: 'dropdown', + layout: 'half', + options: [ + { label: 'Vivid', id: 'vivid' }, + { label: 'Natural', id: 'natural' }, + ], + value: () => 'vivid', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your OpenAI API key', + password: true, + connectionDroppable: false, + }, + ], + tools: { + access: ['dalle_generate'], + config: { + tool: () => 'dalle_generate', + params: (params) => { + if (!params.apiKey) { + throw new Error('API key is required') + } + if (!params.prompt) { + throw new Error('Prompt is required') + } + + return { + prompt: params.prompt, + model: params.model || 'dall-e-3', + size: params.size || '1024x1024', + quality: params.quality || 'standard', + style: params.style || 'vivid', + apiKey: params.apiKey, + } + }, + }, + }, + inputs: { + provider: { type: 'string', required: true }, + prompt: { type: 'string', required: true }, + model: { type: 'string', required: true }, + size: { type: 'string', required: false }, + quality: { type: 'string', required: false }, + style: { type: 'string', required: false }, + apiKey: { type: 'string', required: true }, + }, + outputs: { + response: { + type: { + content: 'string', // URL of the generated image + image: 'string', // Base64 image data + metadata: 'json' // Contains only model information + } + }, + }, +} \ No newline at end of file diff --git a/sim/blocks/index.ts b/sim/blocks/index.ts index 6e92328ff..04fbc77a6 100644 --- a/sim/blocks/index.ts +++ b/sim/blocks/index.ts @@ -12,6 +12,7 @@ import { FunctionBlock } from './blocks/function' import { GitHubBlock } from './blocks/github' import { GmailBlock } from './blocks/gmail' import { GuestyBlock } from './blocks/guesty' +import { ImageGeneratorBlock } from './blocks/image-generator' import { JinaBlock } from './blocks/jina' import { NotionBlock } from './blocks/notion' import { OpenAIBlock } from './blocks/openai' @@ -65,6 +66,7 @@ export { GoogleSheetsBlock, PerplexityBlock, ConfluenceBlock, + ImageGeneratorBlock, } // Registry of all block configurations, alphabetically sorted @@ -83,6 +85,7 @@ const blocks: Record = { google_drive: GoogleDriveBlock, google_sheets: GoogleSheetsBlock, // guesty: GuestyBlock, + image_generator: ImageGeneratorBlock, jina: JinaBlock, notion: NotionBlock, openai: OpenAIBlock, diff --git a/sim/blocks/types.ts b/sim/blocks/types.ts index 33e332dbd..24c5e958f 100644 --- a/sim/blocks/types.ts +++ b/sim/blocks/types.ts @@ -137,6 +137,10 @@ export interface BlockConfig { whenFilled: 'json' } } + visualization?: { + type: 'image' + url: string + } } } } diff --git a/sim/components/icons.tsx b/sim/components/icons.tsx index 1310ddf67..02df29a38 100644 --- a/sim/components/icons.tsx +++ b/sim/components/icons.tsx @@ -1735,3 +1735,24 @@ export function ConfluenceIcon(props: SVGProps) { ) } + +export function ImageIcon(props: SVGProps) { + return ( + + + + + + ) +} \ No newline at end of file diff --git a/sim/executor/index.ts b/sim/executor/index.ts index 811843e1b..c77226c59 100644 --- a/sim/executor/index.ts +++ b/sim/executor/index.ts @@ -822,61 +822,35 @@ export class Executor { * @returns A meaningful error message string */ private extractErrorMessage(error: any): string { - if (!error) return 'Unknown error occurred' - - // Handle Error instances - if (error instanceof Error) { - return error.message || `Error: ${String(error)}` - } - - // Handle string errors + // If it's already a string, return it if (typeof error === 'string') { return error } - // Handle object errors with nested structure - if (typeof error === 'object') { - // Case: { error: { message: "msg" } } - if (error.error && typeof error.error === 'object' && error.error.message) { - return error.error.message - } - - // Case: { error: "msg" } - if (error.error && typeof error.error === 'string') { - return error.error - } - - // Case: { message: "msg" } - if (error.message) { - return error.message - } - - // Add specific handling for HTTP errors - if (error.status || error.request) { - let message = 'API request failed' - - // Add URL information if available - if (error.request && error.request.url) { - message += `: ${error.request.url}` - } - - // Add status code if available - if (error.status) { - message += ` (Status: ${error.status})` - } - - return message - } - - // Last resort: try to stringify the object - try { - return `Error details: ${JSON.stringify(error)}` - } catch { - return 'Error occurred but details could not be displayed' - } + // If it has a message property, use that + if (error.message) { + return error.message } - return 'Unknown error occurred' + // If it's an object with response data, include that + if (error.response?.data) { + const data = error.response.data + if (typeof data === 'string') { + return data + } + if (data.message) { + return data.message + } + return JSON.stringify(data) + } + + // If it's an object, stringify it + if (typeof error === 'object') { + return JSON.stringify(error) + } + + // Fallback to string conversion + return String(error) } /** @@ -887,44 +861,34 @@ export class Executor { * @returns A sanitized version of the error for logging */ private sanitizeError(error: any): any { - if (!error) return { message: 'No error details available' } - - // Handle Error instances - if (error instanceof Error) { - return { - message: error.message || 'Error without message', - stack: error.stack, - } - } - - // Handle string errors + // If it's already a string, return it if (typeof error === 'string') { - return { message: error } - } - - // Handle object errors with nested structure - if (typeof error === 'object') { - // If error has a nested error object with undefined message, fix it - if (error.error && typeof error.error === 'object') { - if (!error.error.message) { - error.error.message = 'No specific error message provided' - } - } - - // If no message property exists at root level, add one - if (!error.message) { - if (error.error && typeof error.error === 'string') { - error.message = error.error - } else if (error.status) { - error.message = `API request failed with status ${error.status}` - } else { - error.message = 'Error occurred during workflow execution' - } - } - return error } - return { message: `Unexpected error type: ${typeof error}` } + // If it has a message property, return that + if (error.message) { + return error.message + } + + // If it's an object with response data, include that + if (error.response?.data) { + const data = error.response.data + if (typeof data === 'string') { + return data + } + if (data.message) { + return data.message + } + return JSON.stringify(data) + } + + // If it's an object, stringify it + if (typeof error === 'object') { + return JSON.stringify(error) + } + + // Fallback to string conversion + return String(error) } } diff --git a/sim/next.config.ts b/sim/next.config.ts index 7966c227a..75055fb89 100644 --- a/sim/next.config.ts +++ b/sim/next.config.ts @@ -6,7 +6,11 @@ const isStandaloneBuild = process.env.USE_LOCAL_STORAGE === 'true' const nextConfig: NextConfig = { devIndicators: false, images: { - domains: ['avatars.githubusercontent.com'], + domains: [ + 'avatars.githubusercontent.com', + 'oaidalleapiprodscus.blob.core.windows.net', + 'api.stability.ai', + ], // Enable static image optimization for standalone export unoptimized: isStandaloneBuild, }, diff --git a/sim/tools/index.ts b/sim/tools/index.ts index 1c40c82fd..ac1f4d74d 100644 --- a/sim/tools/index.ts +++ b/sim/tools/index.ts @@ -19,6 +19,7 @@ import { requestTool as httpRequest } from './http/request' import { contactsTool as hubspotContacts } from './hubspot/contacts' import { readUrlTool } from './jina/reader' import { notionReadTool, notionWriteTool } from './notion' +import { dalleTool } from './openai/dalle' import { embeddingsTool as openAIEmbeddings } from './openai/embeddings' import { perplexityChatTool } from './perplexity' import { @@ -102,6 +103,7 @@ export const tools: Record = { confluence_retrieve: confluenceRetrieveTool, confluence_list: confluenceListTool, confluence_update: confluenceUpdateTool, + dalle_generate: dalleTool, } // Get a tool by its ID diff --git a/sim/tools/openai/dalle.ts b/sim/tools/openai/dalle.ts new file mode 100644 index 000000000..ae884fb38 --- /dev/null +++ b/sim/tools/openai/dalle.ts @@ -0,0 +1,238 @@ +import { ToolConfig, ToolResponse } from '../types' + +export interface DalleResponse extends ToolResponse { + output: { + content: string // This will now be the image URL + image: string // This will be the base64 image data + metadata: { + model: string // Only contains model name now + } + } +} + +export const dalleTool: ToolConfig = { + id: 'dalle_generate', + name: 'DALL-E Generate', + description: 'Generate images using OpenAI\'s DALL-E model', + version: '1.0.0', + params: { + prompt: { + type: 'string', + required: true, + description: 'A text description of the desired image(s)', + }, + model: { + type: 'string', + required: true, + description: 'The DALL-E model to use (dall-e-2 or dall-e-3)', + }, + size: { + type: 'string', + required: false, + description: 'The size of the generated images (1024x1024, 1024x1792, or 1792x1024)', + }, + quality: { + type: 'string', + required: false, + description: 'The quality of the image (standard or hd)', + }, + style: { + type: 'string', + required: false, + description: 'The style of the image (vivid or natural)', + }, + n: { + type: 'number', + required: false, + description: 'The number of images to generate (1-10)', + }, + apiKey: { + type: 'string', + required: true, + description: 'Your OpenAI API key', + }, + }, + request: { + url: 'https://api.openai.com/v1/images/generations', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => ({ + model: params.model, + prompt: params.prompt, + size: params.size || '1024x1024', + quality: params.quality || 'standard', + style: params.style || 'vivid', + n: params.n || 1, + }), + }, + transformResponse: async (response, params) => { + try { + const data = await response.json() + + console.log('DALL-E API response:', JSON.stringify(data, null, 2)) + + if (!data.data?.[0]?.url) { + console.error('No image URL in DALL-E response:', data) + throw new Error('No image URL in response') + } + + const imageUrl = data.data[0].url + const modelName = data.model || params?.model || 'dall-e' + + console.log('Got image URL:', imageUrl) + console.log('Using model:', modelName) + + try { + // Fetch the image using the proxy-image endpoint instead of direct fetch + console.log('Fetching image from URL via proxy...') + const proxyUrl = `/api/proxy-image?url=${encodeURIComponent(imageUrl)}` + + const imageResponse = await fetch(proxyUrl, { + headers: { + 'Accept': 'image/*, */*', + }, + cache: 'no-store', // Don't use cache + }) + + if (!imageResponse.ok) { + console.error('Failed to fetch image:', imageResponse.status, imageResponse.statusText) + throw new Error(`Failed to fetch image: ${imageResponse.statusText}`) + } + + console.log('Image fetch successful, content-type:', imageResponse.headers.get('content-type')) + + const imageBlob = await imageResponse.blob() + console.log('Image blob size:', imageBlob.size) + + if (imageBlob.size === 0) { + console.error('Empty image blob received') + throw new Error('Empty image received') + } + + const reader = new FileReader() + const base64Promise = new Promise((resolve, reject) => { + reader.onloadend = () => { + try { + const base64data = reader.result as string + if (!base64data) { + reject(new Error('No data read from image')) + return + } + + const base64Content = base64data.split(',')[1] // Remove the data URL prefix + console.log('Successfully converted image to base64, length:', base64Content.length) + resolve(base64Content) + } catch (err) { + console.error('Error in FileReader onloadend:', err) + reject(err) + } + } + reader.onerror = (err) => { + console.error('FileReader error:', err) + reject(new Error('Failed to read image data')) + } + reader.readAsDataURL(imageBlob) + }) + + const base64Image = await base64Promise + + console.log('Returning success response with image data') + return { + success: true, + output: { + content: imageUrl, // Now using image URL as content + image: base64Image, // Base64 image in separate field + metadata: { + model: modelName, // Only include model name in metadata + }, + }, + } + } catch (error) { + // Log the error but continue with returning the URL + console.error('Error fetching or processing image:', error) + + // Try again with a direct browser fetch as fallback + try { + console.log('Attempting fallback with direct browser fetch...') + const directImageResponse = await fetch(imageUrl, { + cache: 'no-store', + headers: { + 'Accept': 'image/*, */*', + 'User-Agent': 'Mozilla/5.0 (compatible; DalleProxy/1.0)', + }, + }) + + if (!directImageResponse.ok) { + throw new Error(`Direct fetch failed: ${directImageResponse.status}`) + } + + const imageBlob = await directImageResponse.blob() + if (imageBlob.size === 0) { + throw new Error('Empty blob received from direct fetch') + } + + const reader = new FileReader() + const base64Promise = new Promise((resolve, reject) => { + reader.onloadend = () => { + try { + const base64data = reader.result as string + if (!base64data) { + reject(new Error('No data read from image')) + return + } + + const base64Content = base64data.split(',')[1] + console.log('Successfully converted image to base64 via direct fetch, length:', base64Content.length) + resolve(base64Content) + } catch (err) { + reject(err) + } + } + reader.onerror = reject + reader.readAsDataURL(imageBlob) + }) + + const base64Image = await base64Promise + + return { + success: true, + output: { + content: imageUrl, + image: base64Image, + metadata: { + model: modelName, + }, + }, + } + } catch (fallbackError) { + console.error('Fallback fetch also failed:', fallbackError) + + // Even if both attempts fail, still return the URL and metadata + return { + success: true, + output: { + content: imageUrl, // URL as content + image: '', // Empty image since we couldn't get it + metadata: { + model: modelName, + }, + }, + } + } + } + } catch (error) { + console.error('Error in DALL-E response handling:', error) + throw error; + } + }, + transformError: (error) => { + console.error('DALL-E error:', error) + if (error.response?.data?.error?.message) { + return error.response.data.error.message + } + return error.message || 'Failed to generate image with DALL-E' + }, +} \ No newline at end of file