Skip to content

Commit bab4b9f

Browse files
waleedlatifwaleedlatif
authored andcommitted
feat(deploy-chat): added a logo upload for the chat, incr font size
1 parent c259390 commit bab4b9f

File tree

15 files changed

+513
-26
lines changed

15 files changed

+513
-26
lines changed

apps/sim/app/api/__test-utils__/utils.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,10 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions =
784784
bucket: 'test-s3-kb-bucket',
785785
region: 'us-east-1',
786786
},
787+
S3_CHAT_CONFIG: {
788+
bucket: 'test-s3-chat-bucket',
789+
region: 'us-east-1',
790+
},
787791
BLOB_CONFIG: {
788792
accountName: 'testaccount',
789793
accountKey: 'testkey',
@@ -794,6 +798,11 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions =
794798
accountKey: 'testkey',
795799
containerName: 'test-kb-container',
796800
},
801+
BLOB_CHAT_CONFIG: {
802+
accountName: 'testaccount',
803+
accountKey: 'testkey',
804+
containerName: 'test-chat-container',
805+
},
797806
}))
798807

799808
vi.doMock('@aws-sdk/client-s3', () => ({
@@ -809,7 +818,7 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions =
809818
}),
810819
}))
811820
} else if (provider === 'blob') {
812-
const baseUrl = presignedUrl.replace('?sas-token-string', '')
821+
const baseUrl = 'https://testaccount.blob.core.windows.net/test-container'
813822
const mockBlockBlobClient = {
814823
url: baseUrl,
815824
}
@@ -841,6 +850,11 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions =
841850
accountKey: 'testkey',
842851
containerName: 'test-kb-container',
843852
},
853+
BLOB_CHAT_CONFIG: {
854+
accountName: 'testaccount',
855+
accountKey: 'testkey',
856+
containerName: 'test-chat-container',
857+
},
844858
}))
845859

