@lobehub/lobehub 2.0.0-next.275 → 2.0.0-next.277

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/changelog/v1.json +14 -0
  3. package/locales/en-US/setting.json +11 -0
  4. package/locales/zh-CN/setting.json +11 -0
  5. package/package.json +1 -1
  6. package/packages/builtin-tool-group-agent-builder/src/client/Inspector/BatchCreateAgents/index.tsx +2 -2
  7. package/packages/builtin-tool-group-agent-builder/src/client/Inspector/UpdateGroup/index.tsx +56 -56
  8. package/packages/builtin-tool-group-agent-builder/src/client/Render/BatchCreateAgents.tsx +3 -2
  9. package/packages/builtin-tool-group-agent-builder/src/executor.ts +2 -1
  10. package/packages/types/src/agentCronJob/index.ts +19 -23
  11. package/packages/types/src/serverConfig.ts +1 -0
  12. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/Actions.tsx +31 -0
  13. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/CronTopicGroup.tsx +10 -6
  14. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/index.tsx +7 -11
  15. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/useDropdownMenu.tsx +102 -0
  16. package/src/app/[variants]/(main)/agent/cron/[cronId]/CronConfig.ts +179 -0
  17. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +111 -0
  18. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobHeader.tsx +45 -0
  19. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobSaveButton.tsx +31 -0
  20. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +213 -0
  21. package/src/app/[variants]/(main)/agent/cron/[cronId]/index.tsx +186 -344
  22. package/src/app/[variants]/(main)/agent/features/Conversation/index.tsx +8 -2
  23. package/src/app/[variants]/(main)/agent/features/Portal/_layout/Mobile.tsx +1 -0
  24. package/src/app/[variants]/(main)/agent/features/Portal/features/Portal.tsx +4 -2
  25. package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/index.tsx +42 -97
  26. package/src/app/[variants]/(main)/agent/profile/features/ProfileEditor/index.tsx +4 -20
  27. package/src/app/[variants]/(main)/community/features/UserAvatar/index.tsx +15 -5
  28. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/AgentProfilePopup.tsx +1 -6
  29. package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +1 -0
  30. package/src/app/[variants]/(main)/group/features/Portal/features/Portal.tsx +4 -2
  31. package/src/hooks/useYamlArguments.ts +11 -8
  32. package/src/server/globalConfig/index.ts +1 -0
  33. package/src/services/chatGroup/index.ts +1 -4
  34. package/src/store/serverConfig/selectors.ts +1 -0
