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 (
+
+

{
+ 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