improvement(ux): format json logs response, fix tts and image proxies, fix airtable block

This commit is contained in:
Waleed Latif
2025-05-05 23:07:47 -07:00
parent dce826840c
commit bac4949dfe
6 changed files with 58 additions and 92 deletions
+1 -2
View File
@@ -27,8 +27,7 @@ export async function POST(request: Request) {
text,
model_id: modelId,
}),
// Set a reasonable timeout
signal: AbortSignal.timeout(20000),
signal: AbortSignal.timeout(60000),
})
if (!response.ok) {
@@ -191,7 +191,7 @@ const ImagePreview = ({
// Determine the source for the image
const imageSrc =
isBase64 && imageData && imageData.length > 0
? `data:image/pngbase64,${imageData}`
? `data:image/png;base64,${imageData}`
: imageUrl || ''
return (
@@ -363,13 +363,6 @@ export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONVie
<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}
@@ -478,15 +471,6 @@ export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONVie
<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}
@@ -630,9 +614,11 @@ export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONVie
<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}
initiallyExpanded={initiallyExpanded}
/>
) : (
<JSONView
data={value}
+30 -5
View File
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useMemo, useRef, useState } from 'react'
import { ChevronDown, ChevronUp, X } from 'lucide-react'
import { ChevronDown, ChevronUp, X, Code } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { CopyButton } from '@/components/ui/copy-button'
import { ScrollArea } from '@/components/ui/scroll-area'
@@ -22,6 +22,28 @@ interface LogSidebarProps {
hasPrev?: boolean
}
/**
* Tries to parse a string as JSON and prettify it
*/
const tryPrettifyJson = (content: string): { isJson: boolean; formatted: string } => {
try {
// First check if the content looks like JSON (starts with { or [)
const trimmed = content.trim()
if (!(trimmed.startsWith('{') || trimmed.startsWith('[')) ||
!(trimmed.endsWith('}') || trimmed.endsWith(']'))) {
return { isJson: false, formatted: content }
}
// Try to parse the JSON
const parsed = JSON.parse(trimmed)
const prettified = JSON.stringify(parsed, null, 2)
return { isJson: true, formatted: prettified }
} catch (e) {
// If parsing fails, it's not valid JSON
return { isJson: false, formatted: content }
}
}
/**
* Formats JSON content for display, handling multiple JSON objects separated by '--'
*/
@@ -33,14 +55,15 @@ const formatJsonContent = (content: string): React.ReactNode => {
if (match) {
const systemComment = match[1]
const actualContent = content.substring(match[0].length).trim()
const { formatted } = tryPrettifyJson(actualContent)
return (
<div className="w-full">
<div className="text-sm font-medium mb-2 text-muted-foreground">{systemComment}</div>
<div className="bg-secondary/30 p-3 rounded-md relative group">
<CopyButton text={actualContent} />
<CopyButton text={formatted} className="h-7 w-7 z-10" />
<pre className="text-sm whitespace-pre-wrap break-all w-full overflow-y-auto max-h-[500px] overflow-x-hidden">
{actualContent}
{formatted}
</pre>
</div>
</div>
@@ -48,11 +71,13 @@ const formatJsonContent = (content: string): React.ReactNode => {
}
// If no system comment pattern found, show the whole content
const { formatted } = tryPrettifyJson(content)
return (
<div className="bg-secondary/30 p-3 rounded-md relative group w-full">
<CopyButton text={content} />
<CopyButton text={formatted} className="h-7 w-7 z-10" />
<pre className="text-sm whitespace-pre-wrap break-all w-full overflow-y-auto max-h-[500px] overflow-x-hidden">
{content}
{formatted}
</pre>
</div>
)
+1 -10
View File
@@ -8,7 +8,6 @@ import {
} from '@/tools/airtable/types'
import { BlockConfig } from '../types'
// Union type for all possible Airtable responses
type AirtableResponse =
| AirtableListResponse
| AirtableGetResponse
@@ -28,7 +27,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
bgColor: '#E0E0E0',
icon: AirtableIcon,
subBlocks: [
// Operation selector
{
id: 'operation',
title: 'Operation',
@@ -41,7 +39,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
{ label: 'Update Record', id: 'update' },
],
},
// Airtable Credentials
{
id: 'credential',
title: 'Airtable Account',
@@ -52,7 +49,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
requiredScopes: ['data.records:read', 'data.records:write'], // Keep both scopes
placeholder: 'Select Airtable account',
},
// Base ID
{
id: 'baseId',
title: 'Base ID',
@@ -60,7 +56,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
layout: 'full',
placeholder: 'Enter your base ID (e.g., appXXXXXXXXXXXXXX)',
},
// Table ID
{
id: 'tableId',
title: 'Table ID',
@@ -68,7 +63,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
layout: 'full',
placeholder: 'Enter table ID (e.g., tblXXXXXXXXXXXXXX)',
},
// Record ID (For Get/Update Single)
{
id: 'recordId',
title: 'Record ID',
@@ -77,7 +71,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
placeholder: 'ID of the record (e.g., recXXXXXXXXXXXXXX)',
condition: { field: 'operation', value: ['get', 'update'] },
},
// List Operation Fields
{
id: 'maxRecords',
title: 'Max Records',
@@ -94,7 +87,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
placeholder: 'Airtable formula to filter records (optional)',
condition: { field: 'operation', value: 'list' },
},
// Create / Update Multiple Operation Field: Records (Array)
{
id: 'records',
title: 'Records (JSON Array)',
@@ -103,7 +95,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
placeholder: 'For Create: `[{ "fields": { ... } }]`\n',
condition: { field: 'operation', value: ['create', 'updateMultiple'] },
},
// Update Single Operation Field: Fields (Object)
{
id: 'fields',
title: 'Fields (JSON Object)',
@@ -181,7 +172,7 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
baseId: { type: 'string', required: true },
tableId: { type: 'string', required: true },
// Conditional inputs
recordId: { type: 'string', required: false }, // Required for get/update
recordId: { type: 'string', required: true }, // Required for get/update
maxRecords: { type: 'number', required: false }, // Optional for list
filterFormula: { type: 'string', required: false }, // Optional for list
records: { type: 'json', required: false }, // Required for create/updateMultiple
+1 -1
View File
@@ -106,7 +106,7 @@ const nextConfig: NextConfig = {
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.google.com https://apis.google.com https://*.vercel-insights.com https://vercel.live https://*.vercel.live; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https://*.googleusercontent.com https://*.google.com https://*.atlassian.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' http://localhost:11434 http://host.docker.internal:11434 https://*.googleapis.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.vercel-insights.com https://*.atlassian.com https://vercel.live https://*.vercel.live; frame-src https://drive.google.com https://*.google.com; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; object-src 'none'",
value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.google.com https://apis.google.com https://*.vercel-insights.com https://vercel.live https://*.vercel.live; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https://*.googleusercontent.com https://*.google.com https://*.atlassian.com; media-src 'self' blob:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' http://localhost:11434 http://host.docker.internal:11434 https://*.googleapis.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.vercel-insights.com https://*.atlassian.com https://vercel.live https://*.vercel.live; frame-src https://drive.google.com https://*.google.com; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; object-src 'none'",
},
],
},
+19 -54
View File
@@ -81,9 +81,12 @@ export const dalleTool: ToolConfig = {
try {
// Fetch the image using the proxy/image endpoint instead of direct fetch
logger.info('Fetching image from URL via proxy...')
const proxyUrl = `/api/proxy/image?url=${encodeURIComponent(imageUrl)}`
// Get the base URL from environment or use a fallback
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
const proxyUrl = new URL(`/api/proxy/image`, baseUrl)
proxyUrl.searchParams.append('url', imageUrl)
const imageResponse = await fetch(proxyUrl, {
const imageResponse = await fetch(proxyUrl.toString(), {
headers: {
Accept: 'image/*, */*',
},
@@ -102,32 +105,10 @@ export const dalleTool: ToolConfig = {
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
resolve(base64Content)
} catch (err) {
logger.error('Error in FileReader onloadend:', err)
reject(err)
}
}
reader.onerror = (err) => {
logger.error('FileReader error:', err)
reject(new Error('Failed to read image data'))
}
reader.readAsDataURL(imageBlob)
})
const base64Image = await base64Promise
const arrayBuffer = await imageBlob.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const base64Image = buffer.toString('base64')
return {
success: true,
output: {
@@ -149,7 +130,7 @@ export const dalleTool: ToolConfig = {
cache: 'no-store',
headers: {
Accept: 'image/*, */*',
'User-Agent': 'Mozilla/5.0 (compatible; DalleProxy/1.0)',
'User-Agent': 'Mozilla/5.0 (compatible DalleProxy/1.0)',
},
})
@@ -162,31 +143,15 @@ export const dalleTool: ToolConfig = {
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]
logger.info(
'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
// Server-side safe way to convert blob to base64
const arrayBuffer = await imageBlob.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const base64Image = buffer.toString('base64')
logger.info(
'Successfully converted image to base64 via direct fetch, length:',
base64Image.length
)
return {
success: true,