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 numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react'
import { Square } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn'
import { BookOpen, SquareArrowUpRight } from '@/components/emcn/icons'
import { BookOpen, FileX, SquareArrowUpRight, WorkflowX } from '@/components/emcn/icons'
import {
markRunToolManuallyStopped,
reportManualRunToolStop,
Expand DownExpand Up@@ -62,9 +62,7 @@ export function ResourceContent({ workspaceId, resource, previewMode }: Resource

case 'workflow':
return (
<Suspense fallback={LOADING_SKELETON}>
<Workflow key={resource.id} workspaceId={workspaceId} workflowId={resource.id} embedded />
</Suspense>
<EmbeddedWorkflow key={resource.id} workspaceId={workspaceId} workflowId={resource.id} />
)

case 'knowledgebase':
Expand DownExpand Up@@ -228,6 +226,43 @@ export function EmbeddedKnowledgeBaseActions({
)
}

interface EmbeddedWorkflowProps {
workspaceId: string
workflowId: string
}

function EmbeddedWorkflow({ workspaceId, workflowId }: EmbeddedWorkflowProps) {
const workflowExists = useWorkflowRegistry((state) => Boolean(state.workflows[workflowId]))
const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
const hydrationWorkflowId = useWorkflowRegistry((state) => state.hydration.workflowId)
const isMetadataLoaded = hydrationPhase !== 'idle' && hydrationPhase !== 'metadata-loading'
const hasLoadError = hydrationPhase === 'error' && hydrationWorkflowId === workflowId

if (!isMetadataLoaded) return LOADING_SKELETON

if (!workflowExists || hasLoadError) {
return (
<div className='flex h-full flex-col items-center justify-center gap-[12px]'>
<WorkflowX className='h-[32px] w-[32px] text-[var(--text-muted)]' />
<div className='flex flex-col items-center gap-[4px]'>
<h2 className='font-medium text-[20px] text-[var(--text-secondary)]'>
Workflow not found
</h2>
<p className='text-[13px] text-[var(--text-muted)]'>
This workflow may have been deleted or moved
</p>
</div>
</div>
)
}

return (
<Suspense fallback={LOADING_SKELETON}>
<Workflow workspaceId={workspaceId} workflowId={workflowId} embedded />
</Suspense>
)
}

interface EmbeddedFileProps {
workspaceId: string
fileId: string
Expand All@@ -242,8 +277,14 @@ function EmbeddedFile({ workspaceId, fileId, previewMode }: EmbeddedFileProps) {

if (!file) {
return (
<div className='flex h-full items-center justify-center'>
<span className='text-[13px] text-[var(--text-muted)]'>File not found</span>
<div className='flex h-full flex-col items-center justify-center gap-[12px]'>
<FileX className='h-[32px] w-[32px] text-[var(--text-muted)]' />
<div className='flex flex-col items-center gap-[4px]'>
<h2 className='font-medium text-[20px] text-[var(--text-secondary)]'>File not found</h2>
<p className='text-[13px] text-[var(--text-muted)]'>
This file may have been deleted or moved
</p>
</div>
</div>
)
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -160,12 +160,22 @@ export function ResourceTabs({
[chatId, onRemoveResource]
)

const handleDragStart = useCallback((e: React.DragEvent, idx: number) => {
dragStartIdx.current = idx
setDraggedIdx(idx)
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', String(idx))
}, [])
const handleDragStart = useCallback(
(e: React.DragEvent, idx: number) => {
dragStartIdx.current = idx
setDraggedIdx(idx)
e.dataTransfer.effectAllowed = 'copyMove'
e.dataTransfer.setData('text/plain', String(idx))
const resource = resources[idx]
if (resource) {
e.dataTransfer.setData(
'application/x-sim-resource',
JSON.stringify({ type: resource.type, id: resource.id, title: resource.title })
)
}
},
[resources]
)

const handleDragOver = useCallback((e: React.DragEvent, idx: number) => {
e.preventDefault()
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,19 +31,9 @@ function getContextIcon(ctx: ChatContext) {
switch (ctx.kind) {
case 'workflow':
case 'current_workflow':
return (
<WorkflowPillIcon
workflowId={ctx.workflowId}
className='mr-[4px] h-[10px] w-[10px]'
/>
)
return <WorkflowPillIcon workflowId={ctx.workflowId} className='mr-[4px] h-[10px] w-[10px]' />
case 'workflow_block':
return (
<WorkflowPillIcon
workflowId={ctx.workflowId}
className='mr-[4px] h-[10px] w-[10px]'
/>
)
return <WorkflowPillIcon workflowId={ctx.workflowId} className='mr-[4px] h-[10px] w-[10px]' />
case 'knowledge':
return <Database className='mr-[4px] h-[10px] w-[10px] text-[var(--text-icon)]' />
case 'templates':
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,17 @@ type WindowWithSpeech = Window & {
}

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ArrowUp, AtSign, ChevronRight, Folder, Loader2, Mic, Paperclip, Plus, X } from 'lucide-react'
import {
ArrowUp,
AtSign,
ChevronRight,
Folder,
Loader2,
Mic,
Paperclip,
Plus,
X,
} from 'lucide-react'
import { useParams } from 'next/navigation'
import { createPortal } from 'react-dom'
import {
Expand DownExpand Up@@ -66,7 +76,15 @@ import {
import { useSession } from '@/lib/auth/auth-client'
import { cn } from '@/lib/core/utils/cn'
import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
import { ContextPills } from './components'
import {
type AvailableItem,
useAvailableResources,
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import type {
MothershipResource,
MothershipResourceType,
} from '@/app/workspace/[workspaceId]/home/types'
import {
useCaretViewport,
useContextManagement,
Expand All@@ -78,17 +96,12 @@ import {
computeMentionHighlightRanges,
extractContextTokens,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils'
import type { ChatContext } from '@/stores/panel'
import {
useAvailableResources,
type AvailableItem,
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import type { MothershipResource, MothershipResourceType } from '@/app/workspace/[workspaceId]/home/types'
import { useFolders } from '@/hooks/queries/folders'
import { useFolderStore } from '@/stores/folders/store'
import type { FolderTreeNode } from '@/stores/folders/types'
import type { ChatContext } from '@/stores/panel'
import { useAnimatedPlaceholder } from '../../hooks'
import { ContextPills } from './components'

const TEXTAREA_BASE_CLASSES = cn(
'm-0 box-border h-auto min-h-[24px] w-full resize-none',
Expand DownExpand Up@@ -184,9 +197,7 @@ function ResourceMentionMenu({
)
}
// When no query, show all items flat
return availableResources.flatMap(({ type, items }) =>
items.map((item) => ({ type, item }))
)
return availableResources.flatMap(({ type, items }) => items.map((item) => ({ type, item })))
}, [availableResources, query])

// Reset active index when query changes
Expand DownExpand Up@@ -263,7 +274,9 @@ function ResourceMentionMenu({
onClick={() => handleSelect({ type, id: item.id, title: item.name })}
className={cn(
'flex cursor-pointer items-center gap-[8px] px-[8px] py-[6px] text-[13px]',
index === activeIndex ? 'bg-[var(--surface-active)]' : 'hover:bg-[var(--surface-active)]'
index === activeIndex
? 'bg-[var(--surface-active)]'
: 'hover:bg-[var(--surface-active)]'
)}
>
{config.renderDropdownItem({ item })}
Expand DownExpand Up@@ -292,7 +305,13 @@ interface ResourceTypeFolderProps {
onSelect: (resource: MothershipResource) => void
}

function ResourceTypeFolder({ type, items, config, workspaceId, onSelect }: ResourceTypeFolderProps) {
function ResourceTypeFolder({
type,
items,
config,
workspaceId,
onSelect,
}: ResourceTypeFolderProps) {
const [expanded, setExpanded] = useState(false)
const Icon = config.icon

Expand DownExpand Up@@ -578,7 +597,11 @@ export interface FileAttachmentForApi {

interface UserInputProps {
defaultValue?: string
onSubmit: (text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => void
onSubmit: (
text: string,
fileAttachments?: FileAttachmentForApi[],
contexts?: ChatContext[]
) => void
isSending: boolean
onStopGeneration: () => void
isInitialView?: boolean
Expand DownExpand Up@@ -608,7 +631,11 @@ export function UserInput({
const animatedPlaceholder = useAnimatedPlaceholder(isInitialView)
const placeholder = isInitialView ? animatedPlaceholder : 'Send message to Sim'

const files = useFileAttachments({ userId: userId || session?.user?.id, disabled: false, isLoading: isSending })
const files = useFileAttachments({
userId: userId || session?.user?.id,
disabled: false,
isLoading: isSending,
})
const hasFiles = files.attachedFiles.some((f) => !f.uploading && f.key)

const contextManagement = useContextManagement({ message: value })
Expand DownExpand Up@@ -696,6 +723,38 @@ export function UserInput({
[textareaRef, value, handleContextAdd, mentionMenu]
)

const handleContainerDragOver = useCallback(
(e: React.DragEvent) => {
if (e.dataTransfer.types.includes('application/x-sim-resource')) {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'copy'
return
}
files.handleDragOver(e)
},
[files]
)

const handleContainerDrop = useCallback(
(e: React.DragEvent) => {
const resourceJson = e.dataTransfer.getData('application/x-sim-resource')
if (resourceJson) {
e.preventDefault()
e.stopPropagation()
try {
const resource = JSON.parse(resourceJson) as MothershipResource
handleResourceSelect(resource, false)
} catch {
// Invalid JSON — ignore
}
return
}
files.handleDrop(e)
},
[handleResourceSelect, files]
)
Comment thread
cursor[bot] marked this conversation as resolved.

useEffect(() => {
if (wasSendingRef.current && !isSending) {
textareaRef.current?.focus()
Expand All@@ -709,10 +768,13 @@ export function UserInput({
}
}, [isInitialView, textareaRef])

const handleContainerClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
if ((e.target as HTMLElement).closest('button')) return
textareaRef.current?.focus()
}, [textareaRef])
const handleContainerClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if ((e.target as HTMLElement).closest('button')) return
textareaRef.current?.focus()
},
[textareaRef]
)

const handleSubmit = useCallback(() => {
const fileAttachmentsForApi: FileAttachmentForApi[] = files.attachedFiles
Expand DownExpand Up@@ -866,7 +928,6 @@ export function UserInput({
[isInitialView]
)


const toggleListening = useCallback(() => {
if (isListening) {
recognitionRef.current?.stop()
Expand DownExpand Up@@ -977,8 +1038,8 @@ export function UserInput({
)}
onDragEnter={files.handleDragEnter}
onDragLeave={files.handleDragLeave}
onDragOver={files.handleDragOver}
onDrop={files.handleDrop}
onDragOver={handleContainerDragOver}
onDrop={handleContainerDrop}
>
{/* Context pills row */}
{contextManagement.selectedContexts.length > 0 && (
Expand DownExpand Up@@ -1090,7 +1151,9 @@ export function UserInput({
onClose={() => {
mentionMenu.closeMentionMenu()
}}
query={mentionMenu.getActiveMentionQueryAtPosition(mentionMenu.getCaretPos())?.query ?? ''}
query={
mentionMenu.getActiveMentionQueryAtPosition(mentionMenu.getCaretPos())?.query ?? ''
}
/>,
document.body
)}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
'use client'

import { cn } from '@/lib/core/utils/cn'
import type { ChatMessageContext } from '../types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { ChatMessageContext } from '../types'

interface UserMessageContentProps {
content: string
Expand DownExpand Up@@ -48,15 +47,10 @@ function MentionHighlight({ context, token }: { context: ChatMessageContext; tok
return null
})

const bgColor = workflowColor
? `${workflowColor}40`
: 'rgba(50, 189, 126, 0.4)'
const bgColor = workflowColor ? `${workflowColor}40` : 'rgba(50, 189, 126, 0.4)'

return (
<span
className='rounded-[4px] py-[1px] px-[2px]'
style={{ backgroundColor: bgColor }}
>
<span className='rounded-[4px] px-[2px] py-[1px]' style={{ backgroundColor: bgColor }}>
{token}
</span>
)
Expand Down
14 changes: 10 additions & 4 deletions apps/sim/app/workspace/[workspaceId]/home/home.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,11 +15,17 @@ import {
} from '@/lib/core/utils/browser-storage'
import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export'
import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks'
import { MessageContent, MothershipView, TemplatePrompts, UserInput, UserMessageContent } from './components'
import type { FileAttachmentForApi } from './components/user-input/user-input'
import type { ChatContext } from '@/stores/panel'
import type { MothershipResource, MothershipResourceType } from './types'
import {
MessageContent,
MothershipView,
TemplatePrompts,
UserInput,
UserMessageContent,
} from './components'
import type { FileAttachmentForApi } from './components/user-input/user-input'
import { useAutoScroll, useChat } from './hooks'
import type { MothershipResource, MothershipResourceType } from './types'

const logger = createLogger('Home')

Expand DownExpand Up@@ -245,7 +251,7 @@ export function Home({ chatId }: HomeProps = {}) {
(context: ChatContext) => {
let resourceType: MothershipResourceType | null = null
let resourceId: string | null = null
let resourceTitle: string = context.label
const resourceTitle: string = context.label

switch (context.kind) {
case 'workflow':
Expand Down
Loading