mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(ux): format json logs response, fix tts and image proxies, fix airtable block
This commit is contained in:
@@ -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) {
|
||||
|
||||
+6
-20
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user