From bab4b9f04181729403f0418ae3e27d3ec4720428 Mon Sep 17 00:00:00 2001 From: waleedlatif Date: Thu, 31 Jul 2025 23:52:07 -0700 Subject: [PATCH] feat(deploy-chat): added a logo upload for the chat, incr font size --- apps/sim/app/api/__test-utils__/utils.ts | 16 +- apps/sim/app/api/chat/edit/[id]/route.ts | 1 + apps/sim/app/api/chat/route.ts | 1 + .../sim/app/api/files/presigned/route.test.ts | 65 +++- apps/sim/app/api/files/presigned/route.ts | 60 +++- apps/sim/app/chat/[subdomain]/chat-client.tsx | 1 + .../[subdomain]/components/header/header.tsx | 59 +++- .../components/message/message.tsx | 6 +- .../components/chat-deploy/chat-deploy.tsx | 22 +- .../image-selector/image-selector.tsx | 278 ++++++++++++++++++ .../deploy-modal/components/index.ts | 1 + apps/sim/lib/env.ts | 2 + apps/sim/lib/security/csp.ts | 13 + apps/sim/lib/uploads/index.ts | 2 + apps/sim/lib/uploads/setup.ts | 12 + 15 files changed, 513 insertions(+), 26 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/image-selector/image-selector.tsx diff --git a/apps/sim/app/api/__test-utils__/utils.ts b/apps/sim/app/api/__test-utils__/utils.ts index 8c7caa8c5d..55d2357991 100644 --- a/apps/sim/app/api/__test-utils__/utils.ts +++ b/apps/sim/app/api/__test-utils__/utils.ts @@ -784,6 +784,10 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions = bucket: 'test-s3-kb-bucket', region: 'us-east-1', }, + S3_CHAT_CONFIG: { + bucket: 'test-s3-chat-bucket', + region: 'us-east-1', + }, BLOB_CONFIG: { accountName: 'testaccount', accountKey: 'testkey', @@ -794,6 +798,11 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions = accountKey: 'testkey', containerName: 'test-kb-container', }, + BLOB_CHAT_CONFIG: { + accountName: 'testaccount', + accountKey: 'testkey', + containerName: 'test-chat-container', + }, })) vi.doMock('@aws-sdk/client-s3', () => ({ @@ -809,7 +818,7 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions = }), })) } else if (provider === 'blob') { - const baseUrl = presignedUrl.replace('?sas-token-string', '') + const baseUrl = 'https://testaccount.blob.core.windows.net/test-container' const mockBlockBlobClient = { url: baseUrl, } @@ -841,6 +850,11 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions = accountKey: 'testkey', containerName: 'test-kb-container', }, + BLOB_CHAT_CONFIG: { + accountName: 'testaccount', + accountKey: 'testkey', + containerName: 'test-chat-container', + }, })) vi.doMock('@azure/storage-blob', () => ({ diff --git a/apps/sim/app/api/chat/edit/[id]/route.ts b/apps/sim/app/api/chat/edit/[id]/route.ts index dcaaeee46e..08babbcd58 100644 --- a/apps/sim/app/api/chat/edit/[id]/route.ts +++ b/apps/sim/app/api/chat/edit/[id]/route.ts @@ -29,6 +29,7 @@ const chatUpdateSchema = z.object({ .object({ primaryColor: z.string(), welcomeMessage: z.string(), + imageUrl: z.string().optional(), }) .optional(), authType: z.enum(['public', 'password', 'email']).optional(), diff --git a/apps/sim/app/api/chat/route.ts b/apps/sim/app/api/chat/route.ts index cbbb937c6c..9ea5ca3c1e 100644 --- a/apps/sim/app/api/chat/route.ts +++ b/apps/sim/app/api/chat/route.ts @@ -27,6 +27,7 @@ const chatSchema = z.object({ customizations: z.object({ primaryColor: z.string(), welcomeMessage: z.string(), + imageUrl: z.string().optional(), }), authType: z.enum(['public', 'password', 'email']).default('public'), password: z.string().optional(), diff --git a/apps/sim/app/api/files/presigned/route.test.ts b/apps/sim/app/api/files/presigned/route.test.ts index a96446b005..4702324d52 100644 --- a/apps/sim/app/api/files/presigned/route.test.ts +++ b/apps/sim/app/api/files/presigned/route.test.ts @@ -204,6 +204,32 @@ describe('/api/files/presigned', () => { expect(data.directUploadSupported).toBe(true) }) + it('should generate chat S3 presigned URL with chat prefix and direct path', async () => { + setupFileApiMocks({ + cloudEnabled: true, + storageProvider: 's3', + }) + + const { POST } = await import('@/app/api/files/presigned/route') + + const request = new NextRequest('http://localhost:3000/api/files/presigned?type=chat', { + method: 'POST', + body: JSON.stringify({ + fileName: 'chat-logo.png', + contentType: 'image/png', + fileSize: 4096, + }), + }) + + const response = await POST(request) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.fileInfo.key).toMatch(/^chat\/.*chat-logo\.png$/) + expect(data.fileInfo.path).toMatch(/^https:\/\/.*\.s3\..*\.amazonaws\.com\/chat\//) + expect(data.directUploadSupported).toBe(true) + }) + it('should generate Azure Blob presigned URL successfully', async () => { setupFileApiMocks({ cloudEnabled: true, @@ -225,7 +251,9 @@ describe('/api/files/presigned', () => { const data = await response.json() expect(response.status).toBe(200) - expect(data.presignedUrl).toContain('https://example.com/presigned-url') + expect(data.presignedUrl).toContain( + 'https://testaccount.blob.core.windows.net/test-container' + ) expect(data.presignedUrl).toContain('sas-token-string') expect(data.fileInfo).toMatchObject({ path: expect.stringContaining('/api/files/serve/blob/'), @@ -243,6 +271,41 @@ describe('/api/files/presigned', () => { }) }) + it('should generate chat Azure Blob presigned URL with chat prefix and direct path', async () => { + setupFileApiMocks({ + cloudEnabled: true, + storageProvider: 'blob', + }) + + const { POST } = await import('@/app/api/files/presigned/route') + + const request = new NextRequest('http://localhost:3000/api/files/presigned?type=chat', { + method: 'POST', + body: JSON.stringify({ + fileName: 'chat-logo.png', + contentType: 'image/png', + fileSize: 4096, + }), + }) + + const response = await POST(request) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.fileInfo.key).toMatch(/^chat\/.*chat-logo\.png$/) + expect(data.fileInfo.path).toContain( + 'https://testaccount.blob.core.windows.net/test-container' + ) + expect(data.directUploadSupported).toBe(true) + expect(data.uploadHeaders).toMatchObject({ + 'x-ms-blob-type': 'BlockBlob', + 'x-ms-blob-content-type': 'image/png', + 'x-ms-meta-originalname': expect.any(String), + 'x-ms-meta-uploadedat': '2024-01-01T00:00:00.000Z', + 'x-ms-meta-purpose': 'chat', + }) + }) + it('should return error for unknown storage provider', async () => { // For unknown provider, we'll need to mock manually since our helper doesn't support it vi.doMock('@/lib/uploads', () => ({ diff --git a/apps/sim/app/api/files/presigned/route.ts b/apps/sim/app/api/files/presigned/route.ts index 6ca7879773..30c3321544 100644 --- a/apps/sim/app/api/files/presigned/route.ts +++ b/apps/sim/app/api/files/presigned/route.ts @@ -6,7 +6,14 @@ import { createLogger } from '@/lib/logs/console/logger' import { getStorageProvider, isUsingCloudStorage } from '@/lib/uploads' import { getBlobServiceClient } from '@/lib/uploads/blob/blob-client' import { getS3Client, sanitizeFilenameForMetadata } from '@/lib/uploads/s3/s3-client' -import { BLOB_CONFIG, BLOB_KB_CONFIG, S3_CONFIG, S3_KB_CONFIG } from '@/lib/uploads/setup' +import { + BLOB_CHAT_CONFIG, + BLOB_CONFIG, + BLOB_KB_CONFIG, + S3_CHAT_CONFIG, + S3_CONFIG, + S3_KB_CONFIG, +} from '@/lib/uploads/setup' import { createErrorResponse, createOptionsResponse } from '@/app/api/files/utils' const logger = createLogger('PresignedUploadAPI') @@ -17,7 +24,7 @@ interface PresignedUrlRequest { fileSize: number } -type UploadType = 'general' | 'knowledge-base' +type UploadType = 'general' | 'knowledge-base' | 'chat' class PresignedUrlError extends Error { constructor( @@ -72,7 +79,11 @@ export async function POST(request: NextRequest) { const uploadTypeParam = request.nextUrl.searchParams.get('type') const uploadType: UploadType = - uploadTypeParam === 'knowledge-base' ? 'knowledge-base' : 'general' + uploadTypeParam === 'knowledge-base' + ? 'knowledge-base' + : uploadTypeParam === 'chat' + ? 'chat' + : 'general' if (!isUsingCloudStorage()) { throw new StorageConfigError( @@ -118,14 +129,19 @@ async function handleS3PresignedUrl( uploadType: UploadType ) { try { - const config = uploadType === 'knowledge-base' ? S3_KB_CONFIG : S3_CONFIG + const config = + uploadType === 'knowledge-base' + ? S3_KB_CONFIG + : uploadType === 'chat' + ? S3_CHAT_CONFIG + : S3_CONFIG if (!config.bucket || !config.region) { throw new StorageConfigError(`S3 configuration missing for ${uploadType} uploads`) } const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_') - const prefix = uploadType === 'knowledge-base' ? 'kb/' : '' + const prefix = uploadType === 'knowledge-base' ? 'kb/' : uploadType === 'chat' ? 'chat/' : '' const uniqueKey = `${prefix}${Date.now()}-${uuidv4()}-${safeFileName}` const sanitizedOriginalName = sanitizeFilenameForMetadata(fileName) @@ -137,6 +153,8 @@ async function handleS3PresignedUrl( if (uploadType === 'knowledge-base') { metadata.purpose = 'knowledge-base' + } else if (uploadType === 'chat') { + metadata.purpose = 'chat' } const command = new PutObjectCommand({ @@ -156,14 +174,22 @@ async function handleS3PresignedUrl( ) } - const servePath = `/api/files/serve/s3/${encodeURIComponent(uniqueKey)}` + // For chat images, use direct S3 URLs since they need to be permanently accessible + // For other files, use serve path for access control + const finalPath = + uploadType === 'chat' + ? `https://${config.bucket}.s3.${config.region}.amazonaws.com/${uniqueKey}` + : `/api/files/serve/s3/${encodeURIComponent(uniqueKey)}` logger.info(`Generated ${uploadType} S3 presigned URL for ${fileName} (${uniqueKey})`) + logger.info(`Presigned URL: ${presignedUrl}`) + logger.info(`Final path: ${finalPath}`) return NextResponse.json({ presignedUrl, + uploadUrl: presignedUrl, // Make sure we're returning the uploadUrl field fileInfo: { - path: servePath, + path: finalPath, key: uniqueKey, name: fileName, size: fileSize, @@ -187,7 +213,12 @@ async function handleBlobPresignedUrl( uploadType: UploadType ) { try { - const config = uploadType === 'knowledge-base' ? BLOB_KB_CONFIG : BLOB_CONFIG + const config = + uploadType === 'knowledge-base' + ? BLOB_KB_CONFIG + : uploadType === 'chat' + ? BLOB_CHAT_CONFIG + : BLOB_CONFIG if ( !config.accountName || @@ -198,7 +229,7 @@ async function handleBlobPresignedUrl( } const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_') - const prefix = uploadType === 'knowledge-base' ? 'kb/' : '' + const prefix = uploadType === 'knowledge-base' ? 'kb/' : uploadType === 'chat' ? 'chat/' : '' const uniqueKey = `${prefix}${Date.now()}-${uuidv4()}-${safeFileName}` const blobServiceClient = getBlobServiceClient() @@ -231,7 +262,12 @@ async function handleBlobPresignedUrl( const presignedUrl = `${blockBlobClient.url}?${sasToken}` - const servePath = `/api/files/serve/blob/${encodeURIComponent(uniqueKey)}` + // For chat images, use direct Blob URLs since they need to be permanently accessible + // For other files, use serve path for access control + const finalPath = + uploadType === 'chat' + ? blockBlobClient.url + : `/api/files/serve/blob/${encodeURIComponent(uniqueKey)}` logger.info(`Generated ${uploadType} Azure Blob presigned URL for ${fileName} (${uniqueKey})`) @@ -244,12 +280,14 @@ async function handleBlobPresignedUrl( if (uploadType === 'knowledge-base') { uploadHeaders['x-ms-meta-purpose'] = 'knowledge-base' + } else if (uploadType === 'chat') { + uploadHeaders['x-ms-meta-purpose'] = 'chat' } return NextResponse.json({ presignedUrl, fileInfo: { - path: servePath, + path: finalPath, key: uniqueKey, name: fileName, size: fileSize, diff --git a/apps/sim/app/chat/[subdomain]/chat-client.tsx b/apps/sim/app/chat/[subdomain]/chat-client.tsx index 4eba1ab8fa..5646b9414d 100644 --- a/apps/sim/app/chat/[subdomain]/chat-client.tsx +++ b/apps/sim/app/chat/[subdomain]/chat-client.tsx @@ -30,6 +30,7 @@ interface ChatConfig { customizations: { primaryColor?: string logoUrl?: string + imageUrl?: string welcomeMessage?: string headerText?: string } diff --git a/apps/sim/app/chat/[subdomain]/components/header/header.tsx b/apps/sim/app/chat/[subdomain]/components/header/header.tsx index ce7590d2cf..be88caf077 100644 --- a/apps/sim/app/chat/[subdomain]/components/header/header.tsx +++ b/apps/sim/app/chat/[subdomain]/components/header/header.tsx @@ -8,6 +8,7 @@ interface ChatHeaderProps { customizations?: { headerText?: string logoUrl?: string + imageUrl?: string primaryColor?: string } } | null @@ -16,18 +17,58 @@ interface ChatHeaderProps { export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) { const primaryColor = chatConfig?.customizations?.primaryColor || '#701FFC' + const customImage = chatConfig?.customizations?.imageUrl || chatConfig?.customizations?.logoUrl return ( -
-
- {chatConfig?.customizations?.logoUrl && ( +
+
+ {customImage ? ( {`${chatConfig?.title + ) : ( + // Default Sim Studio logo when no custom image is provided +
+ + + + + + +
)} -

+

{chatConfig?.customizations?.headerText || chatConfig?.title || 'Chat'}

@@ -39,8 +80,8 @@ export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) { target='_blank' rel='noopener noreferrer' > - - {starCount} + + {starCount}
-
+
{isJsonObject ? (
{JSON.stringify(message.content, null, 2)}
) : ( @@ -103,7 +103,7 @@ export const ClientChatMessage = memo( if (item.type === 'text' && item.content.trim()) { return (
-
+
@@ -115,7 +115,7 @@ export const ClientChatMessage = memo( ) : ( /* Fallback for empty content or no inline content */
-
+
{isJsonObject ? (
                       {JSON.stringify(cleanTextContent, null, 2)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy.tsx
index f7c8c701ac..505a8d7ed0 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy.tsx
@@ -33,6 +33,7 @@ import { Textarea } from '@/components/ui/textarea'
 import { createLogger } from '@/lib/logs/console/logger'
 import { getBaseDomain, getEmailDomain } from '@/lib/urls/utils'
 import { cn } from '@/lib/utils'
+import { ImageSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/image-selector/image-selector'
 import { OutputSelect } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components'
 import type { OutputConfig } from '@/stores/panel/chat/types'
 
@@ -68,6 +69,7 @@ const chatSchema = z.object({
   customizations: z.object({
     primaryColor: z.string(),
     welcomeMessage: z.string(),
+    imageUrl: z.string().optional(),
   }),
   authType: z.enum(['public', 'password', 'email']),
   password: z.string().optional(),
@@ -146,6 +148,9 @@ export function ChatDeploy({
   // Welcome message state
   const [welcomeMessage, setWelcomeMessage] = useState('Hi there! How can I help you today?')
 
+  // Image URL state
+  const [imageUrl, setImageUrl] = useState(null)
+
   // Expose a method to handle external submission requests
   useEffect(() => {
     // This will run when the component mounts
@@ -239,6 +244,11 @@ export function ChatDeploy({
             if (chatDetail.customizations?.welcomeMessage) {
               setWelcomeMessage(chatDetail.customizations.welcomeMessage)
             }
+
+            // Set image URL if it exists
+            if (chatDetail.customizations?.imageUrl) {
+              setImageUrl(chatDetail.customizations.imageUrl)
+            }
           } else {
             logger.error('Failed to fetch chat details')
           }
@@ -524,6 +534,7 @@ export function ChatDeploy({
         customizations: {
           primaryColor: '#802FFF',
           welcomeMessage: welcomeMessage.trim(),
+          ...(imageUrl && { imageUrl }),
         },
         authType: authType,
       }
@@ -1189,7 +1200,7 @@ export function ChatDeploy({
             
- {/* Welcome Message Section - Add this before the form closing div */} + {/* Welcome Message Section */}
+ + {/* Image Selector Section */} +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/image-selector/image-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/image-selector/image-selector.tsx new file mode 100644 index 0000000000..7111c31494 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/image-selector/image-selector.tsx @@ -0,0 +1,278 @@ +'use client' + +import { useRef, useState } from 'react' +import { Image, Loader2, Upload, X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' +import { createLogger } from '@/lib/logs/console/logger' +import { cn } from '@/lib/utils' + +const logger = createLogger('ImageSelector') + +const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB +const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp'] + +interface ImageSelectorProps { + value?: string | null + onChange: (imageUrl: string | null) => void + disabled?: boolean + label?: string + placeholder?: string + className?: string +} + +export function ImageSelector({ + value, + onChange, + disabled = false, + label = 'Logo Image', + placeholder = 'Upload an image for your chat logo', + className, +}: ImageSelectorProps) { + const fileInputRef = useRef(null) + const [isUploading, setIsUploading] = useState(false) + const [isDragging, setIsDragging] = useState(false) + const [uploadError, setUploadError] = useState(null) + + const validateFile = (file: File): string | null => { + if (file.size > MAX_FILE_SIZE) { + return `File "${file.name}" is too large. Maximum size is 5MB.` + } + if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { + return `File "${file.name}" is not a supported image format. Please use PNG, JPEG, GIF, or WebP.` + } + return null + } + + const handleFileUpload = async (file: File) => { + const error = validateFile(file) + if (error) { + setUploadError(error) + return + } + + setIsUploading(true) + setUploadError(null) + + try { + // First, try to get a pre-signed URL for direct upload with chat type + const presignedResponse = await fetch('/api/files/presigned?type=chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + fileName: file.name, + contentType: file.type, + fileSize: file.size, + }), + }) + + if (presignedResponse.ok) { + // Use direct upload with presigned URL + const presignedData = await presignedResponse.json() + + // Log the presigned URL response for debugging + logger.info('Presigned URL response:', presignedData) + + // Upload directly to storage provider + const uploadHeaders: Record = { + 'Content-Type': file.type, + } + + // Add any additional headers from the presigned response (for Azure Blob) + if (presignedData.uploadHeaders) { + Object.assign(uploadHeaders, presignedData.uploadHeaders) + } + + const uploadResponse = await fetch(presignedData.uploadUrl, { + method: 'PUT', + body: file, + headers: uploadHeaders, + }) + + logger.info(`Upload response status: ${uploadResponse.status}`) + logger.info( + 'Upload response headers:', + Object.fromEntries(uploadResponse.headers.entries()) + ) + + if (!uploadResponse.ok) { + const responseText = await uploadResponse.text() + logger.error(`Direct upload failed: ${uploadResponse.status} - ${responseText}`) + throw new Error(`Direct upload failed: ${uploadResponse.status} - ${responseText}`) + } + + // Use the file info returned from the presigned URL endpoint + const publicUrl = presignedData.fileInfo.path + onChange(publicUrl) + logger.info(`Image uploaded successfully via direct upload: ${publicUrl}`) + } else { + // Fallback to traditional upload through API route + const formData = new FormData() + formData.append('file', file) + + const response = await fetch('/api/files/upload', { + method: 'POST', + body: formData, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: response.statusText })) + throw new Error(errorData.error || `Failed to upload file: ${response.status}`) + } + + const data = await response.json() + const publicUrl = data.path + onChange(publicUrl) + logger.info(`Image uploaded successfully via server upload: ${publicUrl}`) + } + } catch (error) { + logger.error('Error uploading image:', error) + const errorMessage = + error instanceof Error ? error.message : 'Failed to upload image. Please try again.' + setUploadError(errorMessage) + } finally { + setIsUploading(false) + } + } + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + handleFileUpload(file) + } + } + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (!disabled) { + setIsDragging(true) + } + } + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragging(false) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragging(false) + + if (disabled) return + + const file = e.dataTransfer.files?.[0] + if (file) { + handleFileUpload(file) + } + } + + const handleRemove = () => { + onChange(null) + setUploadError(null) + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + + const handleClick = () => { + if (!disabled && fileInputRef.current) { + fileInputRef.current.click() + } + } + + return ( +
+ + + {value ? ( + // Show uploaded image +
+
+ Uploaded logo + +
+ +
+ ) : ( + // Show upload area +
+ + + {isUploading ? ( +
+ +

Uploading image...

+
+ ) : ( +
+ +
+

+ {isDragging ? 'Drop image here!' : placeholder} +

+

PNG, JPEG, GIF, or WebP (max 5MB)

+
+
+ )} +
+ )} + + {uploadError &&

{uploadError}

} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/index.ts index b583c7b2fc..7ef04b5017 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/index.ts @@ -1,3 +1,4 @@ export { ChatDeploy } from './chat-deploy/chat-deploy' export { DeployForm } from './deploy-form/deploy-form' export { DeploymentInfo } from './deployment-info/deployment-info' +export { ImageSelector } from './image-selector/image-selector' diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 3a67de5b8f..f78249ae0c 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -91,6 +91,7 @@ export const env = createEnv({ S3_BUCKET_NAME: z.string().optional(), // S3 bucket for general file storage S3_LOGS_BUCKET_NAME: z.string().optional(), // S3 bucket for storing logs S3_KB_BUCKET_NAME: z.string().optional(), // S3 bucket for knowledge base files + S3_CHAT_BUCKET_NAME: z.string().optional(), // S3 bucket for chat logos // Cloud Storage - Azure Blob AZURE_ACCOUNT_NAME: z.string().optional(), // Azure storage account name @@ -98,6 +99,7 @@ export const env = createEnv({ AZURE_CONNECTION_STRING: z.string().optional(), // Azure storage connection string AZURE_STORAGE_CONTAINER_NAME: z.string().optional(), // Azure container for general files AZURE_STORAGE_KB_CONTAINER_NAME: z.string().optional(), // Azure container for knowledge base files + AZURE_STORAGE_CHAT_CONTAINER_NAME: z.string().optional(), // Azure container for chat logos // Data Retention FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(), // Log retention days for free plan users diff --git a/apps/sim/lib/security/csp.ts b/apps/sim/lib/security/csp.ts index 1c575b35d2..16aa5d29fb 100644 --- a/apps/sim/lib/security/csp.ts +++ b/apps/sim/lib/security/csp.ts @@ -51,6 +51,19 @@ export const cspDirectives: CSPDirectives = { 'https://cdn.discordapp.com', 'https://*.githubusercontent.com', 'https://*.public.blob.vercel-storage.com', + 'https://*.s3.amazonaws.com', + 'https://s3.amazonaws.com', + ...(env.S3_BUCKET_NAME && env.AWS_REGION + ? [`https://${env.S3_BUCKET_NAME}.s3.${env.AWS_REGION}.amazonaws.com`] + : []), + ...(env.S3_KB_BUCKET_NAME && env.AWS_REGION + ? [`https://${env.S3_KB_BUCKET_NAME}.s3.${env.AWS_REGION}.amazonaws.com`] + : []), + ...(env.S3_CHAT_BUCKET_NAME && env.AWS_REGION + ? [`https://${env.S3_CHAT_BUCKET_NAME}.s3.${env.AWS_REGION}.amazonaws.com`] + : []), + 'https://*.amazonaws.com', + 'https://*.blob.core.windows.net', ], 'media-src': ["'self'", 'blob:'], diff --git a/apps/sim/lib/uploads/index.ts b/apps/sim/lib/uploads/index.ts index fef62fda4d..9cf3f6930f 100644 --- a/apps/sim/lib/uploads/index.ts +++ b/apps/sim/lib/uploads/index.ts @@ -1,9 +1,11 @@ export * as BlobClient from '@/lib/uploads/blob/blob-client' export * as S3Client from '@/lib/uploads/s3/s3-client' export { + BLOB_CHAT_CONFIG, BLOB_CONFIG, BLOB_KB_CONFIG, ensureUploadsDirectory, + S3_CHAT_CONFIG, S3_CONFIG, S3_KB_CONFIG, UPLOAD_DIR, diff --git a/apps/sim/lib/uploads/setup.ts b/apps/sim/lib/uploads/setup.ts index eed88bcbbe..c71b00e21f 100644 --- a/apps/sim/lib/uploads/setup.ts +++ b/apps/sim/lib/uploads/setup.ts @@ -48,6 +48,18 @@ export const BLOB_KB_CONFIG = { containerName: env.AZURE_STORAGE_KB_CONTAINER_NAME || '', } +export const S3_CHAT_CONFIG = { + bucket: env.S3_CHAT_BUCKET_NAME || '', + region: env.AWS_REGION || '', +} + +export const BLOB_CHAT_CONFIG = { + accountName: env.AZURE_ACCOUNT_NAME || '', + accountKey: env.AZURE_ACCOUNT_KEY || '', + connectionString: env.AZURE_CONNECTION_STRING || '', + containerName: env.AZURE_STORAGE_CHAT_CONTAINER_NAME || '', +} + export async function ensureUploadsDirectory() { if (USE_S3_STORAGE) { logger.info('Using S3 storage, skipping local uploads directory creation')