Skip to content

Commit c9d6e05

Browse files
committed
Consolidate file attachments
1 parent 1959edd commit c9d6e05

7 files changed

Lines changed: 72 additions & 159 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use client'
2+
3+
import { getDocumentIcon } from '@/components/icons/document-icons'
4+
import { cn } from '@/lib/core/utils/cn'
5+
import type { ChatMessageAttachment } from '../types'
6+
7+
function FileAttachmentPill(props: { mediaType: string; filename: string }) {
8+
const Icon = getDocumentIcon(props.mediaType, props.filename)
9+
return (
10+
<div className='flex max-w-[140px] items-center gap-[5px] rounded-[10px] bg-[var(--surface-5)] px-[6px] py-[3px]'>
11+
<Icon className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-icon)]' />
12+
<span className='truncate text-[11px] text-[var(--text-body)]'>{props.filename}</span>
13+
</div>
14+
)
15+
}
16+
17+
export function ChatMessageAttachments(props: {
18+
attachments: ChatMessageAttachment[]
19+
align?: 'start' | 'end'
20+
className?: string
21+
}) {
22+
const { attachments, align = 'end', className } = props
23+
24+
if (!attachments.length) return null
25+
26+
return (
27+
<div
28+
className={cn(
29+
'flex flex-wrap gap-[6px]',
30+
align === 'end' ? 'justify-end' : 'justify-start',
31+
className
32+
)}
33+
>
34+
{attachments.map((att) => {
35+
const isImage = att.media_type.startsWith('image/')
36+
return isImage && att.previewUrl ? (
37+
<div key={att.id} className='h-[56px] w-[56px] overflow-hidden rounded-[8px]'>
38+
<img src={att.previewUrl} alt={att.filename} className='h-full w-full object-cover' />
39+
</div>
40+
) : (
41+
<FileAttachmentPill key={att.id} mediaType={att.media_type} filename={att.filename} />
42+
)
43+
})}
44+
</div>
45+
)
46+
}

