diff --git a/apps/shinkai-desktop/src/components/cron-task/component/cron-task.tsx b/apps/shinkai-desktop/src/components/cron-task/component/cron-task.tsx new file mode 100644 index 000000000..bb299e951 --- /dev/null +++ b/apps/shinkai-desktop/src/components/cron-task/component/cron-task.tsx @@ -0,0 +1,622 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useTranslation } from '@shinkai_network/shinkai-i18n'; +import { RecurringTask } from '@shinkai_network/shinkai-message-ts/api/recurring-tasks/types'; +import { DEFAULT_CHAT_CONFIG } from '@shinkai_network/shinkai-node-state/v2/constants'; +import { useCreateRecurringTask } from '@shinkai_network/shinkai-node-state/v2/mutations/createRecurringTask/useCreateRecurringTask'; +import { useUpdateRecurringTask } from '@shinkai_network/shinkai-node-state/v2/mutations/updateRecurringTask/useUpdateRecurringTask'; +import { useGetTools } from '@shinkai_network/shinkai-node-state/v2/queries/getToolsList/useGetToolsList'; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + HoverCard, + HoverCardContent, + HoverCardTrigger, + Label, + Slider, + Switch, + Textarea, + TextField, +} from '@shinkai_network/shinkai-ui'; +import { + ScheduledTasksIcon, + ToolsIcon, +} from '@shinkai_network/shinkai-ui/assets'; +import { formatText } from '@shinkai_network/shinkai-ui/helpers'; +import { cn } from '@shinkai_network/shinkai-ui/utils'; +import cronstrue from 'cronstrue'; +import { ChevronDownIcon } from 'lucide-react'; +import { useEffect, useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import { z } from 'zod'; + +import { SubpageLayout } from '../../../pages/layout/simple-layout'; +import { useAuth } from '../../../store/auth'; +import { useSettings } from '../../../store/settings'; +import { AIModelSelector } from '../../chat/chat-action-bar/ai-update-selection-action-bar'; +import { actionButtonClassnames } from '../../chat/conversation-footer'; + +const createTaskFormSchema = z.object({ + name: z.string(), + description: z.string(), + llmOrAgentId: z.string(), + cronExpression: z.string().refine( + (value) => { + try { + cronstrue.toString(value, { + throwExceptionOnParseError: true, + }); + return true; + } catch (error) { + return false; + } + }, + { + message: + 'Invalid cron expression. Please provide a valid cron expression.', + }, + ), + jobConfig: z.object({ + custom_system_prompt: z.string().optional(), + custom_prompt: z.string(), + temperature: z.number(), + max_tokens: z.number().optional(), + seed: z.number().optional(), + top_k: z.number(), + top_p: z.number(), + stream: z.boolean().optional(), + use_tools: z.boolean().optional(), + }), + jobMessage: z.object({ + content: z.string(), + toolKey: z.string().optional(), + }), +}); +type CreateTaskForm = z.infer; + +type CronTaskProps = { + mode: 'create' | 'edit'; + initialValues?: RecurringTask; +}; +function CronTask({ mode, initialValues }: CronTaskProps) { + const { t } = useTranslation(); + const defaultAgentId = useSettings((state) => state.defaultAgentId); + const navigate = useNavigate(); + const auth = useAuth((state) => state.auth); + const form = useForm({ + resolver: zodResolver(createTaskFormSchema), + defaultValues: { + name: '', + description: '', + cronExpression: '', + jobConfig: { + custom_system_prompt: '', + custom_prompt: '', + temperature: DEFAULT_CHAT_CONFIG.temperature, + top_k: DEFAULT_CHAT_CONFIG.top_k, + top_p: DEFAULT_CHAT_CONFIG.top_p, + stream: DEFAULT_CHAT_CONFIG.stream, + use_tools: DEFAULT_CHAT_CONFIG.use_tools, + }, + llmOrAgentId: defaultAgentId, + jobMessage: { + content: '', + toolKey: '', + }, + }, + }); + + useEffect(() => { + if ( + initialValues && + 'CreateJobWithConfigAndMessage' in initialValues.action + ) { + form.reset({ + cronExpression: initialValues.cron, + description: initialValues.description, + name: initialValues.name, + jobConfig: { + custom_system_prompt: + initialValues.action.CreateJobWithConfigAndMessage.config + .custom_system_prompt ?? '', + custom_prompt: + initialValues.action.CreateJobWithConfigAndMessage.config + .custom_prompt, + temperature: + initialValues.action.CreateJobWithConfigAndMessage.config + .temperature, + top_k: + initialValues.action.CreateJobWithConfigAndMessage.config.top_k, + top_p: + initialValues.action.CreateJobWithConfigAndMessage.config.top_p, + stream: + initialValues.action.CreateJobWithConfigAndMessage.config.stream, + use_tools: + initialValues.action.CreateJobWithConfigAndMessage.config.use_tools, + }, + jobMessage: { + content: + 'CreateJobWithConfigAndMessage' in initialValues.action + ? initialValues.action.CreateJobWithConfigAndMessage.message + .content + : '', + toolKey: + 'CreateJobWithConfigAndMessage' in initialValues.action + ? initialValues.action.CreateJobWithConfigAndMessage.message + .tool_router_key + : '', + }, + llmOrAgentId: defaultAgentId, // TODO: backend doesnt send this atm + }); + } + }, [form, initialValues]); + + const { data: toolsList, isSuccess: isToolListSuccess } = useGetTools({ + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + }); + + const { mutateAsync: createRecurringTask, isPending } = + useCreateRecurringTask({ + onSuccess: () => { + toast.success('Task created successfully'); + navigate('/tasks'); + }, + onError: (error) => { + toast.error('Failed to create task', { + description: error.response?.data?.message ?? error.message, + }); + }, + }); + const { + mutateAsync: updateRecurringTask, + isPending: isUpdateRecurringTaskPending, + } = useUpdateRecurringTask({ + onSuccess: () => { + toast.success('Task updated successfully'); + navigate('/tasks'); + }, + onError: (error) => { + toast.error('Failed to updated task', { + description: error.response?.data?.message ?? error.message, + }); + }, + }); + + const submit = async (values: CreateTaskForm) => { + if (mode === 'create') { + await createRecurringTask({ + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + cronExpression: values.cronExpression, + chatConfig: values.jobConfig, + message: values.jobMessage.content, + toolRouterKey: values.jobMessage.toolKey, + llmProvider: values.llmOrAgentId, + name: values.name, + description: values.description, + }); + return; + } + if (mode === 'edit' && initialValues) { + await updateRecurringTask({ + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + cronExpression: values.cronExpression, + chatConfig: values.jobConfig, + message: values.jobMessage.content, + toolRouterKey: values.jobMessage.toolKey, + llmProvider: values.llmOrAgentId, + name: values.name, + description: values.description, + taskId: initialValues.task_id.toString() ?? '', + jobId: + 'CreateJobWithConfigAndMessage' in initialValues.action + ? initialValues?.action.CreateJobWithConfigAndMessage.message.job_id + : '', + }); + } + }; + + useEffect(() => { + if (defaultAgentId) { + form.setValue('llmOrAgentId', defaultAgentId); + } + }, [defaultAgentId]); + + const currentCronExpression = form.watch('cronExpression'); + + const readableCronExpression = useMemo(() => { + const readableCron = cronstrue.toString(currentCronExpression, { + throwExceptionOnParseError: false, + }); + if (readableCron.toLowerCase().includes('error')) { + return null; + } + return readableCron; + }, [currentCronExpression, form]); + return ( + +

+ Schedule recurring tasks at a specified time +

+
+ +
+ ( + + )} + /> + + ( + + )} + /> + ( + + )} + /> + + ( + + )} + /> + {readableCronExpression && ( +
+ + + This cron will run {readableCronExpression.toLowerCase()}{' '} + + ({form.watch('cronExpression')}) + + +
+ )} +
+
+
+
+ + AI Model Configuration + + +
+
+ AI/Agent + { + form.setValue('llmOrAgentId', value); + }} + value={form.watch('llmOrAgentId')} + /> +
+
+ + Tool (optional) + + + + .icon]:rotate-180', + )} + > +
+ + + {form.watch('jobMessage.toolKey') + ? formatText( + form + .watch('jobMessage.toolKey') + ?.split(':::')?.[2] ?? '', + ) + : 'None'} + +
+ +
+ + { + form.setValue('jobMessage.toolKey', value); + }} + value={form.watch('jobMessage.toolKey')} + > + + +
+ None +
+
+ {isToolListSuccess && + toolsList.length > 0 && + toolsList?.map((tool) => ( + + +
+ + {formatText(tool.name)} + +
+
+ ))} +
+
+
+
+
+
+ ( + + System Prompt + +