Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 10 additions & 1 deletion apps/sim/app/api/copilot/chat/resources/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,19 @@ const VALID_RESOURCE_TYPES = new Set<ResourceType>([
'workflow',
'knowledgebase',
'folder',
'scheduledtask',
'log',
'integration',
])
const GENERIC_TITLES = new Set(['Table', 'File', 'Workflow', 'Knowledge Base', 'Folder', 'Log'])
const GENERIC_TITLES = new Set([
'Table',
'File',
'Workflow',
'Knowledge Base',
'Folder',
'Scheduled Task',
'Log',
])
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

export const POST = withRouteHandler(async (req: NextRequest) => {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { useMemo, useState } from 'react'
import { truncate } from '@sim/utils/string'
import {
Button,
DropdownMenu,
Expand Down Expand Up @@ -30,6 +31,7 @@ import { useFolders } from '@/hooks/queries/folders'
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
import { useLogsList } from '@/hooks/queries/logs'
import { useMothershipChats } from '@/hooks/queries/mothership-chats'
import { useWorkspaceSchedules } from '@/hooks/queries/schedules'
import { useTablesList } from '@/hooks/queries/tables'
import { useWorkflows } from '@/hooks/queries/workflows'
import { useWorkspaceFileFolders } from '@/hooks/queries/workspace-file-folders'
Expand Down Expand Up @@ -77,6 +79,7 @@ export function useAvailableResources(
const { data: folders = [] } = useFolders(workspaceId)
const { data: fileFolders = [] } = useWorkspaceFileFolders(workspaceId)
const { data: tasks = [] } = useMothershipChats(workspaceId)
const { data: schedules = [] } = useWorkspaceSchedules(workspaceId)
const { data: logsData } = useLogsList(workspaceId, LOG_DROPDOWN_FILTERS)
const logs = useMemo(() => (logsData?.pages ?? []).flatMap((page) => page.logs), [logsData])

Expand Down Expand Up @@ -155,6 +158,16 @@ export function useAvailableResources(
isOpen: existingKeys.has(`task:${t.id}`),
})),
},
{
type: 'scheduledtask' as const,
items: schedules
.filter((s) => s.sourceType === 'job')
.map((s) => ({
id: s.id,
name: s.jobTitle || truncate(s.prompt ?? '', 40) || 'Scheduled Task',
isOpen: existingKeys.has(`scheduledtask:${s.id}`),
})),
},
{
type: 'log' as const,
items: logs.map((log) => {
Expand All @@ -179,6 +192,7 @@ export function useAvailableResources(
files,
knowledgeBases,
tasks,
schedules,
logs,
existingKeys,
excludeTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import { lazy, memo, Suspense, useEffect, useMemo, useRef } from 'react'
import { createLogger } from '@sim/logger'
import { format } from 'date-fns'
import { useRouter } from 'next/navigation'
import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn'
import {
Calendar,
Download,
FileX,
Folder as FolderIcon,
Expand All @@ -24,6 +26,7 @@ import {
import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils'
import { triggerFileDownload } from '@/lib/uploads/client/download'
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
import {
FileViewer,
type PreviewMode,
Expand All @@ -50,6 +53,7 @@ import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/com
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
import { useFolders } from '@/hooks/queries/folders'
import { useLogDetail } from '@/hooks/queries/logs'
import { useWorkspaceSchedules } from '@/hooks/queries/schedules'
import { downloadTableExport } from '@/hooks/queries/tables'
import { useWorkflows } from '@/hooks/queries/workflows'
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
Expand Down Expand Up @@ -182,6 +186,15 @@ export const ResourceContent = memo(function ResourceContent({
case 'folder':
return <EmbeddedFolder key={resource.id} workspaceId={workspaceId} folderId={resource.id} />

case 'scheduledtask':
return (
<EmbeddedScheduledTask
key={resource.id}
workspaceId={workspaceId}
scheduleId={resource.id}
/>
)

case 'log':
return (
<EmbeddedLog
Expand Down Expand Up @@ -233,6 +246,8 @@ export function ResourceActions({ workspaceId, resource }: ResourceActionsProps)
)
case 'log':
return <EmbeddedLogActions workspaceId={workspaceId} logId={resource.id} />
case 'scheduledtask':
return <EmbeddedScheduledTaskActions workspaceId={workspaceId} />
case 'folder':
case 'generic':
return null
Expand Down Expand Up @@ -647,6 +662,141 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) {
)
}

const SCHEDULE_STATUS_LABEL: Record<string, string> = {
active: 'Active',
disabled: 'Paused',
completed: 'Completed',
}

function formatScheduleInstant(iso: string | null): string {
if (!iso) return '—'
const date = new Date(iso)
return Number.isNaN(date.getTime()) ? '—' : format(date, "EEE, MMM d 'at' h:mm a")
}

interface ScheduledTaskFieldProps {
title: string
value: string
}

function ScheduledTaskField({ title, value }: ScheduledTaskFieldProps) {
return (
<div className='flex flex-col gap-1'>
<span className='text-[var(--text-muted)] text-caption'>{title}</span>
<span className='text-[var(--text-body)] text-small'>{value}</span>
</div>
)
}

interface EmbeddedScheduledTaskProps {
workspaceId: string
scheduleId: string
}

function EmbeddedScheduledTask({ workspaceId, scheduleId }: EmbeddedScheduledTaskProps) {
const { data: schedules = [], isLoading } = useWorkspaceSchedules(workspaceId)
const schedule = useMemo(
() => schedules.find((s) => s.id === scheduleId),
[schedules, scheduleId]
)

if (isLoading && !schedule) return LOADING_SKELETON
Comment thread
waleedlatif1 marked this conversation as resolved.

if (!schedule) {
return (
<div className='flex h-full flex-col items-center justify-center gap-3'>
<Calendar className='size-[32px] text-[var(--text-icon)]' />
<div className='flex flex-col items-center gap-1'>
<h2 className='font-medium text-[20px] text-[var(--text-primary)]'>
Scheduled task not found
</h2>
<p className='text-[var(--text-body)] text-small'>
This scheduled task may have been deleted
</p>
</div>
</div>
)
}

const title = schedule.jobTitle || schedule.prompt || 'Scheduled task'
const timing = schedule.cronExpression
? parseCronToHumanReadable(schedule.cronExpression, schedule.timezone)
: 'Runs once'
const status = SCHEDULE_STATUS_LABEL[schedule.status] ?? schedule.status

return (
<div className='flex h-full flex-col gap-6 overflow-y-auto p-6'>
<div className='flex items-center gap-2'>
<Calendar className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
<h2 className='truncate font-medium text-[16px] text-[var(--text-primary)]'>{title}</h2>
</div>

<div className='grid grid-cols-2 gap-4'>
<ScheduledTaskField title='Status' value={status} />
<ScheduledTaskField title='Schedule' value={timing} />
<ScheduledTaskField title='Next run' value={formatScheduleInstant(schedule.nextRunAt)} />
<ScheduledTaskField title='Last run' value={formatScheduleInstant(schedule.lastRanAt)} />
</div>

<div className='flex flex-col gap-1'>
<span className='text-[var(--text-muted)] text-caption'>Prompt</span>
<p className='whitespace-pre-wrap text-[var(--text-body)] text-small'>
{schedule.prompt || '—'}
</p>
</div>

{schedule.jobHistory && schedule.jobHistory.length > 0 && (
<div className='flex flex-col gap-2'>
<span className='text-[var(--text-muted)] text-caption'>Recent runs</span>
<div className='flex flex-col gap-2'>
{schedule.jobHistory.slice(0, 5).map((run) => (
<div
key={run.timestamp}
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
className='flex flex-col gap-1 rounded-[6px] bg-[var(--surface-4)] px-3 py-2'
>
<span className='text-[var(--text-tertiary)] text-caption'>
{formatScheduleInstant(run.timestamp)}
</span>
<span className='text-[var(--text-body)] text-small'>{run.summary}</span>
</div>
))}
</div>
</div>
)}
</div>
)
}

interface EmbeddedScheduledTaskActionsProps {
workspaceId: string
}

function EmbeddedScheduledTaskActions({ workspaceId }: EmbeddedScheduledTaskActionsProps) {
const router = useRouter()

const handleOpenScheduledTasks = () => {
router.push(`/workspace/${workspaceId}/scheduled-tasks`)
}

return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='subtle'
onClick={handleOpenScheduledTasks}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Open in scheduled tasks'
>
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Open in scheduled tasks</p>
</Tooltip.Content>
</Tooltip.Root>
)
}

interface EmbeddedLogProps {
workspaceId: string
logId: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { ElementType, ReactNode } from 'react'
import type { QueryClient } from '@tanstack/react-query'
import {
Calendar,
Connections,
Database,
File as FileIcon,
Expand All @@ -23,6 +24,7 @@ import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color'
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
import { logKeys } from '@/hooks/queries/logs'
import { mothershipChatKeys } from '@/hooks/queries/mothership-chats'
import { scheduleKeys } from '@/hooks/queries/schedules'
import { tableKeys } from '@/hooks/queries/tables'
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
Expand Down Expand Up @@ -183,6 +185,15 @@ export const RESOURCE_REGISTRY: Record<MothershipResourceType, ResourceTypeConfi
),
renderDropdownItem: (props) => <DefaultDropdownItem {...props} />,
},
scheduledtask: {
type: 'scheduledtask',
label: 'Scheduled Tasks',
icon: Calendar,
renderTabIcon: (_resource, className) => (
<Calendar className={cn(className, 'text-[var(--text-icon)]')} />
),
renderDropdownItem: (props) => <IconDropdownItem {...props} icon={Calendar} />,
},
log: {
type: 'log',
label: 'Logs',
Expand Down Expand Up @@ -241,6 +252,9 @@ const RESOURCE_INVALIDATORS: Record<
task: (qc, wId) => {
qc.invalidateQueries({ queryKey: mothershipChatKeys.list(wId) })
},
scheduledtask: (qc, wId) => {
qc.invalidateQueries({ queryKey: scheduleKeys.list(wId) })
},
log: (qc, _wId, id) => {
qc.invalidateQueries({ queryKey: logKeys.details() })
qc.invalidateQueries({ queryKey: logKeys.detail(id) })
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/api/contracts/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const copilotResourceTypeSchema = z.enum([
'workflow',
'knowledgebase',
'folder',
'scheduledtask',
'log',
])

Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/copilot/resources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const MothershipResourceType = {
folder: 'folder',
filefolder: 'filefolder',
task: 'task',
scheduledtask: 'scheduledtask',
log: 'log',
integration: 'integration',
generic: 'generic',
Expand Down
Loading