feat(image-gen) (#199)

* feat: image generation model with dalle

* updates to image-gen-block

* improvements: image-gen

---------

Co-authored-by: Rishabh Shinde <rishabhsshinde27@gail.com>
This commit is contained in:
Emir Karabeg
2025-03-27 19:58:35 -07:00
committed by GitHub
co-authored by Rishabh Shinde
parent 56bec10749
commit 4eb0f5f629
11 changed files with 949 additions and 119 deletions
+10 -27
View File
@@ -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
docs/.content-collections
.qodo
+62
View File
@@ -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 }
)
}
}
@@ -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 <div className="my-2 text-muted-foreground">Image data unavailable</div>
}
// Determine the source for the image
const imageSrc =
isBase64 && imageData && imageData.length > 0
? `data:image/png;base64,${imageData}`
: imageUrl || ''
return (
<div className="my-2 relative group">
<img
src={imageSrc}
alt="Generated image"
className="max-w-full h-auto rounded-md border"
onError={(e) => {
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)'
}}
/>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="secondary"
size="icon"
className="h-8 w-8 bg-background/80 backdrop-blur-sm"
onClick={(e) => {
e.stopPropagation()
downloadImage()
}}
>
<Download className="h-4 w-4" />
<span className="sr-only">Download image</span>
</Button>
</div>
</div>
)
}
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 <span className="text-muted-foreground">null</span>
// Handle base64 image strings directly
if (isBase64ImageString) {
return (
<div onContextMenu={handleContextMenu}>
<ImagePreview imageData={data} isBase64={true} />
{contextMenuPosition && (
<div
className="fixed z-50 bg-popover border rounded-md shadow-md py-1 min-w-[160px]"
style={{ left: contextMenuPosition.x, top: contextMenuPosition.y }}
>
<button
className="w-full px-3 py-1.5 text-sm text-left hover:bg-accent"
onClick={() => copyToClipboard(data)}
>
Copy base64 string
</button>
<button
className="w-full px-3 py-1.5 text-sm text-left hover:bg-accent flex items-center gap-2"
onClick={() => {
document
.querySelector<HTMLButtonElement>('.group .bg-background\\/80 button')
?.click()
}}
>
<Download className="h-4 w-4" />
Download image
</button>
</div>
)}
</div>
)
}
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 (
<div className="relative" onContextMenu={handleContextMenu}>
<span
className="cursor-pointer select-none inline-flex items-center text-muted-foreground"
onClick={(e) => {
e.stopPropagation()
setIsCollapsed(!isCollapsed)
}}
>
<span className="text-xs leading-none mr-1">{isCollapsed ? '▶' : '▼'}</span>
<span>{'{'}</span>
{isCollapsed ? '...' : ''}
</span>
{!isCollapsed && (
<div className="ml-4 break-words">
{Object.entries(data).map(([key, value], index) => {
const isImageKey = key === 'image'
return (
<div key={key} className="break-all">
<span className="text-muted-foreground">{key}</span>:{' '}
{isImageKey ? (
<div>
<span className="text-success inline-block mb-2">
{hasValidImage && typeof value === 'string' && value.length > 100 ? (
<TruncatedValue value={JSON.stringify('[base64 image data]')} />
) : (
'""'
)}
</span>
{/* Show image preview within the image field */}
<ImagePreview
imageUrl={imageUrl}
imageData={
hasValidImage && isBase64Image(data.image) ? data.image : undefined
}
isBase64={hasValidImage && isBase64Image(data.image)}
/>
</div>
) : (
<JSONView data={value} level={level + 1} />
)}
{index < Object.entries(data).length - 1 && ','}
</div>
)
})}
</div>
)}
{contextMenuPosition && (
<div
className="fixed z-50 bg-popover border rounded-md shadow-md py-1 min-w-[160px]"
style={{ left: contextMenuPosition.x, top: contextMenuPosition.y }}
>
<button
className="w-full px-3 py-1.5 text-sm text-left hover:bg-accent"
onClick={() => copyToClipboard(data)}
>
Copy object
</button>
<button
className="w-full px-3 py-1.5 text-sm text-left hover:bg-accent flex items-center gap-2"
onClick={() => {
document
.querySelector<HTMLButtonElement>('.group .bg-background\\/80 button')
?.click()
}}
>
<Download className="h-4 w-4" />
Download image
</button>
</div>
)}
<span className="text-muted-foreground">{'}'}</span>
</div>
)
}
// 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 (
<div className="relative" onContextMenu={handleContextMenu}>
<span
className="cursor-pointer select-none inline-flex items-center text-muted-foreground"
onClick={(e) => {
e.stopPropagation()
setIsCollapsed(!isCollapsed)
}}
>
<span className="text-xs leading-none mr-1">{isCollapsed ? '▶' : '▼'}</span>
<span>{'{'}</span>
{isCollapsed ? '...' : ''}
</span>
{!isCollapsed && (
<div className="ml-4 break-words">
{Object.entries(data).map(([key, value]: [string, any], index) => {
const isOutputKey = key === 'output'
return (
<div key={key} className="break-all">
<span className="text-muted-foreground">{key}</span>:{' '}
{isOutputKey ? (
<div className="relative">
<span
className="cursor-pointer select-none inline-flex items-center text-muted-foreground"
onClick={(e) => {
e.stopPropagation()
const nestedElem = e.currentTarget.nextElementSibling
if (nestedElem) {
nestedElem.classList.toggle('hidden')
}
}}
>
<span className="text-xs leading-none mr-1">▼</span>
<span>{'{'}</span>
</span>
<div className="ml-4 break-words">
{Object.entries(value).map(
([outputKey, outputValue]: [string, any], idx) => {
const isImageSubKey = outputKey === 'image'
return (
<div key={outputKey} className="break-all">
<span className="text-muted-foreground">{outputKey}</span>:{' '}
{isImageSubKey ? (
<div>
<span className="text-success inline-block mb-2">
{hasValidImage && outputValue.length > 100 ? (
<TruncatedValue
value={JSON.stringify('[base64 image data]')}
/>
) : (
'""'
)}
</span>
{/* Show image preview within nested image field */}
<ImagePreview
imageUrl={imageUrl}
imageData={
hasValidImage && isBase64Image(outputValue)
? outputValue
: undefined
}
isBase64={hasValidImage && isBase64Image(outputValue)}
/>
</div>
) : (
<JSONView data={outputValue} level={level + 2} />
)}
{idx < Object.entries(value).length - 1 && ','}
</div>
)
}
)}
</div>
<span className="text-muted-foreground">{'}'}</span>
</div>
) : (
<JSONView data={value} level={level + 1} />
)}
{index < Object.entries(data).length - 1 && ','}
</div>
)
})}
</div>
)}
{contextMenuPosition && (
<div
className="fixed z-50 bg-popover border rounded-md shadow-md py-1 min-w-[160px]"
style={{ left: contextMenuPosition.x, top: contextMenuPosition.y }}
>
<button
className="w-full px-3 py-1.5 text-sm text-left hover:bg-accent"
onClick={() => copyToClipboard(data)}
>
Copy object
</button>
<button
className="w-full px-3 py-1.5 text-sm text-left hover:bg-accent flex items-center gap-2"
onClick={() => {
document
.querySelector<HTMLButtonElement>('.group .bg-background\\/80 button')
?.click()
}}
>
<Download className="h-4 w-4" />
Download image
</button>
</div>
)}
<span className="text-muted-foreground">{'}'}</span>
</div>
)
}
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
<span>{isArray ? '[' : '{'}</span>
{isCollapsed ? '...' : ''}
</span>
{/* Direct image render for objects with image URLs */}
{!isCollapsed && hasImageUrl && <ImagePreview imageUrl={data.url} />}
{contextMenuPosition && (
<div
className="fixed z-50 bg-popover border rounded-md shadow-md py-1 min-w-[160px]"
@@ -121,8 +516,22 @@ export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONVie
>
Copy object
</button>
{hasImageUrl && (
<button
className="w-full px-3 py-1.5 text-sm text-left hover:bg-accent flex items-center gap-2"
onClick={() => {
document
.querySelector<HTMLButtonElement>('.group .bg-background\\/80 button')
?.click()
}}
>
<Download className="h-4 w-4" />
Download image
</button>
)}
</div>
)}
{!isCollapsed && (
<div className="ml-4 break-words">
{isArray
@@ -132,13 +541,25 @@ export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONVie
{index < items.length - 1 && ','}
</div>
))
: (items as [string, any][]).map(([key, value], index) => (
<div key={key} className="break-all">
<span className="text-muted-foreground">{key}</span>:{' '}
<JSONView data={value} level={level + 1} />
{index < items.length - 1 && ','}
</div>
))}
: (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 (
<div key={key} className="break-all">
<span className="text-muted-foreground">{key}</span>:{' '}
{isImageField ? (
<span className="text-success">
<TruncatedValue value={JSON.stringify('[base64 image data]')} />
</span>
) : (
<JSONView data={value} level={level + 1} />
)}
{index < items.length - 1 && ','}
</div>
)
})}
</div>
)}
<span className="text-muted-foreground">{isArray ? ']' : '}'}</span>
+128
View File
@@ -0,0 +1,128 @@
import { AirplaneIcon, ImageIcon } from '@/components/icons'
import { DalleResponse } from '@/tools/openai/dalle'
import { BlockConfig } from '../types'
export const ImageGeneratorBlock: BlockConfig<DalleResponse> = {
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
}
},
},
}
+3
View File
@@ -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<string, BlockConfig> = {
google_drive: GoogleDriveBlock,
google_sheets: GoogleSheetsBlock,
// guesty: GuestyBlock,
image_generator: ImageGeneratorBlock,
jina: JinaBlock,
notion: NotionBlock,
openai: OpenAIBlock,
+4
View File
@@ -137,6 +137,10 @@ export interface BlockConfig<T extends ToolResponse = ToolResponse> {
whenFilled: 'json'
}
}
visualization?: {
type: 'image'
url: string
}
}
}
}
+21
View File
@@ -1735,3 +1735,24 @@ export function ConfluenceIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function ImageIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="3" width="22" height="22" rx="2" />
<circle cx="9" cy="9" r="2" />
<path d="M3 21L10 14L14 18L21 11L25 15V21" />
</svg>
)
}
+48 -84
View File
@@ -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)
}
}
+5 -1
View File
@@ -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,
},
+2
View File
@@ -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<string, ToolConfig> = {
confluence_retrieve: confluenceRetrieveTool,
confluence_list: confluenceListTool,
confluence_update: confluenceUpdateTool,
dalle_generate: dalleTool,
}
// Get a tool by its ID
+238
View File
@@ -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<string>((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<string>((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'
},
}