@@ -0,0 +1,179 @@
1
+ import type { Dayjs } from 'dayjs';
2
+
3
+ export type ScheduleType = 'daily' | 'hourly' | 'weekly';
4
+
5
+ // Schedule type options
6
+ export const SCHEDULE_TYPE_OPTIONS = [
7
+ { label: 'agentCronJobs.scheduleType.daily', value: 'daily' },
8
+ { label: 'agentCronJobs.scheduleType.hourly', value: 'hourly' },
9
+ { label: 'agentCronJobs.scheduleType.weekly', value: 'weekly' },
10
+ ] as const;
11
+
12
+ // Timezone options - covering major cities worldwide
13
+ export const TIMEZONE_OPTIONS = [
14
+ { label: 'UTC', value: 'UTC' },
15
+
16
+ // Americas
17
+ { label: 'America/New_York (EST/EDT, UTC-5/-4)', value: 'America/New_York' },
18
+ { label: 'America/Chicago (CST/CDT, UTC-6/-5)', value: 'America/Chicago' },
19
+ { label: 'America/Denver (MST/MDT, UTC-7/-6)', value: 'America/Denver' },
20
+ { label: 'America/Los_Angeles (PST/PDT, UTC-8/-7)', value: 'America/Los_Angeles' },
21
+ { label: 'America/Toronto (EST/EDT, UTC-5/-4)', value: 'America/Toronto' },
22
+ { label: 'America/Vancouver (PST/PDT, UTC-8/-7)', value: 'America/Vancouver' },
23
+ { label: 'America/Mexico_City (CST, UTC-6)', value: 'America/Mexico_City' },
24
+ { label: 'America/Sao_Paulo (BRT, UTC-3)', value: 'America/Sao_Paulo' },
25
+ { label: 'America/Buenos_Aires (ART, UTC-3)', value: 'America/Buenos_Aires' },
26
+
27
+ // Europe
28
+ { label: 'Europe/London (GMT/BST, UTC+0/+1)', value: 'Europe/London' },
29
+ { label: 'Europe/Paris (CET/CEST, UTC+1/+2)', value: 'Europe/Paris' },
30
+ { label: 'Europe/Berlin (CET/CEST, UTC+1/+2)', value: 'Europe/Berlin' },
31
+ { label: 'Europe/Madrid (CET/CEST, UTC+1/+2)', value: 'Europe/Madrid' },
32
+ { label: 'Europe/Rome (CET/CEST, UTC+1/+2)', value: 'Europe/Rome' },
33
+ { label: 'Europe/Amsterdam (CET/CEST, UTC+1/+2)', value: 'Europe/Amsterdam' },
34
+ { label: 'Europe/Brussels (CET/CEST, UTC+1/+2)', value: 'Europe/Brussels' },
35
+ { label: 'Europe/Moscow (MSK, UTC+3)', value: 'Europe/Moscow' },
36
+ { label: 'Europe/Istanbul (TRT, UTC+3)', value: 'Europe/Istanbul' },
37
+
38
+ // Asia
39
+ { label: 'Asia/Dubai (GST, UTC+4)', value: 'Asia/Dubai' },
40
+ { label: 'Asia/Kolkata (IST, UTC+5:30)', value: 'Asia/Kolkata' },
41
+ { label: 'Asia/Shanghai (CST, UTC+8)', value: 'Asia/Shanghai' },
42
+ { label: 'Asia/Hong_Kong (HKT, UTC+8)', value: 'Asia/Hong_Kong' },
43
+ { label: 'Asia/Taipei (CST, UTC+8)', value: 'Asia/Taipei' },
44
+ { label: 'Asia/Singapore (SGT, UTC+8)', value: 'Asia/Singapore' },
45
+ { label: 'Asia/Tokyo (JST, UTC+9)', value: 'Asia/Tokyo' },
46
+ { label: 'Asia/Seoul (KST, UTC+9)', value: 'Asia/Seoul' },
47
+ { label: 'Asia/Bangkok (ICT, UTC+7)', value: 'Asia/Bangkok' },
48
+ { label: 'Asia/Jakarta (WIB, UTC+7)', value: 'Asia/Jakarta' },
49
+
50
+ // Oceania
51
+ { label: 'Australia/Sydney (AEDT/AEST, UTC+11/+10)', value: 'Australia/Sydney' },
52
+ { label: 'Australia/Melbourne (AEDT/AEST, UTC+11/+10)', value: 'Australia/Melbourne' },
53
+ { label: 'Australia/Brisbane (AEST, UTC+10)', value: 'Australia/Brisbane' },
54
+ { label: 'Australia/Perth (AWST, UTC+8)', value: 'Australia/Perth' },
55
+ { label: 'Pacific/Auckland (NZDT/NZST, UTC+13/+12)', value: 'Pacific/Auckland' },
56
+
57
+ // Africa & Middle East
58
+ { label: 'Africa/Cairo (EET, UTC+2)', value: 'Africa/Cairo' },
59
+ { label: 'Africa/Johannesburg (SAST, UTC+2)', value: 'Africa/Johannesburg' },
60
+ ];
61
+
62
+ // Weekday options for checkbox group
63
+ export const WEEKDAY_OPTIONS = [
64
+ { label: 'Mon', value: 1 },
65
+ { label: 'Tue', value: 2 },
66
+ { label: 'Wed', value: 3 },
67
+ { label: 'Thu', value: 4 },
68
+ { label: 'Fri', value: 5 },
69
+ { label: 'Sat', value: 6 },
70
+ { label: 'Sun', value: 0 },
71
+ ];
72
+
73
+ // Weekday labels for display
74
+ export const WEEKDAY_LABELS: Record<number, string> = {
75
+ 0: 'Sunday',
76
+ 1: 'Monday',
77
+ 2: 'Tuesday',
78
+ 3: 'Wednesday',
79
+ 4: 'Thursday',
80
+ 5: 'Friday',
81
+ 6: 'Saturday',
82
+ };
83
+
84
+ /**
85
+ * Parse cron pattern to extract schedule info
86
+ * Format: minute hour day month weekday
87
+ */
88
+ export const parseCronPattern = (
89
+ cronPattern: string,
90
+ ): {
91
+ hourlyInterval?: number;
92
+ scheduleType: ScheduleType;
93
+ triggerHour: number;
94
+ triggerMinute: number;
95
+ weekdays?: number[];
96
+ } => {
97
+ const parts = cronPattern.split(' ');
98
+ if (parts.length !== 5) {
99
+ return { scheduleType: 'daily', triggerHour: 0, triggerMinute: 0 };
100
+ }
101
+
102
+ // eslint-disable-next-line unicorn/no-unreadable-array-destructuring
103
+ const [minute, hour, , , weekday] = parts;
104
+ const rawMinute = minute === '*' ? 0 : Number.parseInt(minute);
105
+ // Normalize to nearest 30-minute interval (0 or 30)
106
+ const triggerMinute = rawMinute >= 15 && rawMinute < 45 ? 30 : 0;
107
+
108
+ // Hourly: 0 * * * * or 0 */N * * *
109
+ if (hour.startsWith('*/')) {
110
+ const interval = Number.parseInt(hour.slice(2));
111
+ return {
112
+ hourlyInterval: interval,
113
+ scheduleType: 'hourly',
114
+ triggerHour: 0,
115
+ triggerMinute,
116
+ };
117
+ }
118
+ if (hour === '*') {
119
+ return {
120
+ hourlyInterval: 1,
121
+ scheduleType: 'hourly',
122
+ triggerHour: 0,
123
+ triggerMinute,
124
+ };
125
+ }
126
+
127
+ const triggerHour = Number.parseInt(hour);
128
+
129
+ // Weekly: has specific weekday(s)
130
+ if (weekday !== '*') {
131
+ const weekdays = weekday.split(',').map((d) => Number.parseInt(d));
132
+ return {
133
+ scheduleType: 'weekly',
134
+ triggerHour,
135
+ triggerMinute,
136
+ weekdays,
137
+ };
138
+ }
139
+
140
+ // Daily: specific hour, any weekday
141
+ return {
142
+ scheduleType: 'daily',
143
+ triggerHour,
144
+ triggerMinute,
145
+ };
146
+ };
147
+
148
+ /**
149
+ * Build cron pattern from schedule info
150
+ * Format: minute hour day month weekday
151
+ */
152
+ export const buildCronPattern = (
153
+ scheduleType: ScheduleType,
154
+ triggerTime: Dayjs,
155
+ hourlyInterval?: number,
156
+ weekdays?: number[],
157
+ ): string => {
158
+ const rawMinute = triggerTime.minute();
159
+ // Normalize to 0 or 30
160
+ const minute = rawMinute >= 15 && rawMinute < 45 ? 30 : 0;
161
+ const hour = triggerTime.hour();
162
+
163
+ switch (scheduleType) {
164
+ case 'hourly': {
165
+ const interval = hourlyInterval || 1;
166
+ if (interval === 1) {
167
+ return `${minute} * * * *`;
168
+ }
169
+ return `${minute} */${interval} * * *`;
170
+ }
171
+ case 'daily': {
172
+ return `${minute} ${hour} * * *`;
173
+ }
174
+ case 'weekly': {
175
+ const days = weekdays && weekdays.length > 0 ? weekdays.sort().join(',') : '0,1,2,3,4,5,6';
176
+ return `${minute} ${hour} * * ${days}`;
177
+ }
178
+ }
179
+ };
@@ -0,0 +1,111 @@
1
+ import {
2
+ ReactCodePlugin,
3
+ ReactCodemirrorPlugin,
4
+ ReactHRPlugin,
5
+ ReactLinkPlugin,
6
+ ReactListPlugin,
7
+ ReactMathPlugin,
8
+ ReactTablePlugin,
9
+ } from '@lobehub/editor';
10
+ import { Editor, useEditor } from '@lobehub/editor/react';
11
+ import { Flexbox, Icon, Text } from '@lobehub/ui';
12
+ import { Card } from 'antd';
13
+ import { Clock } from 'lucide-react';
14
+ import { memo, useCallback, useEffect, useRef } from 'react';
15
+ import { useTranslation } from 'react-i18next';
16
+
17
+ interface CronJobContentEditorProps {
18
+ enableRichRender: boolean;
19
+ initialValue: string;
20
+ onChange: (value: string) => void;
21
+ }
22
+
23
+ const CronJobContentEditor = memo<CronJobContentEditorProps>(
24
+ ({ enableRichRender, initialValue, onChange }) => {
25
+ const { t } = useTranslation('setting');
26
+ const editor = useEditor();
27
+ const currentValueRef = useRef(initialValue);
28
+
29
+ // Update currentValueRef when initialValue changes
30
+ useEffect(() => {
31
+ currentValueRef.current = initialValue;
32
+ }, [initialValue]);
33
+
34
+ // Initialize editor content when editor is ready
35
+ useEffect(() => {
36
+ if (!editor) return;
37
+ try {
38
+ setTimeout(() => {
39
+ if (initialValue) {
40
+ editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
41
+ }
42
+ }, 100);
43
+ } catch (error) {
44
+ console.error('[CronJobContentEditor] Failed to initialize editor content:', error);
45
+ setTimeout(() => {
46
+ editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
47
+ }, 100);
48
+ }
49
+ }, [editor, enableRichRender, initialValue]);
50
+
51
+ // Handle content changes
52
+ const handleContentChange = useCallback(
53
+ (e: any) => {
54
+ const nextContent = enableRichRender
55
+ ? (e.getDocument('markdown') as unknown as string)
56
+ : (e.getDocument('text') as unknown as string);
57
+
58
+ const finalContent = nextContent || '';
59
+
60
+ // Only call onChange if content actually changed
61
+ if (finalContent !== currentValueRef.current) {
62
+ currentValueRef.current = finalContent;
63
+ onChange(finalContent);
64
+ }
65
+ },
66
+ [enableRichRender, onChange],
67
+ );
68
+
69
+ return (
70
+ <Flexbox gap={12}>
71
+ <Flexbox align="center" gap={6} horizontal>
72
+ <Icon icon={Clock} size={16} />
73
+ <Text style={{ fontWeight: 600 }}>{t('agentCronJobs.content')}</Text>
74
+ </Flexbox>
75
+ <Card
76
+ size="small"
77
+ style={{ borderRadius: 12, overflow: 'hidden' }}
78
+ styles={{ body: { padding: 0 } }}
79
+ >
80
+ <Flexbox padding={16} style={{ minHeight: 220 }}>
81
+ <Editor
82
+ content={''}
83
+ editor={editor}
84
+ lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
85
+ onTextChange={handleContentChange}
86
+ placeholder={t('agentCronJobs.form.content.placeholder')}
87
+ plugins={
88
+ enableRichRender
89
+ ? [
90
+ ReactListPlugin,
91
+ ReactCodePlugin,
92
+ ReactCodemirrorPlugin,
93
+ ReactHRPlugin,
94
+ ReactLinkPlugin,
95
+ ReactTablePlugin,
96
+ ReactMathPlugin,
97
+ ]
98
+ : undefined
99
+ }
100
+ style={{ paddingBottom: 48 }}
101
+ type={'text'}
102
+ variant={'chat'}
103
+ />
104
+ </Flexbox>
105
+ </Card>
106
+ </Flexbox>
107
+ );
108
+ },
109
+ );
110
+
111
+ export default CronJobContentEditor;
@@ -0,0 +1,45 @@
1
+ import { Flexbox, Input } from '@lobehub/ui';
2
+ import { Switch } from 'antd';
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ interface CronJobHeaderProps {
7
+ enabled?: boolean;
8
+ isNewJob?: boolean;
9
+ name: string;
10
+ onNameChange: (name: string) => void;
11
+ onToggleEnabled?: (enabled: boolean) => void;
12
+ }
13
+
14
+ const CronJobHeader = memo<CronJobHeaderProps>(
15
+ ({ enabled, isNewJob, name, onNameChange, onToggleEnabled }) => {
16
+ const { t } = useTranslation(['setting', 'common']);
17
+
18
+ return (
19
+ <Flexbox gap={16}>
20
+ {/* Title Input */}
21
+ <Input
22
+ onChange={(e) => onNameChange(e.target.value)}
23
+ placeholder={t('agentCronJobs.form.name.placeholder')}
24
+ style={{
25
+ fontSize: 28,
26
+ fontWeight: 600,
27
+ padding: 0,
28
+ }}
29
+ value={name}
30
+ variant={'borderless'}
31
+ />
32
+
33
+ {/* Controls Row */}
34
+ {!isNewJob && (
35
+ <Flexbox align="center" gap={12} horizontal>
36
+ {/* Enable/Disable Switch */}
37
+ <Switch checked={enabled ?? false} onChange={onToggleEnabled} />
38
+ </Flexbox>
39
+ )}
40
+ </Flexbox>
41
+ );
42
+ },
43
+ );
44
+
45
+ export default CronJobHeader;
@@ -0,0 +1,31 @@
1
+ import { Button, Flexbox } from '@lobehub/ui';
2
+ import { Save } from 'lucide-react';
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ interface CronJobSaveButtonProps {
7
+ disabled?: boolean;
8
+ loading?: boolean;
9
+ onSave: () => void;
10
+ }
11
+
12
+ const CronJobSaveButton = memo<CronJobSaveButtonProps>(({ disabled, loading, onSave }) => {
13
+ const { t } = useTranslation('setting');
14
+
15
+ return (
16
+ <Flexbox paddingBlock={16}>
17
+ <Button
18
+ disabled={disabled}
19
+ icon={Save}
20
+ loading={loading}
21
+ onClick={onSave}
22
+ style={{ width: 200 }}
23
+ type="primary"
24
+ >
25
+ {t('agentCronJobs.saveAsNew', { defaultValue: 'Save as New Scheduled Task' })}
26
+ </Button>
27
+ </Flexbox>
28
+ );
29
+ });
30
+
31
+ export default CronJobSaveButton;
@@ -0,0 +1,213 @@
1
+ import { Flexbox, Tag, Text } from '@lobehub/ui';
2
+ import { Card, InputNumber, Select, TimePicker } from 'antd';
3
+ import type { Dayjs } from 'dayjs';
4
+ import dayjs from 'dayjs';
5
+ import { memo, useMemo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ import {
9
+ SCHEDULE_TYPE_OPTIONS,
10
+ type ScheduleType,
11
+ TIMEZONE_OPTIONS,
12
+ WEEKDAY_LABELS,
13
+ WEEKDAY_OPTIONS,
14
+ } from '../CronConfig';
15
+
16
+ interface CronJobScheduleConfigProps {
17
+ hourlyInterval?: number;
18
+ maxExecutions?: number | null;
19
+ onScheduleChange: (updates: {
20
+ hourlyInterval?: number;
21
+ maxExecutions?: number | null;
22
+ scheduleType?: ScheduleType;
23
+ timezone?: string;
24
+ triggerTime?: Dayjs;
25
+ weekdays?: number[];
26
+ }) => void;
27
+ scheduleType: ScheduleType;
28
+ timezone: string;
29
+ triggerTime: Dayjs;
30
+ weekdays: number[];
31
+ }
32
+
33
+ const CronJobScheduleConfig = memo<CronJobScheduleConfigProps>(
34
+ ({
35
+ hourlyInterval,
36
+ maxExecutions,
37
+ onScheduleChange,
38
+ scheduleType,
39
+ timezone,
40
+ triggerTime,
41
+ weekdays,
42
+ }) => {
43
+ const { t } = useTranslation('setting');
44
+
45
+ // Compute summary tags
46
+ const summaryTags = useMemo(() => {
47
+ const result: Array<{ key: string; label: string }> = [];
48
+
49
+ // Schedule type
50
+ const scheduleTypeLabel = SCHEDULE_TYPE_OPTIONS.find(
51
+ (opt) => opt.value === scheduleType,
52
+ )?.label;
53
+ if (scheduleTypeLabel) {
54
+ result.push({
55
+ key: 'scheduleType',
56
+ label: t(scheduleTypeLabel as any),
57
+ });
58
+ }
59
+
60
+ // Trigger time
61
+ if (scheduleType === 'hourly') {
62
+ const minute = triggerTime.minute();
63
+ result.push({
64
+ key: 'interval',
65
+ label: `Every ${hourlyInterval || 1} hour(s) at :${minute.toString().padStart(2, '0')}`,
66
+ });
67
+ } else {
68
+ result.push({
69
+ key: 'triggerTime',
70
+ label: triggerTime.format('HH:mm'),
71
+ });
72
+ }
73
+
74
+ // Timezone
75
+ result.push({
76
+ key: 'timezone',
77
+ label: timezone,
78
+ });
79
+
80
+ // Weekdays for weekly schedule
81
+ if (scheduleType === 'weekly' && weekdays.length > 0) {
82
+ result.push({
83
+ key: 'weekdays',
84
+ label: weekdays.map((day) => WEEKDAY_LABELS[day]).join(', '),
85
+ });
86
+ }
87
+
88
+ return result;
89
+ }, [scheduleType, triggerTime, timezone, weekdays, hourlyInterval, t]);
90
+
91
+ return (
92
+ <Card size="small" style={{ borderRadius: 12 }} styles={{ body: { padding: 12 } }}>
93
+ <Flexbox gap={12}>
94
+ {/* Summary Tags */}
95
+ {summaryTags.length > 0 && (
96
+ <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
97
+ {summaryTags.map((tag) => (
98
+ <Tag key={tag.key} variant={'filled'}>
99
+ {tag.label}
100
+ </Tag>
101
+ ))}
102
+ </Flexbox>
103
+ )}
104
+ {/* Schedule Configuration - All in one row */}
105
+ <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
106
+ <Tag variant={'borderless'}>{t('agentCronJobs.schedule')}</Tag>
107
+ <Select
108
+ onChange={(value: ScheduleType) =>
109
+ onScheduleChange({
110
+ scheduleType: value,
111
+ weekdays: value === 'weekly' ? [0, 1, 2, 3, 4, 5, 6] : [],
112
+ })
113
+ }
114
+ options={SCHEDULE_TYPE_OPTIONS.map((opt) => ({
115
+ label: t(opt.label as any),
116
+ value: opt.value,
117
+ }))}
118
+ size="small"
119
+ style={{ minWidth: 120 }}
120
+ value={scheduleType}
121
+ />
122
+
123
+ {/* Weekdays - show only for weekly */}
124
+ {scheduleType === 'weekly' && (
125
+ <Select
126
+ maxTagCount="responsive"
127
+ mode="multiple"
128
+ onChange={(values: number[]) => onScheduleChange({ weekdays: values })}
129
+ options={WEEKDAY_OPTIONS}
130
+ placeholder="Select days"
131
+ size="small"
132
+ style={{ minWidth: 150 }}
133
+ value={weekdays}
134
+ />
135
+ )}
136
+
137
+ {/* Trigger Time - show for daily and weekly */}
138
+ {scheduleType !== 'hourly' && (
139
+ <TimePicker
140
+ format="HH:mm"
141
+ minuteStep={30}
142
+ onChange={(value) => {
143
+ if (value) onScheduleChange({ triggerTime: value });
144
+ }}
145
+ size="small"
146
+ style={{ minWidth: 120 }}
147
+ value={triggerTime ?? dayjs().hour(0).minute(0)}
148
+ />
149
+ )}
150
+
151
+ {/* Hourly Interval - show only for hourly */}
152
+ {scheduleType === 'hourly' && (
153
+ <>
154
+ <Tag variant={'borderless'}>Every</Tag>
155
+ <InputNumber
156
+ max={24}
157
+ min={1}
158
+ onChange={(value: number | null) =>
159
+ onScheduleChange({ hourlyInterval: value ?? 1 })
160
+ }
161
+ size="small"
162
+ style={{ width: 80 }}
163
+ value={hourlyInterval ?? 1}
164
+ />
165
+ <Text type="secondary">hour(s) at</Text>
166
+ <Select
167
+ onChange={(value: number) =>
168
+ onScheduleChange({ triggerTime: dayjs().hour(0).minute(value) })
169
+ }
170
+ options={[
171
+ { label: ':00', value: 0 },
172
+ { label: ':30', value: 30 },
173
+ ]}
174
+ size="small"
175
+ style={{ width: 80 }}
176
+ value={triggerTime?.minute() ?? 0}
177
+ />
178
+ </>
179
+ )}
180
+
181
+ {/* Timezone */}
182
+ <Select
183
+ onChange={(value: string) => onScheduleChange({ timezone: value })}
184
+ options={TIMEZONE_OPTIONS}
185
+ showSearch
186
+ size="small"
187
+ style={{ maxWidth: 300, minWidth: 200 }}
188
+ value={timezone}
189
+ />
190
+
191
+ </Flexbox>
192
+
193
+ {/* Max Executions */}
194
+ <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
195
+ <Tag variant={'borderless'}>{t('agentCronJobs.maxExecutions')}</Tag>
196
+ <InputNumber
197
+ min={1}
198
+ onChange={(value: number | null) =>
199
+ onScheduleChange({ maxExecutions: value ?? null })
200
+ }
201
+ placeholder={t('agentCronJobs.form.maxExecutions.placeholder')}
202
+ size="small"
203
+ style={{ width: 160 }}
204
+ value={maxExecutions ?? null}
205
+ />
206
+ </Flexbox>
207
+ </Flexbox>
208
+ </Card>
209
+ );
210
+ },
211
+ );
212
+
213
+ export default CronJobScheduleConfig;