Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fragment } from 'react'
import { Fragment, memo } from 'react'
import {
Button,
ChevronDown,
Expand Down Expand Up @@ -54,7 +54,7 @@ interface ResourceHeaderProps {
actions?: HeaderAction[]
}

export function ResourceHeader({
export const ResourceHeader = memo(function ResourceHeader({
icon: Icon,
title,
breadcrumbs,
Expand Down Expand Up @@ -134,7 +134,7 @@ export function ResourceHeader({
</div>
</div>
)
}
})

function BreadcrumbSegment({
icon: Icon,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import { memo, type ReactNode } from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import {
ArrowDown,
Expand Down Expand Up @@ -65,7 +65,7 @@ interface ResourceOptionsBarProps {
extras?: ReactNode
}

export function ResourceOptionsBar({
export const ResourceOptionsBar = memo(function ResourceOptionsBar({
search,
sort,
filter,
Expand Down Expand Up @@ -172,7 +172,7 @@ export function ResourceOptionsBar({
</div>
</div>
)
}
})

function SortDropdown({ config }: { config: SortConfig }) {
const { options, active, onSort, onClear } = config
Expand Down
168 changes: 106 additions & 62 deletions apps/sim/app/workspace/[workspaceId]/files/files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,73 @@ export function Files() {
}
}, [isDirty])

const handleStartHeaderRename = useCallback(() => {
if (selectedFile) headerRename.startRename(selectedFile.id, selectedFile.name)
}, [selectedFile, headerRename.startRename])

const handleDownloadSelected = useCallback(() => {
if (selectedFile) handleDownload(selectedFile)
}, [selectedFile, handleDownload])

const handleDeleteSelected = useCallback(() => {
if (selectedFile) {
setDeleteTargetFile(selectedFile)
setShowDeleteConfirm(true)
}
}, [selectedFile])

const fileDetailBreadcrumbs = useMemo(
() =>
selectedFile
? [
{ label: 'Files', onClick: handleBackAttempt },
{
label: selectedFile.name,
editing: headerRename.editingId
? {
isEditing: true,
value: headerRename.editValue,
onChange: headerRename.setEditValue,
onSubmit: headerRename.submitRename,
onCancel: headerRename.cancelRename,
}
: undefined,
dropdownItems: [
{
label: 'Rename',
icon: Pencil,
onClick: handleStartHeaderRename,
},
{
label: 'Download',
icon: Download,
onClick: handleDownloadSelected,
},
{
label: 'Delete',
icon: Trash,
onClick: handleDeleteSelected,
},
],
},
]
: [],
[
selectedFile,
handleBackAttempt,
headerRename.editingId,
headerRename.editValue,
headerRename.setEditValue,
headerRename.submitRename,
headerRename.cancelRename,
handleStartHeaderRename,
handleDownloadSelected,
handleDeleteSelected,
]
)

const handleTogglePreview = useCallback(() => setShowPreview((prev) => !prev), [])

const handleDiscardChanges = useCallback(() => {
setShowUnsavedChangesAlert(false)
setIsDirty(false)
Expand Down Expand Up @@ -427,25 +494,11 @@ export function Files() {
return () => window.removeEventListener('beforeunload', handler)
}, [isDirty])

if (selectedFileId && !selectedFile) {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<ResourceHeader
icon={FilesIcon}
breadcrumbs={[
{ label: 'Files', onClick: () => setSelectedFileId(null) },
{ label: '...' },
]}
/>
<div className='flex flex-1 items-center justify-center'>
<Skeleton className='h-[16px] w-[200px]' />
</div>
</div>
)
}

if (selectedFile) {
const fileActions = useMemo<HeaderAction[]>(() => {
if (!selectedFile) return []
const canEditText = isTextEditable(selectedFile)
const canPreview = isPreviewable(selectedFile)

const saveLabel =
saveStatus === 'saving'
? 'Saving...'
Expand All @@ -455,15 +508,13 @@ export function Files() {
? 'Save failed'
: 'Save'

const canPreview = isPreviewable(selectedFile)

const fileActions: HeaderAction[] = [
return [
...(canPreview
? [
{
label: showPreview ? 'Hide Preview' : 'Preview',
icon: Eye,
onClick: () => setShowPreview((prev) => !prev),
onClick: handleTogglePreview,
},
]
: []),
Expand All @@ -482,58 +533,51 @@ export function Files() {
{
label: 'Download',
icon: Download,
onClick: () => handleDownload(selectedFile),
onClick: handleDownloadSelected,
},
{
label: 'Delete',
icon: Trash,
onClick: () => {
setDeleteTargetFile(selectedFile)
setShowDeleteConfirm(true)
},
onClick: handleDeleteSelected,
},
]
}, [
selectedFile,
saveStatus,
showPreview,
handleTogglePreview,
handleSave,
isDirty,
handleDownloadSelected,
handleDeleteSelected,
])

if (selectedFileId && !selectedFile) {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<ResourceHeader
icon={FilesIcon}
breadcrumbs={[
{ label: 'Files', onClick: () => setSelectedFileId(null) },
{ label: '...' },
]}
/>
<div className='flex flex-1 items-center justify-center'>
<Skeleton className='h-[16px] w-[200px]' />
</div>
</div>
)
}

if (selectedFile) {
const canPreview = isPreviewable(selectedFile)

return (
<>
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<ResourceHeader
icon={FilesIcon}
breadcrumbs={[
{ label: 'Files', onClick: handleBackAttempt },
{
label: selectedFile.name,
editing: headerRename.editingId
? {
isEditing: true,
value: headerRename.editValue,
onChange: headerRename.setEditValue,
onSubmit: headerRename.submitRename,
onCancel: headerRename.cancelRename,
}
: undefined,
dropdownItems: [
{
label: 'Rename',
icon: Pencil,
onClick: () => headerRename.startRename(selectedFile.id, selectedFile.name),
},
{
label: 'Download',
icon: Download,
onClick: () => handleDownload(selectedFile),
},
{
label: 'Delete',
icon: Trash,
onClick: () => {
setDeleteTargetFile(selectedFile)
setShowDeleteConfirm(true)
},
},
],
},
]}
breadcrumbs={fileDetailBreadcrumbs}
actions={fileActions}
/>
<FileViewer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,29 @@ const ACCEPTED_FILE_TYPES =
'image/*,.pdf,.txt,.csv,.md,.html,.json,.xml,text/plain,text/csv,text/markdown,text/html,application/json,application/xml,application/pdf'

interface UserInputProps {
value: string
onChange: (value: string) => void
onSubmit: (fileAttachments?: FileAttachmentForApi[]) => void
defaultValue?: string
onSubmit: (text: string, fileAttachments?: FileAttachmentForApi[]) => void
isSending: boolean
onStopGeneration: () => void
isInitialView?: boolean
userId?: string
}

export function UserInput({
value,
onChange,
defaultValue = '',
onSubmit,
isSending,
onStopGeneration,
isInitialView = true,
userId,
}: UserInputProps) {
const animatedPlaceholder = useAnimatedPlaceholder()
const [value, setValue] = useState(defaultValue)

useEffect(() => {
if (defaultValue) setValue(defaultValue)
}, [defaultValue])

const animatedPlaceholder = useAnimatedPlaceholder(isInitialView)
const placeholder = isInitialView ? animatedPlaceholder : 'Send message to Sim'

const files = useFileAttachments({ userId, disabled: false, isLoading: isSending })
Expand Down Expand Up @@ -95,13 +99,14 @@ export function UserInput({
size: f.size,
}))

onSubmit(fileAttachmentsForApi.length > 0 ? fileAttachmentsForApi : undefined)
onSubmit(value, fileAttachmentsForApi.length > 0 ? fileAttachmentsForApi : undefined)
setValue('')
files.clearAttachedFiles()

if (textareaRef.current) {
textareaRef.current.style.height = 'auto'
}
}, [onSubmit, files])
}, [onSubmit, files, value])

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
Expand Down Expand Up @@ -149,7 +154,7 @@ export function UserInput({
transcript += event.results[i][0].transcript
}
const prefix = prefixRef.current
onChange(prefix ? `${prefix} ${transcript}` : transcript)
setValue(prefix ? `${prefix} ${transcript}` : transcript)
}

recognition.onend = () => {
Expand All @@ -172,7 +177,7 @@ export function UserInput({
recognitionRef.current = recognition
recognition.start()
setIsListening(true)
}, [isListening, value, onChange])
}, [isListening, value])

return (
<div
Expand All @@ -195,7 +200,7 @@ export function UserInput({
return (
<div
key={file.id}
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)] transition-all hover:bg-[var(--surface-4)]'
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)]'
title={`${file.name} (${files.formatFileSize(file.size)})`}
onClick={() => files.handleFileClick(file)}
>
Expand Down Expand Up @@ -229,7 +234,7 @@ export function UserInput({
e.stopPropagation()
files.removeFile(file.id)
}}
className='absolute top-[2px] right-[2px] flex h-[16px] w-[16px] items-center justify-center rounded-full bg-black/60 opacity-0 transition-opacity group-hover:opacity-100'
className='absolute top-[2px] right-[2px] flex h-[16px] w-[16px] items-center justify-center rounded-full bg-black/60 opacity-0 group-hover:opacity-100'
>
<X className='h-[10px] w-[10px] text-white' />
</button>
Expand All @@ -243,7 +248,7 @@ export function UserInput({
<textarea
ref={textareaRef}
value={value}
onChange={(e) => onChange(e.target.value)}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
onInput={handleInput}
placeholder={files.isDragging ? 'Drop files here...' : placeholder}
Expand Down
Loading