feat(mothership): restore attachment previews on draft and add video support (#4435)

* feat(mothership): restore attachment previews on draft and add video support

* fix(mothership): icon fallback behind video preview
This commit is contained in:
Waleed
2026-05-04 12:46:04 -07:00
committed by GitHub
parent 578fc505ec
commit 2f90e41274
7 changed files with 66 additions and 13 deletions
@@ -30,13 +30,36 @@ export function ChatMessageAttachments(props: {
)}
>
{attachments.map((att) => {
const isImage = att.media_type.startsWith('image/')
return isImage && att.previewUrl ? (
if (!att.previewUrl) {
return (
<FileAttachmentPill key={att.id} mediaType={att.media_type} filename={att.filename} />
)
}
const isVideo = att.media_type.startsWith('video/')
if (isVideo) {
const Icon = getDocumentIcon(att.media_type, att.filename)
return (
<div
key={att.id}
className='relative h-[56px] w-[56px] overflow-hidden rounded-[8px] bg-[var(--surface-5)]'
>
<div className='absolute inset-0 flex items-center justify-center text-[var(--text-icon)]'>
<Icon className='h-[18px] w-[18px]' />
</div>
<video
src={att.previewUrl}
muted
playsInline
preload='metadata'
className='relative h-full w-full object-cover'
/>
</div>
)
}
return (
<div key={att.id} className='h-[56px] w-[56px] overflow-hidden rounded-[8px]'>
<img src={att.previewUrl} alt={att.filename} className='h-full w-full object-cover' />
</div>
) : (
<FileAttachmentPill key={att.id} mediaType={att.media_type} filename={att.filename} />
)
})}
</div>
@@ -22,7 +22,8 @@ export const AttachedFilesList = React.memo(function AttachedFilesList({
return (
<div className='mb-1.5 flex flex-wrap gap-1.5'>
{attachedFiles.map((file) => {
const isImage = file.type.startsWith('image/')
const isVideo = file.type.startsWith('video/')
const hasPreview = Boolean(file.previewUrl)
return (
<Tooltip.Root key={file.id}>
<Tooltip.Trigger asChild>
@@ -30,7 +31,23 @@ export const AttachedFilesList = React.memo(function AttachedFilesList({
className='group relative h-[56px] w-[56px] flex-shrink-0 cursor-pointer overflow-hidden rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-5)] hover:bg-[var(--surface-4)]'
onClick={() => onFileClick(file)}
>
{isImage && file.previewUrl ? (
{hasPreview && isVideo ? (
<>
<div className='absolute inset-0 flex items-center justify-center text-[var(--text-icon)]'>
{(() => {
const Icon = getDocumentIcon(file.type, file.name)
return <Icon className='h-[18px] w-[18px]' />
})()}
</div>
<video
src={file.previewUrl}
muted
playsInline
preload='metadata'
className='relative h-full w-full object-cover'
/>
</>
) : hasPreview ? (
<img
src={file.previewUrl}
alt={file.name}
@@ -16,6 +16,7 @@ import { Paperclip } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button, Tooltip } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { getMothershipAttachmentPreviewUrl } from '@/lib/copilot/chat/attachment-preview'
import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
import { cn } from '@/lib/core/utils/cn'
import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
@@ -211,6 +212,7 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
path: a.path ?? '',
key: a.key,
uploading: false,
previewUrl: getMothershipAttachmentPreviewUrl(a),
}))
}
if (typeof draft.text === 'string' && draft.text.length > 0) {
@@ -385,6 +387,7 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
path: a.path ?? '',
key: a.key,
uploading: false,
previewUrl: getMothershipAttachmentPreviewUrl(a),
}))
files.restoreAttachedFiles(restored)
contextManagement.setSelectedContexts(msg.contexts ?? [])
@@ -5,6 +5,7 @@ import { sleep } from '@sim/utils/helpers'
import { generateId } from '@sim/utils/id'
import { useQueryClient } from '@tanstack/react-query'
import { usePathname, useRouter } from 'next/navigation'
import { getMothershipAttachmentPreviewUrl } from '@/lib/copilot/chat/attachment-preview'
import { toDisplayMessage } from '@/lib/copilot/chat/display-message'
import { getLiveAssistantMessageId } from '@/lib/copilot/chat/effective-transcript'
import type {
@@ -3303,9 +3304,7 @@ export function useChat(
filename: f.filename,
media_type: f.media_type,
size: f.size,
previewUrl: f.media_type.startsWith('image/')
? `/api/files/serve/${encodeURIComponent(f.key)}?context=mothership`
: undefined,
previewUrl: getMothershipAttachmentPreviewUrl(f),
}))
const optimisticUserMessage: ChatMessage = {
@@ -126,7 +126,10 @@ export function useFileAttachments(props: UseFileAttachmentsProps) {
type: resolveFileType(file),
path: '',
uploading: true,
previewUrl: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,
previewUrl:
file.type.startsWith('image/') || file.type.startsWith('video/')
? URL.createObjectURL(file)
: undefined,
}))
setAttachedFiles((prev) => [...prev, ...placeholders])
@@ -0,0 +1,9 @@
export function getMothershipAttachmentPreviewUrl(file: {
key: string
media_type: string
}): string | undefined {
if (!file.media_type.startsWith('image/') && !file.media_type.startsWith('video/')) {
return undefined
}
return `/api/files/serve/${encodeURIComponent(file.key)}?context=mothership`
}
+2 -3
View File
@@ -15,6 +15,7 @@ import {
type ToolCallInfo,
ToolCallStatus,
} from '@/app/workspace/[workspaceId]/home/types'
import { getMothershipAttachmentPreviewUrl } from './attachment-preview'
import type { PersistedContentBlock, PersistedMessage } from './persisted-message'
import { withBlockTiming } from './persisted-message'
@@ -91,9 +92,7 @@ function toDisplayAttachment(f: PersistedMessage['fileAttachments']): ChatMessag
filename: a.filename,
media_type: a.media_type,
size: a.size,
previewUrl: a.media_type.startsWith('image/')
? `/api/files/serve/${encodeURIComponent(a.key)}?context=mothership`
: undefined,
previewUrl: getMothershipAttachmentPreviewUrl(a),
}))
}