Skip to content

Commit f86e67d

Browse files
committed
improvement: loading and file dropping
1 parent c8098d3 commit f86e67d

File tree

6 files changed

+194
-95
lines changed

6 files changed

+194
-95
lines changed

apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
Integration,
1515
Library,
1616
Pencil,
17-
Play,
17+
PlayOutline,
1818
Rocket,
1919
Search,
2020
Settings,
@@ -39,13 +39,13 @@ const TOOL_ICONS: Record<MothershipToolName | SubagentName | 'mothership', IconC
3939
manage_skill: Asterisk,
4040
user_memory: Database,
4141
function_execute: TerminalWindow,
42-
superagent: Play,
42+
superagent: Blimp,
4343
user_table: TableIcon,
4444
workspace_file: File,
4545
create_workflow: Connections,
4646
edit_workflow: Pencil,
4747
build: Hammer,
48-
run: Play,
48+
run: PlayOutline,
4949
deploy: Rocket,
5050
auth: Integration,
5151
knowledge: Database,

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import type { ElementType, SVGProps } from 'react'
4-
import { Button } from '@/components/emcn'
4+
import { Button, Tooltip } from '@/components/emcn'
55
import { PanelLeft, Table as TableIcon } from '@/components/emcn/icons'
66
import { WorkflowIcon } from '@/components/icons'
77
import { getDocumentIcon } from '@/components/icons/document-icons'
@@ -76,47 +76,67 @@ export function ResourceTabs({
7676
}: ResourceTabsProps) {
7777
return (
7878
<div className='flex shrink-0 items-center border-[var(--border)] border-b px-[16px] py-[8.5px]'>
79-
<Button
80-
variant='subtle'
81-
onClick={onCollapse}
82-
className='shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
83-
aria-label='Collapse resource view'
84-
>
85-
<PanelLeft className='-scale-x-100 h-[16px] w-[16px] text-[var(--text-icon)]' />
86-
</Button>
87-
<div className='flex min-w-0 items-center gap-[6px] overflow-x-auto pl-[6px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
79+
<Tooltip.Root>
80+
<Tooltip.Trigger asChild>
81+
<Button
82+
variant='subtle'
83+
onClick={onCollapse}
84+
className='shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
85+
aria-label='Collapse resource view'
86+
>
87+
<PanelLeft className='-scale-x-100 h-[16px] w-[16px] text-[var(--text-icon)]' />
88+
</Button>
89+
</Tooltip.Trigger>
90+
<Tooltip.Content side='bottom'>
91+
<p>Collapse</p>
92+
</Tooltip.Content>
93+
</Tooltip.Root>
94+
<div className='mx-[2px] flex min-w-0 items-center gap-[6px] overflow-x-auto px-[6px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
8895
{resources.map((resource) => {
8996
const Icon = getResourceIcon(resource)
9097
const isActive = activeId === resource.id
9198

9299
return (
93-
<Button
94-
key={resource.id}
95-
variant='subtle'
96-
onClick={() => onSelect(resource.id)}
97-
className={cn(
98-
'shrink-0 bg-transparent px-[8px] py-[4px] text-[12px]',
99-
isActive && 'bg-[var(--surface-4)]'
100-
)}
101-
>
102-
<Icon className={cn('mr-[6px] h-[14px] w-[14px] text-[var(--text-icon)]')} />
103-
{resource.title}
104-
</Button>
100+
<Tooltip.Root key={resource.id}>
101+
<Tooltip.Trigger asChild>
102+
<Button
103+
variant='subtle'
104+
onClick={() => onSelect(resource.id)}
105+
className={cn(
106+
'shrink-0 bg-transparent px-[8px] py-[4px] text-[12px]',
107+
isActive && 'bg-[var(--surface-4)]'
108+
)}
109+
>
110+
<Icon className={cn('mr-[6px] h-[14px] w-[14px] text-[var(--text-icon)]')} />
111+
{resource.title}
112+
</Button>
113+
</Tooltip.Trigger>
114+
<Tooltip.Content side='bottom'>
115+
<p>{resource.title}</p>
116+
</Tooltip.Content>
117+
</Tooltip.Root>
105118
)
106119
})}
107120
</div>
108121
{previewMode && onCyclePreviewMode && (
109-
<Button
110-
variant='subtle'
111-
onClick={onCyclePreviewMode}
112-
className='ml-auto shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
113-
aria-label='Cycle preview mode'
114-
>
115-
<PreviewModeIcon
116-
mode={previewMode}
117-
className='h-[16px] w-[16px] text-[var(--text-icon)]'
118-
/>
119-
</Button>
122+
<Tooltip.Root>
123+
<Tooltip.Trigger asChild>
124+
<Button
125+
variant='subtle'
126+
onClick={onCyclePreviewMode}
127+
className='ml-auto shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
128+
aria-label='Cycle preview mode'
129+
>
130+
<PreviewModeIcon
131+
mode={previewMode}
132+
className='h-[16px] w-[16px] text-[var(--text-icon)]'
133+
/>
134+
</Button>
135+
</Tooltip.Trigger>
136+
<Tooltip.Content side='bottom'>
137+
<p>Preview mode</p>
138+
</Tooltip.Content>
139+
</Tooltip.Root>
120140
)}
121141
</div>
122142
)

apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx

Lines changed: 86 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
'use client'
22

33
import { useCallback, useEffect, useRef, useState } from 'react'
4-
import { ArrowUp, FileText, Loader2, Mic, Paperclip, X } from 'lucide-react'
5-
import { Button } from '@/components/emcn'
4+
import { ArrowUp, Loader2, Mic, Paperclip, X } from 'lucide-react'
5+
import { Button, Tooltip } from '@/components/emcn'
6+
import {
7+
AudioIcon,
8+
CsvIcon,
9+
DocxIcon,
10+
getDocumentIcon,
11+
JsonIcon,
12+
MarkdownIcon,
13+
PdfIcon,
14+
TxtIcon,
15+
VideoIcon,
16+
XlsxIcon,
17+
} from '@/components/icons/document-icons'
618
import { cn } from '@/lib/core/utils/cn'
719
import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
820
import { useFileAttachments } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments'
@@ -23,7 +35,19 @@ const SEND_BUTTON_ACTIVE =
2335
'bg-[var(--c-383838)] hover:bg-[var(--c-575757)] dark:bg-[var(--c-E0E0E0)] dark:hover:bg-[var(--c-CFCFCF)]'
2436
const SEND_BUTTON_DISABLED = 'bg-[var(--c-808080)] dark:bg-[var(--c-808080)]'
2537

26-
const MAX_CHAT_TEXTAREA_HEIGHT = 200 // 8 lines × 24px line-height + 8px padding
38+
const MAX_CHAT_TEXTAREA_HEIGHT = 200
39+
40+
const DROP_OVERLAY_ICONS = [
41+
PdfIcon,
42+
DocxIcon,
43+
XlsxIcon,
44+
CsvIcon,
45+
TxtIcon,
46+
MarkdownIcon,
47+
JsonIcon,
48+
AudioIcon,
49+
VideoIcon,
50+
] as const
2751

2852
function autoResizeTextarea(e: React.FormEvent<HTMLTextAreaElement>, maxHeight: number) {
2953
const target = e.target as HTMLTextAreaElement
@@ -189,9 +213,8 @@ export function UserInput({
189213
<div
190214
onClick={handleContainerClick}
191215
className={cn(
192-
'mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-[10px] py-[8px] dark:bg-[var(--surface-4)]',
193-
isInitialView && 'shadow-sm',
194-
files.isDragging && 'ring-[1.75px] ring-[var(--brand-secondary)]'
216+
'relative mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-[10px] py-[8px] dark:bg-[var(--surface-4)]',
217+
isInitialView && 'shadow-sm'
195218
)}
196219
onDragEnter={files.handleDragEnter}
197220
onDragLeave={files.handleDragLeave}
@@ -204,48 +227,52 @@ export function UserInput({
204227
{files.attachedFiles.map((file) => {
205228
const isImage = file.type.startsWith('image/')
206229
return (
207-
<div
208-
key={file.id}
209-
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)]'
210-
title={`${file.name} (${files.formatFileSize(file.size)})`}
211-
onClick={() => files.handleFileClick(file)}
212-
>
213-
{isImage && file.previewUrl ? (
214-
<img
215-
src={file.previewUrl}
216-
alt={file.name}
217-
className='h-full w-full object-cover'
218-
/>
219-
) : (
220-
<div className='flex h-full w-full flex-col items-center justify-center gap-[2px]'>
221-
{file.type.includes('pdf') ? (
222-
<FileText className='h-[18px] w-[18px] text-red-500' />
230+
<Tooltip.Root key={file.id}>
231+
<Tooltip.Trigger asChild>
232+
<div
233+
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)]'
234+
onClick={() => files.handleFileClick(file)}
235+
>
236+
{isImage && file.previewUrl ? (
237+
<img
238+
src={file.previewUrl}
239+
alt={file.name}
240+
className='h-full w-full object-cover'
241+
/>
223242
) : (
224-
<FileText className='h-[18px] w-[18px] text-blue-500' />
243+
<div className='flex h-full w-full flex-col items-center justify-center gap-[2px] text-[var(--text-icon)]'>
244+
{(() => {
245+
const Icon = getDocumentIcon(file.type, file.name)
246+
return <Icon className='h-[18px] w-[18px]' />
247+
})()}
248+
<span className='max-w-[48px] truncate px-[2px] text-[9px] text-[var(--text-muted)]'>
249+
{file.name.split('.').pop()}
250+
</span>
251+
</div>
252+
)}
253+
{file.uploading && (
254+
<div className='absolute inset-0 flex items-center justify-center bg-black/50'>
255+
<Loader2 className='h-[14px] w-[14px] animate-spin text-white' />
256+
</div>
257+
)}
258+
{!file.uploading && (
259+
<button
260+
type='button'
261+
onClick={(e) => {
262+
e.stopPropagation()
263+
files.removeFile(file.id)
264+
}}
265+
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'
266+
>
267+
<X className='h-[10px] w-[10px] text-white' />
268+
</button>
225269
)}
226-
<span className='max-w-[48px] truncate px-[2px] text-[9px] text-[var(--text-muted)]'>
227-
{file.name.split('.').pop()}
228-
</span>
229-
</div>
230-
)}
231-
{file.uploading && (
232-
<div className='absolute inset-0 flex items-center justify-center bg-black/50'>
233-
<Loader2 className='h-[14px] w-[14px] animate-spin text-white' />
234270
</div>
235-
)}
236-
{!file.uploading && (
237-
<button
238-
type='button'
239-
onClick={(e) => {
240-
e.stopPropagation()
241-
files.removeFile(file.id)
242-
}}
243-
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'
244-
>
245-
<X className='h-[10px] w-[10px] text-white' />
246-
</button>
247-
)}
248-
</div>
271+
</Tooltip.Trigger>
272+
<Tooltip.Content side='top'>
273+
<p className='max-w-[200px] truncate'>{file.name}</p>
274+
</Tooltip.Content>
275+
</Tooltip.Root>
249276
)
250277
})}
251278
</div>
@@ -257,7 +284,7 @@ export function UserInput({
257284
onChange={(e) => setValue(e.target.value)}
258285
onKeyDown={handleKeyDown}
259286
onInput={handleInput}
260-
placeholder={files.isDragging ? 'Drop files here...' : placeholder}
287+
placeholder={placeholder}
261288
rows={1}
262289
className={cn(TEXTAREA_BASE_CLASSES, isInitialView ? 'max-h-[30vh]' : 'max-h-[200px]')}
263290
/>
@@ -319,7 +346,6 @@ export function UserInput({
319346
</div>
320347
</div>
321348

322-
{/* Hidden file input */}
323349
<input
324350
ref={files.fileInputRef}
325351
type='file'
@@ -328,6 +354,19 @@ export function UserInput({
328354
accept={CHAT_ACCEPT_ATTRIBUTE}
329355
multiple
330356
/>
357+
358+
{files.isDragging && (
359+
<div className='pointer-events-none absolute inset-[6px] z-10 flex items-center justify-center rounded-[14px] border-[1.5px] border-[var(--border-1)] border-dashed bg-[var(--white)] dark:bg-[var(--surface-4)]'>
360+
<div className='flex flex-col items-center gap-[8px]'>
361+
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>Drop files</span>
362+
<div className='flex items-center gap-[8px] text-[var(--text-icon)]'>
363+
{DROP_OVERLAY_ICONS.map((Icon, i) => (
364+
<Icon key={i} className='h-[14px] w-[14px]' />
365+
))}
366+
</div>
367+
</div>
368+
</div>
369+
)}
331370
</div>
332371
)
333372
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { FileText } from 'lucide-react'
66
import { useParams, useRouter } from 'next/navigation'
7+
import { Skeleton } from '@/components/emcn'
78
import { PanelLeft } from '@/components/emcn/icons'
89
import { useSession } from '@/lib/auth/auth-client'
910
import {
@@ -13,6 +14,7 @@ import {
1314
LandingWorkflowSeedStorage,
1415
} from '@/lib/core/utils/browser-storage'
1516
import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export'
17+
import { useChatHistory } from '@/hooks/queries/tasks'
1618
import { useSidebarStore } from '@/stores/sidebar/store'
1719
import { MessageContent, MothershipView, UserInput } from './components'
1820
import type { FileAttachmentForApi } from './components/user-input/user-input'
@@ -43,6 +45,23 @@ function ThinkingIndicator() {
4345
)
4446
}
4547

48+
const SKELETON_LINE_COUNT = 4
49+
50+
function ChatSkeleton({ children }: { children: React.ReactNode }) {
51+
return (
52+
<div className='flex h-full flex-col bg-[var(--bg)]'>
53+
<div className='min-h-0 flex-1 overflow-hidden px-6 py-4'>
54+
<div className='mx-auto max-w-[42rem] space-y-[10px] pt-3'>
55+
{Array.from({ length: SKELETON_LINE_COUNT }).map((_, i) => (
56+
<Skeleton key={i} className='h-[16px]' style={{ width: `${120 + (i % 4) * 48}px` }} />
57+
))}
58+
</div>
59+
</div>
60+
<div className='flex-shrink-0 px-[24px] pb-[16px]'>{children}</div>
61+
</div>
62+
)
63+
}
64+
4665
interface HomeProps {
4766
chatId?: string
4867
}
@@ -125,6 +144,8 @@ export function Home({ chatId }: HomeProps = {}) {
125144
}
126145
}, [createWorkflowFromLandingSeed, workspaceId, router])
127146

147+
const { isLoading: isLoadingHistory } = useChatHistory(chatId)
148+
128149
const {
129150
messages,
130151
isSending,
@@ -174,6 +195,20 @@ export function Home({ chatId }: HomeProps = {}) {
174195

175196
const hasMessages = messages.length > 0
176197

198+
if (!hasMessages && chatId && isLoadingHistory) {
199+
return (
200+
<ChatSkeleton>
201+
<UserInput
202+
onSubmit={handleSubmit}
203+
isSending={isSending}
204+
onStopGeneration={stopGeneration}
205+
isInitialView={false}
206+
userId={session?.user?.id}
207+
/>
208+
</ChatSkeleton>
209+
)
210+
}
211+
177212
if (!hasMessages) {
178213
return (
179214
<div className='flex h-full flex-col items-center justify-center bg-[var(--bg)] px-[24px]'>

0 commit comments

Comments
 (0)