apps/sim/app/workspace/[workspaceId]/home/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export {
55
export { MothershipView } from './mothership-view'
66
export { QueuedMessages } from './queued-messages'
77
export { TemplatePrompts } from './template-prompts'
8+
export { ChatMessageAttachments } from './chat-message-attachments'
89
export { UserInput } from './user-input'
910
export { UserMessageContent } from './user-message-content'

apps/sim/app/workspace/[workspaceId]/home/home.tsx

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
44
import { createLogger } from '@sim/logger'
55
import { useParams, useRouter } from 'next/navigation'
66
import { PanelLeft } from '@/components/emcn/icons'
7-
import { getDocumentIcon } from '@/components/icons/document-icons'
87
import { useSession } from '@/lib/auth/auth-client'
98
import {
109
LandingPromptStorage,
@@ -18,6 +17,7 @@ import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks'
1817
import type { ChatContext } from '@/stores/panel'
1918
import {
2019
assistantMessageHasRenderableContent,
20+
ChatMessageAttachments,
2121
MessageContent,
2222
MothershipView,
2323
QueuedMessages,
@@ -31,21 +31,6 @@ import type { FileAttachmentForApi, MothershipResource, MothershipResourceType }
3131

3232
const logger = createLogger('Home')
3333

34-
interface FileAttachmentPillProps {
35-
mediaType: string
36-
filename: string
37-
}
38-
39-
function FileAttachmentPill({ mediaType, filename }: FileAttachmentPillProps) {
40-
const Icon = getDocumentIcon(mediaType, filename)
41-
return (
42-
<div className='flex max-w-[140px] items-center gap-[5px] rounded-[10px] bg-[var(--surface-5)] px-[6px] py-[3px]'>
43-
<Icon className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-icon)]' />
44-
<span className='truncate text-[11px] text-[var(--text-body)]'>{filename}</span>
45-
</div>
46-
)
47-
}
48-
4934
interface HomeProps {
5035
chatId?: string
5136
}
@@ -377,29 +362,11 @@ export function Home({ chatId }: HomeProps = {}) {
377362
return (
378363
<div key={msg.id} className='flex flex-col items-end gap-[6px] pt-3'>
379364
{hasAttachments && (
380-
<div className='flex max-w-[70%] flex-wrap justify-end gap-[6px]'>
381-
{msg.attachments!.map((att) => {
382-
const isImage = att.media_type.startsWith('image/')
383-
return isImage && att.previewUrl ? (
384-
<div
385-
key={att.id}
386-
className='h-[56px] w-[56px] overflow-hidden rounded-[8px]'
387-
>
388-
<img
389-
src={att.previewUrl}
390-
alt={att.filename}
391-
className='h-full w-full object-cover'
392-
/>
393-
</div>
394-
) : (
395-
<FileAttachmentPill
396-
key={att.id}
397-
mediaType={att.media_type}
398-
filename={att.filename}
399-
/>
400-
)
401-
})}
402-
</div>
365+
<ChatMessageAttachments
366+
attachments={msg.attachments!}
367+
align='end'
368+
className='max-w-[70%]'
369+
/>
403370
)}
404371
<div className='max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2'>
405372
<UserMessageContent content={msg.content} contexts={msg.contexts} />

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
ChatMessage,
3939
OutputSelect,
4040
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components'
41+
import type { ChatMessageAttachment } from '@/app/workspace/[workspaceId]/home/types'
4142
import { useChatFileUpload } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks'
4243
import {
4344
usePreventZoom,
@@ -84,17 +85,6 @@ interface ChatFile {
8485
file: File
8586
}
8687

87-
/**
88-
* Represents a processed file attachment with data URL for display
89-
*/
90-
interface ProcessedAttachment {
91-
id: string
92-
name: string
93-
type: string
94-
size: number
95-
dataUrl: string
96-
}
97-
9888
/** Timeout for FileReader operations in milliseconds */
9989
const FILE_READ_TIMEOUT_MS = 60000
10090

@@ -103,13 +93,13 @@ const FILE_READ_TIMEOUT_MS = 60000
10393
* @param chatFiles - Array of chat files to process
10494
* @returns Promise resolving to array of files with data URLs for images
10595
*/
106-
const processFileAttachments = async (chatFiles: ChatFile[]): Promise<ProcessedAttachment[]> => {
96+
const processFileAttachments = async (chatFiles: ChatFile[]): Promise<ChatMessageAttachment[]> => {
10797
return Promise.all(
10898
chatFiles.map(async (file) => {
109-
let dataUrl = ''
99+
let previewUrl: string | undefined
110100
if (file.type.startsWith('image/')) {
111101
try {
112-
dataUrl = await new Promise<string>((resolve, reject) => {
102+
previewUrl = await new Promise<string>((resolve, reject) => {
113103
const reader = new FileReader()
114104
let settled = false
115105

@@ -150,10 +140,10 @@ const processFileAttachments = async (chatFiles: ChatFile[]): Promise<ProcessedA
150140
}
151141
return {
152142
id: file.id,
153-
name: file.name,
154-
type: file.type,
143+
filename: file.name,
144+
media_type: file.type,
155145
size: file.size,
156-
dataUrl,
146+
previewUrl,
157147
}
158148
})
159149
)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx

Lines changed: 8 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
import { useMemo } from 'react'
2-
import { FileText } from 'lucide-react'
32
import { useThrottledValue } from '@/hooks/use-throttled-value'
4-
5-
function StreamingIndicator() {
6-
return <span className='inline-block h-[14px] w-[6px] animate-pulse bg-current opacity-70' />
7-
}
8-
9-
interface ChatAttachment {
10-
id: string
11-
name: string
12-
type: string
13-
dataUrl: string
14-
size?: number
15-
}
3+
import {
4+
ChatMessageAttachments,
5+
} from '@/app/workspace/[workspaceId]/home/components'
6+
import type { ChatMessageAttachment } from '@/app/workspace/[workspaceId]/home/types'
167

178
interface ChatMessageProps {
189
message: {
@@ -21,45 +12,14 @@ interface ChatMessageProps {
2112
timestamp: string | Date
2213
type: 'user' | 'workflow'
2314
isStreaming?: boolean
24-
attachments?: ChatAttachment[]
15+
attachments?: ChatMessageAttachment[]
2516
}
2617
}
2718

2819
const MAX_WORD_LENGTH = 25
2920

30-
/**
31-
* Formats file size in human-readable format
32-
*/
33-
const formatFileSize = (bytes?: number): string => {
34-
if (!bytes || bytes === 0) return ''
35-
const sizes = ['B', 'KB', 'MB', 'GB']
36-
const i = Math.floor(Math.log(bytes) / Math.log(1024))
37-
return `${Math.round((bytes / 1024 ** i) * 10) / 10} ${sizes[i]}`
38-
}
39-
40-
/**
41-
* Opens image attachment in new window
42-
*/
43-
const openImageInNewWindow = (dataUrl: string, fileName: string) => {
44-
const newWindow = window.open('', '_blank')
45-
if (!newWindow) return
46-
47-
newWindow.document.write(`
48-
<!DOCTYPE html>
49-
<html>
50-
<head>
51-
<title>${fileName}</title>
52-
<style>
53-
body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #000; }
54-
img { max-width: 100%; max-height: 100vh; object-fit: contain; }
55-
</style>
56-
</head>
57-
<body>
58-
<img src="${dataUrl}" alt="${fileName}" />
59-
</body>
60-
</html>
61-
`)
62-
newWindow.document.close()
21+
function StreamingIndicator() {
22+
return <span className='inline-block h-[14px] w-[6px] animate-pulse bg-current opacity-70' />
6323
}
6424

6525
/**
@@ -108,54 +68,12 @@ export function ChatMessage({ message }: ChatMessageProps) {
10868
const throttled = useThrottledValue(rawContent)
10969
const formattedContent = message.type === 'user' ? rawContent : throttled
11070

111-
const handleAttachmentClick = (attachment: ChatAttachment) => {
112-
const validDataUrl = attachment.dataUrl?.trim()
113-
if (validDataUrl?.startsWith('data:')) {
114-
openImageInNewWindow(validDataUrl, attachment.name)
115-
}
116-
}
117-
11871
if (message.type === 'user') {
11972
const hasAttachments = message.attachments && message.attachments.length > 0
12073
return (
12174
<div className='w-full max-w-full overflow-hidden opacity-100 transition-opacity duration-200'>
12275
{hasAttachments && (
123-
<div className='mb-[4px] flex flex-wrap gap-[4px]'>
124-
{message.attachments!.map((attachment) => {
125-
const hasValidDataUrl =
126-
attachment.dataUrl?.trim() && attachment.dataUrl.startsWith('data:')
127-
const canDisplayAsImage = attachment.type.startsWith('image/') && hasValidDataUrl
128-
129-
return (
130-
<div
131-
key={attachment.id}
132-
className={`flex max-w-[150px] items-center gap-[5px] rounded-[6px] bg-[var(--surface-2)] px-[5px] py-[3px] ${
133-
hasValidDataUrl ? 'cursor-pointer' : ''
134-
}`}
135-
onClick={(e) => {
136-
if (hasValidDataUrl) {
137-
e.preventDefault()
138-
e.stopPropagation()
139-
handleAttachmentClick(attachment)
140-
}
141-
}}
142-
>
143-
{canDisplayAsImage ? (
144-
<img
145-
src={attachment.dataUrl}
146-
alt={attachment.name}
147-
className='h-[20px] w-[20px] flex-shrink-0 rounded-[3px] object-cover'
148-
/>
149-
) : (
150-
<FileText className='h-[12px] w-[12px] flex-shrink-0 text-[var(--text-tertiary)]' />
151-
)}
152-
<span className='truncate text-[10px] text-[var(--text-secondary)]'>
153-
{attachment.name}
154-
</span>
155-
</div>
156-
)
157-
})}
158-
</div>
76+
<ChatMessageAttachments attachments={message.attachments!} align='start' className='mb-[4px]' />
15977
)}
16078

16179
{formattedContent && !formattedContent.startsWith('Uploaded') && (

apps/sim/stores/chat/store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export const useChatStore = create<ChatState>()(
260260
...msg,
261261
attachments: msg.attachments?.map((att) => ({
262262
...att,
263-
dataUrl: '',
263+
previewUrl: undefined,
264264
})),
265265
})),
266266
}),

apps/sim/stores/chat/types.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ChatMessageAttachment } from '@/app/workspace/[workspaceId]/home/types'
2+
13
/**
24
* Position interface for floating chat
35
*/
@@ -6,17 +8,6 @@ export interface ChatPosition {
68
y: number
79
}
810

9-
/**
10-
* Chat attachment interface
11-
*/
12-
export interface ChatAttachment {
13-
id: string
14-
name: string
15-
type: string
16-
dataUrl: string
17-
size?: number
18-
}
19-
2011
/**
2112
* Chat message interface
2213
*/
@@ -28,7 +19,7 @@ export interface ChatMessage {
2819
timestamp: string
2920
blockId?: string
3021
isStreaming?: boolean
31-
attachments?: ChatAttachment[]
22+
attachments?: ChatMessageAttachment[]
3223
}
3324

3425
/**

0 commit comments

Comments
 (0)