Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cron job pause, activity and UI improvements #574

Merged
merged 4 commits into from
Dec 20, 2024
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
1 change: 1 addition & 0 deletions apps/shinkai-desktop/src/pages/edit-task.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default function EditTaskPage() {
created_at: task.created_at,
last_modified: task.last_modified,
description: task.description,
paused: task.paused,
name: task.name,
}
: undefined
Expand Down
103 changes: 56 additions & 47 deletions apps/shinkai-desktop/src/pages/task-logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
CheckCircle2,
Clock,
Edit,
RefreshCwIcon,
Sparkles,
TrashIcon,
XCircle,
Expand Down Expand Up @@ -59,7 +60,9 @@ export const TaskLogs = () => {
const {
data: logs,
isPending,
isRefetching,
isSuccess,
refetch,
} = useGetRecurringTaskLogs({
nodeAddress: auth?.node_address ?? '',
token: auth?.api_v2_key ?? '',
Expand All @@ -77,6 +80,7 @@ export const TaskLogs = () => {
<TaskCard
cronExpression={task.cron}
description={task.description}
isRunning={!task.paused}
key={task.task_id}
llmProvider={
'CreateJobWithConfigAndMessage' in task.action
Expand Down Expand Up @@ -114,19 +118,19 @@ export const TaskLogs = () => {
</div>
))}

<div className="flex items-center justify-between p-2">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Logs</h2>
{/*<Button*/}
{/* size="sm"*/}
{/* variant="outline"*/}
{/*>*/}
{/* {isRunning ? (*/}
{/* <Pause className="mr-2 h-4 w-4" />*/}
{/* ) : (*/}
{/* <Play className="mr-2 h-4 w-4" />*/}
{/* )}*/}
{/* {isRunning ? 'Pause Task' : 'Resume Task'}*/}
{/*</Button>*/}
<Button
className="h-8 w-auto gap-2 rounded-lg p-1 px-2 text-xs"
disabled={isRefetching}
isLoading={isRefetching}
onClick={() => refetch()}
size="auto"
variant="outline"
>
<RefreshCwIcon className="h-4 w-4" />
Refresh logs
</Button>
</div>

<Card className="border-0 p-0">
Expand All @@ -139,15 +143,15 @@ export const TaskLogs = () => {
</div>
)}
{isSuccess && logs.length > 0 && (
<div className="divide-y divide-gray-300 p-2">
<div className="grid grid-cols-[360px_100px_1fr] items-center gap-6 py-1.5 text-xs text-gray-50">
<div className="divide-gray-375 divide-y py-2">
<div className="grid grid-cols-[360px_100px_1fr] items-center gap-6 py-1.5 font-mono text-xs text-gray-50">
<span>Execution Time</span>
<span>Status</span>
<span>Message</span>
</div>
{logs.map((log) => (
<div
className="grid grid-cols-[360px_100px_1fr] items-center gap-6 py-1.5 text-sm"
className="grid grid-cols-[360px_100px_1fr] items-center gap-6 py-3 text-xs"
key={log.execution_time}
>
<div className="text-muted-foreground shrink-0 font-mono">
Expand All @@ -164,6 +168,9 @@ export const TaskLogs = () => {
{log.success ? 'Success' : 'Failed'}
</div>
</div>
<div className="text-gray-80 font-mono">
{log.error_message || '-'}
</div>
</div>
))}
</div>
Expand All @@ -181,15 +188,15 @@ const TaskCard = ({
cronExpression,
prompt,
llmProvider,
isRunning = true,
isRunning,
}: {
taskId: number;
name: string;
description?: string;
cronExpression: string;
prompt: string;
llmProvider: string;
isRunning?: boolean;
isRunning: boolean;
}) => {
const navigate = useNavigate();
const { t } = useTranslation();
Expand All @@ -201,17 +208,21 @@ const TaskCard = ({
});

return (
<Card className="mb-8 border">
<CardHeader>
<Card className="mb-4 border-none p-0 py-2 shadow-none">
<CardHeader className="px-0 py-0">
<div className="flex items-start justify-between">
<div className="space-y-1">
<CardTitle className="flex items-center gap-2 capitalize">
{name}
<Badge
className="rounded-md border border-gray-300"
className={cn(
'rounded-md border border-gray-300',
isRunning &&
'border-cyan-600 bg-cyan-900/20 font-normal text-cyan-400',
)}
variant={isRunning ? 'default' : 'secondary'}
>
{isRunning ? 'Active' : 'Paused'}
{isRunning ? 'Active' : 'Inactive'}
</Badge>
</CardTitle>
<CardDescription>{description}</CardDescription>
Expand Down Expand Up @@ -282,34 +293,32 @@ const TaskCard = ({
/>
</div>
</CardHeader>
<CardContent>
<div className="grid gap-6 sm:grid-cols-2">
<div className="space-y-2">
<div className="text-muted-foreground flex items-center gap-2 text-sm font-medium">
<Sparkles className="h-4 w-4" />
Prompt
</div>
<div className="rounded-md text-sm">{prompt}</div>
<CardContent className="flex flex-col gap-4 px-0 py-6">
<div className="flex items-center gap-4">
<div className="text-muted-foreground flex items-center gap-2 text-sm font-medium">
<Sparkles className="h-4 w-4" />
Prompt:
</div>
<div className="grid gap-4">
<div className="space-y-2">
<div className="text-muted-foreground flex items-center gap-2 text-sm font-medium">
<Clock className="h-4 w-4" />
Schedule
</div>
<div className="text-sm">
{readableCron}
<span className="text-gray-80 ml-2">({cronExpression})</span>
</div>
</div>
<div className="space-y-2">
<div className="text-muted-foreground flex items-center gap-2 text-sm font-medium">
<Bot className="h-4 w-4" />
Agent/AI Model
</div>
<div className="text-sm">{llmProvider}</div>
</div>
<div className="rounded-md text-sm">{prompt}</div>
</div>
<div className="flex items-center gap-4">
<div className="text-muted-foreground flex items-center gap-2 text-sm font-medium">
<Clock className="h-4 w-4" />
Schedule:
</div>
<div className="text-sm">
{readableCron}
<span className="text-gray-80 ml-2 rounded-lg bg-gray-300 px-2 py-1 font-mono">
{cronExpression}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-muted-foreground flex items-center gap-2 text-sm font-medium">
<Bot className="h-4 w-4" />
Agent/AI Model:
</div>
<div className="text-sm">{llmProvider}</div>
</div>
</CardContent>
</Card>
Expand Down
139 changes: 132 additions & 7 deletions apps/shinkai-desktop/src/pages/tasks.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { DialogClose } from '@radix-ui/react-dialog';
import { DotsVerticalIcon } from '@radix-ui/react-icons';
import { useTranslation } from '@shinkai_network/shinkai-i18n';
import { JobConfig } from '@shinkai_network/shinkai-message-ts/api/jobs/types';
import { useRemoveRecurringTask } from '@shinkai_network/shinkai-node-state/v2/mutations/removeRecurringTask/useRemoveRecurringTask';
import { useUpdateRecurringTask } from '@shinkai_network/shinkai-node-state/v2/mutations/updateRecurringTask/useUpdateRecurringTask';
import { useGetRecurringTaskNextExecutionTime } from '@shinkai_network/shinkai-node-state/v2/queries/getRecurringTaskNextExecutionTime/useGetRecurringTaskNextExecutionTime';
import { useGetRecurringTasks } from '@shinkai_network/shinkai-node-state/v2/queries/getRecurringTasks/useGetRecurringTasks';
import {
Button,
Expand All @@ -16,11 +19,19 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Popover,
PopoverContent,
PopoverTrigger,
Switch,
} from '@shinkai_network/shinkai-ui';
import {
ScheduledTasksComingSoonIcon,
ScheduledTasksIcon,
} from '@shinkai_network/shinkai-ui/assets';
import { cn } from '@shinkai_network/shinkai-ui/utils';
import cronstrue from 'cronstrue';
import { Edit, PlusIcon, TrashIcon } from 'lucide-react';
import { formatDistance } from 'date-fns';
import { Edit, PlusIcon, RefreshCwIcon, TrashIcon } from 'lucide-react';
import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
Expand All @@ -40,10 +51,95 @@ export const Tasks = () => {
token: auth?.api_v2_key ?? '',
});

const {
data: cronTasksNextExecutionTime,
isSuccess: isCronTasksNextExecutionTimeSuccess,
refetch,
isRefetching,
} = useGetRecurringTaskNextExecutionTime({
nodeAddress: auth?.node_address ?? '',
token: auth?.api_v2_key ?? '',
});

const { mutateAsync: updateRecurringTask } = useUpdateRecurringTask({
onError: (error) => {
toast.error('Failed to updated task', {
description: error.response?.data?.message ?? error.message,
});
},
});

return (
<SimpleLayout
headerRightElement={
<div className="flex items-center gap-2">
<div className="flex items-center gap-3">
{isCronTasksNextExecutionTimeSuccess &&
cronTasksNextExecutionTime.length > 0 && (
<Popover>
<PopoverTrigger asChild>
<Button
className="h-[30px] gap-2 rounded-lg px-3 text-xs"
onClick={() => refetch()}
size="auto"
type="button"
variant="outline"
>
<ScheduledTasksComingSoonIcon className="size-3.5" />
<span className="text-xs">Activity</span>
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
alignOffset={-4}
className="flex w-[400px] flex-col gap-2 bg-gray-300 px-3.5 py-4 text-xs"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<div className="flex items-center justify-between gap-2">
<h1 className="text-sm">Scheduled Cron Tasks </h1>
<Button
className="h-8 w-auto gap-2 rounded-lg p-1 px-2 text-xs"
disabled={isRefetching}
isLoading={isRefetching}
onClick={() => refetch()}
size="auto"
variant="outline"
>
{!isRefetching && (
<RefreshCwIcon className="h-3.5 w-3.5" />
)}
</Button>
</div>
{cronTasksNextExecutionTime?.map(([task, date]) => (
<div
className="flex items-start gap-2 py-1"
key={task.task_id}
>
<ScheduledTasksIcon className="text-gray-80 mt-1 size-4" />
<div className="flex flex-col gap-1 text-left">
<span className="text-sm text-gray-50">
{task.name}
<span className="text-gray-80 mx-1 rounded-lg border border-gray-200 px-1.5 py-1 text-xs">
{cronstrue.toString(task.cron, {
throwExceptionOnParseError: false,
})}
</span>
</span>
<span className="text-gray-80">
{' '}
Next execution in{' '}
<span className="text-gray-80 font-semibold">
{formatDistance(new Date(date), new Date(), {
addSuffix: true,
})}
</span>
</span>
</div>
</div>
))}
</PopoverContent>
</Popover>
)}

<Link
className={cn(
buttonVariants({
Expand Down Expand Up @@ -89,6 +185,33 @@ export const Tasks = () => {
description={task.description}
key={task.task_id}
name={task.name}
onCheckedChange={async (active) => {
if ('CreateJobWithConfigAndMessage' in task.action) {
const config: JobConfig =
task.action.CreateJobWithConfigAndMessage.config;
const message =
task.action.CreateJobWithConfigAndMessage.message.content;
const llmProvider =
task.action.CreateJobWithConfigAndMessage.llm_provider;
const jobId =
task.action.CreateJobWithConfigAndMessage.message.job_id;
await updateRecurringTask({
nodeAddress: auth?.node_address ?? '',
token: auth?.api_v2_key ?? '',
taskId: task.task_id.toString(),
active,
chatConfig: config,
cronExpression: task.cron,
description: task.description,
jobId,
llmProvider,
name: task.name,
message,
});
return;
}
}}
paused={task.paused}
prompt={
'CreateJobWithConfigAndMessage' in task.action
? task.action.CreateJobWithConfigAndMessage.message.content
Expand All @@ -113,15 +236,20 @@ const TaskCard = ({
description,
cronExpression,
prompt,
paused,
onCheckedChange,
}: {
taskId: number;
name: string;
description?: string;
cronExpression: string;
prompt: string;
paused: boolean;
onCheckedChange: (active: boolean) => void;
}) => {
const navigate = useNavigate();
const { t } = useTranslation();

const [isDeleteTaskDrawerOpen, setIsDeleteTaskDrawerOpen] =
React.useState(false);

Expand Down Expand Up @@ -154,12 +282,9 @@ const TaskCard = ({
</p>
</div>
<div className="flex items-center gap-3 pt-1">
<Switch
checked={true}
// TODO: need backend changes
/>
<Switch checked={!paused} onCheckedChange={onCheckedChange} />
<label className="text-xs text-gray-50" htmlFor="all">
Active
{paused ? 'Inactive' : 'Active'}
</label>
</div>
<Link
Expand Down
Loading