846860
vi.doMock('@azure/storage-blob', () => ({

apps/sim/app/api/chat/edit/[id]/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const chatUpdateSchema = z.object({
2929
.object({
3030
primaryColor: z.string(),
3131
welcomeMessage: z.string(),
32+
imageUrl: z.string().optional(),
3233
})
3334
.optional(),
3435
authType: z.enum(['public', 'password', 'email']).optional(),

apps/sim/app/api/chat/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const chatSchema = z.object({
2727
customizations: z.object({
2828
primaryColor: z.string(),
2929
welcomeMessage: z.string(),
30+
imageUrl: z.string().optional(),
3031
}),
3132
authType: z.enum(['public', 'password', 'email']).default('public'),
3233
password: z.string().optional(),

apps/sim/app/api/files/presigned/route.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,32 @@ describe('/api/files/presigned', () => {
204204
expect(data.directUploadSupported).toBe(true)
205205
})
206206

207+
it('should generate chat S3 presigned URL with chat prefix and direct path', async () => {
208+
setupFileApiMocks({
209+
cloudEnabled: true,
210+
storageProvider: 's3',
211+
})
212+
213+
const { POST } = await import('@/app/api/files/presigned/route')
214+
215+
const request = new NextRequest('http://localhost:3000/api/files/presigned?type=chat', {
216+
method: 'POST',
217+
body: JSON.stringify({
218+
fileName: 'chat-logo.png',
219+
contentType: 'image/png',
220+
fileSize: 4096,
221+
}),
222+
})
223+
224+
const response = await POST(request)
225+
const data = await response.json()
226+
227+
expect(response.status).toBe(200)
228+
expect(data.fileInfo.key).toMatch(/^chat\/.*chat-logo\.png$/)
229+
expect(data.fileInfo.path).toMatch(/^https:\/\/.*\.s3\..*\.amazonaws\.com\/chat\//)
230+
expect(data.directUploadSupported).toBe(true)
231+
})
232+
207233
it('should generate Azure Blob presigned URL successfully', async () => {
208234
setupFileApiMocks({
209235
cloudEnabled: true,
@@ -225,7 +251,9 @@ describe('/api/files/presigned', () => {
225251
const data = await response.json()
226252

227253
expect(response.status).toBe(200)
228-
expect(data.presignedUrl).toContain('https://example.com/presigned-url')
254+
expect(data.presignedUrl).toContain(
255+
'https://testaccount.blob.core.windows.net/test-container'
256+
)
229257
expect(data.presignedUrl).toContain('sas-token-string')
230258
expect(data.fileInfo).toMatchObject({
231259
path: expect.stringContaining('/api/files/serve/blob/'),
@@ -243,6 +271,41 @@ describe('/api/files/presigned', () => {
243271
})
244272
})
245273

274+
it('should generate chat Azure Blob presigned URL with chat prefix and direct path', async () => {
275+
setupFileApiMocks({
276+
cloudEnabled: true,
277+
storageProvider: 'blob',
278+
})
279+
280+
const { POST } = await import('@/app/api/files/presigned/route')
281+
282+
const request = new NextRequest('http://localhost:3000/api/files/presigned?type=chat', {
283+
method: 'POST',
284+
body: JSON.stringify({
285+
fileName: 'chat-logo.png',
286+
contentType: 'image/png',
287+
fileSize: 4096,
288+
}),
289+
})
290+
291+
const response = await POST(request)
292+
const data = await response.json()
293+
294+
expect(response.status).toBe(200)
295+
expect(data.fileInfo.key).toMatch(/^chat\/.*chat-logo\.png$/)
296+
expect(data.fileInfo.path).toContain(
297+
'https://testaccount.blob.core.windows.net/test-container'
298+
)
299+
expect(data.directUploadSupported).toBe(true)
300+
expect(data.uploadHeaders).toMatchObject({
301+
'x-ms-blob-type': 'BlockBlob',
302+
'x-ms-blob-content-type': 'image/png',
303+
'x-ms-meta-originalname': expect.any(String),
304+
'x-ms-meta-uploadedat': '2024-01-01T00:00:00.000Z',
305+
'x-ms-meta-purpose': 'chat',
306+
})
307+
})
308+
246309
it('should return error for unknown storage provider', async () => {
247310
// For unknown provider, we'll need to mock manually since our helper doesn't support it
248311
vi.doMock('@/lib/uploads', () => ({

apps/sim/app/api/files/presigned/route.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import { createLogger } from '@/lib/logs/console/logger'
66
import { getStorageProvider, isUsingCloudStorage } from '@/lib/uploads'
77
import { getBlobServiceClient } from '@/lib/uploads/blob/blob-client'
88
import { getS3Client, sanitizeFilenameForMetadata } from '@/lib/uploads/s3/s3-client'
9-
import { BLOB_CONFIG, BLOB_KB_CONFIG, S3_CONFIG, S3_KB_CONFIG } from '@/lib/uploads/setup'
9+
import {
10+
BLOB_CHAT_CONFIG,
11+
BLOB_CONFIG,
12+
BLOB_KB_CONFIG,
13+
S3_CHAT_CONFIG,
14+
S3_CONFIG,
15+
S3_KB_CONFIG,
16+
} from '@/lib/uploads/setup'
1017
import { createErrorResponse, createOptionsResponse } from '@/app/api/files/utils'
1118

1219
const logger = createLogger('PresignedUploadAPI')
@@ -17,7 +24,7 @@ interface PresignedUrlRequest {
1724
fileSize: number
1825
}
1926

20-
type UploadType = 'general' | 'knowledge-base'
27+
type UploadType = 'general' | 'knowledge-base' | 'chat'
2128

2229
class PresignedUrlError extends Error {
2330
constructor(
@@ -72,7 +79,11 @@ export async function POST(request: NextRequest) {
7279

7380
const uploadTypeParam = request.nextUrl.searchParams.get('type')
7481
const uploadType: UploadType =
75-
uploadTypeParam === 'knowledge-base' ? 'knowledge-base' : 'general'
82+
uploadTypeParam === 'knowledge-base'
83+
? 'knowledge-base'
84+
: uploadTypeParam === 'chat'
85+
? 'chat'
86+
: 'general'
7687

7788
if (!isUsingCloudStorage()) {
7889
throw new StorageConfigError(
@@ -118,14 +129,19 @@ async function handleS3PresignedUrl(
118129
uploadType: UploadType
119130
) {
120131
try {
121-
const config = uploadType === 'knowledge-base' ? S3_KB_CONFIG : S3_CONFIG
132+
const config =
133+
uploadType === 'knowledge-base'
134+
? S3_KB_CONFIG
135+
: uploadType === 'chat'
136+
? S3_CHAT_CONFIG
137+
: S3_CONFIG
122138

123139
if (!config.bucket || !config.region) {
124140
throw new StorageConfigError(`S3 configuration missing for ${uploadType} uploads`)
125141
}
126142

127143
const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_')
128-
const prefix = uploadType === 'knowledge-base' ? 'kb/' : ''
144+
const prefix = uploadType === 'knowledge-base' ? 'kb/' : uploadType === 'chat' ? 'chat/' : ''
129145
const uniqueKey = `${prefix}${Date.now()}-${uuidv4()}-${safeFileName}`
130146

131147
const sanitizedOriginalName = sanitizeFilenameForMetadata(fileName)
@@ -137,6 +153,8 @@ async function handleS3PresignedUrl(
137153

138154
if (uploadType === 'knowledge-base') {
139155
metadata.purpose = 'knowledge-base'
156+
} else if (uploadType === 'chat') {
157+
metadata.purpose = 'chat'
140158
}
141159

142160
const command = new PutObjectCommand({
@@ -156,14 +174,22 @@ async function handleS3PresignedUrl(
156174
)
157175
}
158176

159-
const servePath = `/api/files/serve/s3/${encodeURIComponent(uniqueKey)}`
177+
// For chat images, use direct S3 URLs since they need to be permanently accessible
178+
// For other files, use serve path for access control
179+
const finalPath =
180+
uploadType === 'chat'
181+
? `https://${config.bucket}.s3.${config.region}.amazonaws.com/${uniqueKey}`
182+
: `/api/files/serve/s3/${encodeURIComponent(uniqueKey)}`
160183

161184
logger.info(`Generated ${uploadType} S3 presigned URL for ${fileName} (${uniqueKey})`)
185+
logger.info(`Presigned URL: ${presignedUrl}`)
186+
logger.info(`Final path: ${finalPath}`)
162187

163188
return NextResponse.json({
164189
presignedUrl,
190+
uploadUrl: presignedUrl, // Make sure we're returning the uploadUrl field
165191
fileInfo: {
166-
path: servePath,
192+
path: finalPath,
167193
key: uniqueKey,
168194
name: fileName,
169195
size: fileSize,
@@ -187,7 +213,12 @@ async function handleBlobPresignedUrl(
187213
uploadType: UploadType
188214
) {
189215
try {
190-
const config = uploadType === 'knowledge-base' ? BLOB_KB_CONFIG : BLOB_CONFIG
216+
const config =
217+
uploadType === 'knowledge-base'
218+
? BLOB_KB_CONFIG
219+
: uploadType === 'chat'
220+
? BLOB_CHAT_CONFIG
221+
: BLOB_CONFIG
191222

192223
if (
193224
!config.accountName ||
@@ -198,7 +229,7 @@ async function handleBlobPresignedUrl(
198229
}
199230

200231
const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_')
201-
const prefix = uploadType === 'knowledge-base' ? 'kb/' : ''
232+
const prefix = uploadType === 'knowledge-base' ? 'kb/' : uploadType === 'chat' ? 'chat/' : ''
202233
const uniqueKey = `${prefix}${Date.now()}-${uuidv4()}-${safeFileName}`
203234

204235
const blobServiceClient = getBlobServiceClient()
@@ -231,7 +262,12 @@ async function handleBlobPresignedUrl(
231262

232263
const presignedUrl = `${blockBlobClient.url}?${sasToken}`
233264

234-
const servePath = `/api/files/serve/blob/${encodeURIComponent(uniqueKey)}`
265+
// For chat images, use direct Blob URLs since they need to be permanently accessible
266+
// For other files, use serve path for access control
267+
const finalPath =
268+
uploadType === 'chat'
269+
? blockBlobClient.url
270+
: `/api/files/serve/blob/${encodeURIComponent(uniqueKey)}`
235271

236272
logger.info(`Generated ${uploadType} Azure Blob presigned URL for ${fileName} (${uniqueKey})`)
237273

@@ -244,12 +280,14 @@ async function handleBlobPresignedUrl(
244280

245281
if (uploadType === 'knowledge-base') {
246282
uploadHeaders['x-ms-meta-purpose'] = 'knowledge-base'
283+
} else if (uploadType === 'chat') {
284+
uploadHeaders['x-ms-meta-purpose'] = 'chat'
247285
}
248286

249287
return NextResponse.json({
250288
presignedUrl,
251289
fileInfo: {
252-
path: servePath,
290+
path: finalPath,
253291
key: uniqueKey,
254292
name: fileName,
255293
size: fileSize,

apps/sim/app/chat/[subdomain]/chat-client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface ChatConfig {
3030
customizations: {
3131
primaryColor?: string
3232
logoUrl?: string
33+
imageUrl?: string
3334
welcomeMessage?: string
3435
headerText?: string
3536
}

apps/sim/app/chat/[subdomain]/components/header/header.tsx

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface ChatHeaderProps {
88
customizations?: {
99
headerText?: string
1010
logoUrl?: string
11+
imageUrl?: string
1112
primaryColor?: string
1213
}
1314
} | null
@@ -16,18 +17,58 @@ interface ChatHeaderProps {
1617

1718
export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) {
1819
const primaryColor = chatConfig?.customizations?.primaryColor || '#701FFC'
20+
const customImage = chatConfig?.customizations?.imageUrl || chatConfig?.customizations?.logoUrl
1921

2022
return (
21-
<div className='flex items-center justify-between bg-background/95 px-5 py-3 pt-4 backdrop-blur supports-[backdrop-filter]:bg-background/60 md:px-6 md:pt-3'>
22-
<div className='flex items-center gap-3'>
23-
{chatConfig?.customizations?.logoUrl && (
23+
<div className='flex items-center justify-between bg-background/95 px-6 py-4 pt-6 backdrop-blur supports-[backdrop-filter]:bg-background/60 md:px-8 md:pt-4'>
24+
<div className='flex items-center gap-4'>
25+
{customImage ? (
2426
<img
25-
src={chatConfig.customizations.logoUrl}
27+
src={customImage}
2628
alt={`${chatConfig?.title || 'Chat'} logo`}
27-
className='h-7 w-7 rounded-md object-contain'
29+
className='h-12 w-12 rounded-md object-cover'
2830
/>
31+
) : (
32+
// Default Sim Studio logo when no custom image is provided
33+
<div
34+
className='flex h-12 w-12 items-center justify-center rounded-md'
35+
style={{ backgroundColor: primaryColor }}
36+
>
37+
<svg
38+
width='20'
39+
height='20'
40+
viewBox='0 0 50 50'
41+
fill='none'
42+
xmlns='http://www.w3.org/2000/svg'
43+
>
44+
<path
45+
d='M34.1455 20.0728H16.0364C12.7026 20.0728 10 22.7753 10 26.1091V35.1637C10 38.4975 12.7026 41.2 16.0364 41.2H34.1455C37.4792 41.2 40.1818 38.4975 40.1818 35.1637V26.1091C40.1818 22.7753 37.4792 20.0728 34.1455 20.0728Z'
46+
fill={primaryColor}
47+
stroke='white'
48+
strokeWidth='3.5'
49+
strokeLinecap='round'
50+
strokeLinejoin='round'
51+
/>
52+
<path
53+
d='M25.0919 14.0364C26.7588 14.0364 28.1101 12.6851 28.1101 11.0182C28.1101 9.35129 26.7588 8 25.0919 8C23.425 8 22.0737 9.35129 22.0737 11.0182C22.0737 12.6851 23.425 14.0364 25.0919 14.0364Z'
54+
fill={primaryColor}
55+
stroke='white'
56+
strokeWidth='4'
57+
strokeLinecap='round'
58+
strokeLinejoin='round'
59+
/>
60+
<path
61+
d='M25.0915 14.856V19.0277M20.5645 32.1398V29.1216M29.619 29.1216V32.1398'
62+
stroke='white'
63+
strokeWidth='4'
64+
strokeLinecap='round'
65+
strokeLinejoin='round'
66+
/>
67+
<circle cx='25' cy='11' r='2' fill={primaryColor} />
68+
</svg>
69+
</div>
2970
)}
30-
<h2 className='font-medium text-base'>
71+
<h2 className='font-medium text-lg'>
3172
{chatConfig?.customizations?.headerText || chatConfig?.title || 'Chat'}
3273
</h2>
3374
</div>
@@ -39,8 +80,8 @@ export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) {
3980
target='_blank'
4081
rel='noopener noreferrer'
4182
>
42-
<GithubIcon className='h-[18px] w-[18px]' />
43-
<span className='hidden font-medium text-xs sm:inline-block'>{starCount}</span>
83+
<GithubIcon className='h-5 w-5' />
84+
<span className='hidden font-medium text-sm sm:inline-block'>{starCount}</span>
4485
</a>
4586
<a
4687
href='https://sim.ai'
@@ -49,7 +90,7 @@ export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) {
4990
className='flex items-center rounded-md p-1 text-foreground/80 transition-colors duration-200 hover:text-foreground/100'
5091
>
5192
<div
52-
className='flex h-6 w-6 items-center justify-center rounded-md'
93+
className='flex h-7 w-7 items-center justify-center rounded-md'
5394
style={{ backgroundColor: primaryColor }}
5495
>
5596
<svg

0 commit comments

Comments
 (0)