mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat: refactor cron job UI and use runtime enableBusinessFeatures flag (#11975)
- Replace compile-time ENABLE_BUSINESS_FEATURES constant with runtime serverConfigSelectors.enableBusinessFeatures for cron module - Redesign CronJobScheduleConfig with FormGroup pattern matching Settings UI - Update CronJobHeader with simplified layout (28px title, Switch only) - Convert all cron feature components to use createStaticStyles with cssVar - Add i18n keys for cron job form labels LOBE-4540
This commit is contained in:
@@ -34,11 +34,20 @@
|
||||
"agentCronJobs.empty.description": "Create your first scheduled task to automate your agent",
|
||||
"agentCronJobs.empty.title": "No scheduled tasks yet",
|
||||
"agentCronJobs.enable": "Enable",
|
||||
"agentCronJobs.form.at": "at",
|
||||
"agentCronJobs.form.content.placeholder": "Enter the prompt or instruction for the agent",
|
||||
"agentCronJobs.form.every": "Every",
|
||||
"agentCronJobs.form.frequency": "Frequency",
|
||||
"agentCronJobs.form.hours": "hour(s)",
|
||||
"agentCronJobs.form.maxExecutions": "Stop after",
|
||||
"agentCronJobs.form.maxExecutions.placeholder": "Leave empty for unlimited",
|
||||
"agentCronJobs.form.name.placeholder": "Enter task name",
|
||||
"agentCronJobs.form.time": "Time",
|
||||
"agentCronJobs.form.timeRange.end": "End Time",
|
||||
"agentCronJobs.form.timeRange.start": "Start Time",
|
||||
"agentCronJobs.form.times": "times",
|
||||
"agentCronJobs.form.timezone": "Timezone",
|
||||
"agentCronJobs.form.unlimited": "Run continuously",
|
||||
"agentCronJobs.form.validation.contentRequired": "Task content is required",
|
||||
"agentCronJobs.form.validation.invalidTimeRange": "Start time must be before end time",
|
||||
"agentCronJobs.form.validation.nameRequired": "Task name is required",
|
||||
@@ -83,6 +92,13 @@
|
||||
"agentCronJobs.weekday.tuesday": "Tuesday",
|
||||
"agentCronJobs.weekday.wednesday": "Wednesday",
|
||||
"agentCronJobs.weekdays": "Weekdays",
|
||||
"agentCronJobs.weekdays.fri": "Fri",
|
||||
"agentCronJobs.weekdays.mon": "Mon",
|
||||
"agentCronJobs.weekdays.sat": "Sat",
|
||||
"agentCronJobs.weekdays.sun": "Sun",
|
||||
"agentCronJobs.weekdays.thu": "Thu",
|
||||
"agentCronJobs.weekdays.tue": "Tue",
|
||||
"agentCronJobs.weekdays.wed": "Wed",
|
||||
"agentInfoDescription.basic.avatar": "Avatar",
|
||||
"agentInfoDescription.basic.description": "Description",
|
||||
"agentInfoDescription.basic.name": "Name",
|
||||
|
||||
@@ -34,11 +34,20 @@
|
||||
"agentCronJobs.empty.description": "创建您的第一个定时任务以实现智能体自动化",
|
||||
"agentCronJobs.empty.title": "暂无定时任务",
|
||||
"agentCronJobs.enable": "启用",
|
||||
"agentCronJobs.form.at": "于",
|
||||
"agentCronJobs.form.content.placeholder": "输入智能体的提示词或指令",
|
||||
"agentCronJobs.form.every": "每",
|
||||
"agentCronJobs.form.frequency": "执行频率",
|
||||
"agentCronJobs.form.hours": "小时",
|
||||
"agentCronJobs.form.maxExecutions": "执行",
|
||||
"agentCronJobs.form.maxExecutions.placeholder": "留空表示无限次",
|
||||
"agentCronJobs.form.name.placeholder": "输入任务名称",
|
||||
"agentCronJobs.form.time": "时间",
|
||||
"agentCronJobs.form.timeRange.end": "结束时间",
|
||||
"agentCronJobs.form.timeRange.start": "开始时间",
|
||||
"agentCronJobs.form.times": "次后停止",
|
||||
"agentCronJobs.form.timezone": "时区",
|
||||
"agentCronJobs.form.unlimited": "持续执行",
|
||||
"agentCronJobs.form.validation.contentRequired": "任务内容不能为空",
|
||||
"agentCronJobs.form.validation.invalidTimeRange": "开始时间必须早于结束时间",
|
||||
"agentCronJobs.form.validation.nameRequired": "任务名称不能为空",
|
||||
@@ -83,6 +92,13 @@
|
||||
"agentCronJobs.weekday.tuesday": "星期二",
|
||||
"agentCronJobs.weekday.wednesday": "星期三",
|
||||
"agentCronJobs.weekdays": "工作日",
|
||||
"agentCronJobs.weekdays.fri": "周五",
|
||||
"agentCronJobs.weekdays.mon": "周一",
|
||||
"agentCronJobs.weekdays.sat": "周六",
|
||||
"agentCronJobs.weekdays.sun": "周日",
|
||||
"agentCronJobs.weekdays.thu": "周四",
|
||||
"agentCronJobs.weekdays.tue": "周二",
|
||||
"agentCronJobs.weekdays.wed": "周三",
|
||||
"agentInfoDescription.basic.avatar": "头像",
|
||||
"agentInfoDescription.basic.description": "描述",
|
||||
"agentInfoDescription.basic.name": "名称",
|
||||
|
||||
@@ -26,9 +26,11 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
|
||||
s.activeAgentId,
|
||||
s.useFetchCronTopicsWithJobInfo,
|
||||
]);
|
||||
const { data: cronTopicsGroupsWithJobInfo = [], isLoading } =
|
||||
useFetchCronTopicsWithJobInfo(agentId);
|
||||
const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
|
||||
const { data: cronTopicsGroupsWithJobInfo = [], isLoading } = useFetchCronTopicsWithJobInfo(
|
||||
agentId,
|
||||
enableBusinessFeatures,
|
||||
);
|
||||
|
||||
const handleCreateCronJob = useCallback(() => {
|
||||
if (!agentId) return;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ReactCodePlugin,
|
||||
ReactCodemirrorPlugin,
|
||||
@@ -8,12 +10,19 @@ import {
|
||||
ReactTablePlugin,
|
||||
} from '@lobehub/editor';
|
||||
import { Editor, useEditor } from '@lobehub/editor/react';
|
||||
import { Flexbox, Icon, Text } from '@lobehub/ui';
|
||||
import { Card } from 'antd';
|
||||
import { Clock } from 'lucide-react';
|
||||
import { FormGroup } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
editorWrapper: css`
|
||||
min-height: 200px;
|
||||
padding-block: 8px;
|
||||
padding-inline: 0;
|
||||
`,
|
||||
}));
|
||||
|
||||
interface CronJobContentEditorProps {
|
||||
enableRichRender: boolean;
|
||||
initialValue: string;
|
||||
@@ -26,12 +35,10 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
|
||||
const editor = useEditor();
|
||||
const currentValueRef = useRef(initialValue);
|
||||
|
||||
// Update currentValueRef when initialValue changes
|
||||
useEffect(() => {
|
||||
currentValueRef.current = initialValue;
|
||||
}, [initialValue]);
|
||||
|
||||
// Initialize editor content when editor is ready
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
try {
|
||||
@@ -48,7 +55,6 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
|
||||
}
|
||||
}, [editor, enableRichRender, initialValue]);
|
||||
|
||||
// Handle content changes
|
||||
const handleContentChange = useCallback(
|
||||
(e: any) => {
|
||||
const nextContent = enableRichRender
|
||||
@@ -57,7 +63,6 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
|
||||
|
||||
const finalContent = nextContent || '';
|
||||
|
||||
// Only call onChange if content actually changed
|
||||
if (finalContent !== currentValueRef.current) {
|
||||
currentValueRef.current = finalContent;
|
||||
onChange(finalContent);
|
||||
@@ -67,43 +72,33 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
|
||||
);
|
||||
|
||||
return (
|
||||
<Flexbox gap={12}>
|
||||
<Flexbox align="center" gap={6} horizontal>
|
||||
<Icon icon={Clock} size={16} />
|
||||
<Text style={{ fontWeight: 600 }}>{t('agentCronJobs.content')}</Text>
|
||||
</Flexbox>
|
||||
<Card
|
||||
size="small"
|
||||
style={{ borderRadius: 12, overflow: 'hidden' }}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<Flexbox padding={16} style={{ minHeight: 220 }}>
|
||||
<Editor
|
||||
content={''}
|
||||
editor={editor}
|
||||
lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
|
||||
onTextChange={handleContentChange}
|
||||
placeholder={t('agentCronJobs.form.content.placeholder')}
|
||||
plugins={
|
||||
enableRichRender
|
||||
? [
|
||||
ReactListPlugin,
|
||||
ReactCodePlugin,
|
||||
ReactCodemirrorPlugin,
|
||||
ReactHRPlugin,
|
||||
ReactLinkPlugin,
|
||||
ReactTablePlugin,
|
||||
ReactMathPlugin,
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
style={{ paddingBottom: 48 }}
|
||||
type={'text'}
|
||||
variant={'chat'}
|
||||
/>
|
||||
</Flexbox>
|
||||
</Card>
|
||||
</Flexbox>
|
||||
<FormGroup title={t('agentCronJobs.content')} variant="filled">
|
||||
<div className={styles.editorWrapper}>
|
||||
<Editor
|
||||
content={''}
|
||||
editor={editor}
|
||||
lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
|
||||
onTextChange={handleContentChange}
|
||||
placeholder={t('agentCronJobs.form.content.placeholder')}
|
||||
plugins={
|
||||
enableRichRender
|
||||
? [
|
||||
ReactListPlugin,
|
||||
ReactCodePlugin,
|
||||
ReactCodemirrorPlugin,
|
||||
ReactHRPlugin,
|
||||
ReactLinkPlugin,
|
||||
ReactTablePlugin,
|
||||
ReactMathPlugin,
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
style={{ paddingBottom: 48 }}
|
||||
type={'text'}
|
||||
variant={'chat'}
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { Flexbox, Input } from '@lobehub/ui';
|
||||
import { Switch } from 'antd';
|
||||
'use client';
|
||||
|
||||
import { Flexbox, Input, LobeSwitch as Switch } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
titleInput: css`
|
||||
flex: 1;
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
`,
|
||||
}));
|
||||
|
||||
interface CronJobHeaderProps {
|
||||
enabled?: boolean;
|
||||
isNewJob?: boolean;
|
||||
@@ -17,30 +28,26 @@ const CronJobHeader = memo<CronJobHeaderProps>(
|
||||
const { t } = useTranslation(['setting', 'common']);
|
||||
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
{/* Title Input */}
|
||||
<Flexbox
|
||||
align="center"
|
||||
gap={16}
|
||||
horizontal
|
||||
justify="space-between"
|
||||
style={{ marginBottom: 8 }}
|
||||
>
|
||||
<Input
|
||||
className={styles.titleInput}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
placeholder={t('agentCronJobs.form.name.placeholder')}
|
||||
style={{
|
||||
fontSize: 28,
|
||||
fontWeight: 600,
|
||||
padding: 0,
|
||||
}}
|
||||
value={name}
|
||||
variant={'borderless'}
|
||||
variant="borderless"
|
||||
/>
|
||||
|
||||
{/* Controls Row */}
|
||||
{!isNewJob && (
|
||||
<Flexbox align="center" gap={12} horizontal>
|
||||
{/* Enable/Disable Switch */}
|
||||
<Switch
|
||||
checked={enabled ?? false}
|
||||
loading={isTogglingEnabled}
|
||||
onChange={onToggleEnabled}
|
||||
/>
|
||||
</Flexbox>
|
||||
<Switch
|
||||
checked={enabled ?? false}
|
||||
loading={isTogglingEnabled}
|
||||
onChange={onToggleEnabled}
|
||||
/>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { Button, Flexbox } from '@lobehub/ui';
|
||||
import { Save } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
@@ -13,13 +15,13 @@ const CronJobSaveButton = memo<CronJobSaveButtonProps>(({ disabled, loading, onS
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
return (
|
||||
<Flexbox paddingBlock={16}>
|
||||
<Flexbox paddingBlock={8}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
icon={Save}
|
||||
loading={loading}
|
||||
onClick={onSave}
|
||||
style={{ width: 200 }}
|
||||
style={{ maxWidth: 200, width: '100%' }}
|
||||
type="primary"
|
||||
>
|
||||
{t('agentCronJobs.saveAsNew')}
|
||||
|
||||
@@ -1,17 +1,62 @@
|
||||
import { Flexbox, Tag, Text } from '@lobehub/ui';
|
||||
import { Card, InputNumber, Select, TimePicker } from 'antd';
|
||||
'use client';
|
||||
|
||||
import { Checkbox, Flexbox, FormGroup, LobeSelect as Select, Text } from '@lobehub/ui';
|
||||
import { Divider, InputNumber, TimePicker } from 'antd';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
SCHEDULE_TYPE_OPTIONS,
|
||||
type ScheduleType,
|
||||
TIMEZONE_OPTIONS,
|
||||
WEEKDAY_LABELS,
|
||||
WEEKDAY_OPTIONS,
|
||||
} from '../CronConfig';
|
||||
import { SCHEDULE_TYPE_OPTIONS, type ScheduleType, TIMEZONE_OPTIONS } from '../CronConfig';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
label: css`
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
`,
|
||||
row: css`
|
||||
min-height: 48px;
|
||||
padding-block: 12px;
|
||||
padding-inline: 0;
|
||||
`,
|
||||
weekdayButton: css`
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
border: 1px solid ${cssVar.colorBorder};
|
||||
border-radius: 6px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
|
||||
background: transparent;
|
||||
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: ${cssVar.colorPrimary};
|
||||
color: ${cssVar.colorPrimary};
|
||||
}
|
||||
`,
|
||||
weekdayButtonActive: css`
|
||||
border-color: ${cssVar.colorPrimary};
|
||||
color: ${cssVar.colorTextLightSolid};
|
||||
background: ${cssVar.colorPrimary};
|
||||
|
||||
&:hover {
|
||||
border-color: ${cssVar.colorPrimaryHover};
|
||||
color: ${cssVar.colorTextLightSolid};
|
||||
background: ${cssVar.colorPrimaryHover};
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
interface CronJobScheduleConfigProps {
|
||||
hourlyInterval?: number;
|
||||
@@ -30,6 +75,16 @@ interface CronJobScheduleConfigProps {
|
||||
weekdays: number[];
|
||||
}
|
||||
|
||||
const WEEKDAYS = [
|
||||
{ key: 1, label: 'agentCronJobs.weekdays.mon' },
|
||||
{ key: 2, label: 'agentCronJobs.weekdays.tue' },
|
||||
{ key: 3, label: 'agentCronJobs.weekdays.wed' },
|
||||
{ key: 4, label: 'agentCronJobs.weekdays.thu' },
|
||||
{ key: 5, label: 'agentCronJobs.weekdays.fri' },
|
||||
{ key: 6, label: 'agentCronJobs.weekdays.sat' },
|
||||
{ key: 0, label: 'agentCronJobs.weekdays.sun' },
|
||||
];
|
||||
|
||||
const CronJobScheduleConfig = memo<CronJobScheduleConfigProps>(
|
||||
({
|
||||
hourlyInterval,
|
||||
@@ -42,172 +97,155 @@ const CronJobScheduleConfig = memo<CronJobScheduleConfigProps>(
|
||||
}) => {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
// Compute summary tags
|
||||
const summaryTags = useMemo(() => {
|
||||
const result: Array<{ key: string; label: string }> = [];
|
||||
const toggleWeekday = (day: number) => {
|
||||
const newWeekdays = weekdays.includes(day)
|
||||
? weekdays.filter((d) => d !== day)
|
||||
: [...weekdays, day];
|
||||
onScheduleChange({ weekdays: newWeekdays });
|
||||
};
|
||||
|
||||
// Schedule type
|
||||
const scheduleTypeLabel = SCHEDULE_TYPE_OPTIONS.find(
|
||||
(opt) => opt.value === scheduleType,
|
||||
)?.label;
|
||||
if (scheduleTypeLabel) {
|
||||
result.push({
|
||||
key: 'scheduleType',
|
||||
label: t(scheduleTypeLabel as any),
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger time
|
||||
if (scheduleType === 'hourly') {
|
||||
const minute = triggerTime.minute();
|
||||
result.push({
|
||||
key: 'interval',
|
||||
label: `Every ${hourlyInterval || 1} hour(s) at :${minute.toString().padStart(2, '0')}`,
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
key: 'triggerTime',
|
||||
label: triggerTime.format('HH:mm'),
|
||||
});
|
||||
}
|
||||
|
||||
// Timezone
|
||||
result.push({
|
||||
key: 'timezone',
|
||||
label: timezone,
|
||||
});
|
||||
|
||||
// Weekdays for weekly schedule
|
||||
if (scheduleType === 'weekly' && weekdays.length > 0) {
|
||||
result.push({
|
||||
key: 'weekdays',
|
||||
label: weekdays.map((day) => t(WEEKDAY_LABELS[day] as any)).join(', '),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [scheduleType, triggerTime, timezone, weekdays, hourlyInterval, t]);
|
||||
const isUnlimited = maxExecutions === null || maxExecutions === undefined;
|
||||
|
||||
return (
|
||||
<Card size="small" style={{ borderRadius: 12 }} styles={{ body: { padding: 12 } }}>
|
||||
<Flexbox gap={12}>
|
||||
{/* Summary Tags */}
|
||||
{summaryTags.length > 0 && (
|
||||
<Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
|
||||
{summaryTags.map((tag) => (
|
||||
<Tag key={tag.key} variant={'filled'}>
|
||||
{tag.label}
|
||||
</Tag>
|
||||
))}
|
||||
</Flexbox>
|
||||
)}
|
||||
{/* Schedule Configuration - All in one row */}
|
||||
<Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
|
||||
<Tag variant={'borderless'}>{t('agentCronJobs.schedule')}</Tag>
|
||||
<Select
|
||||
onChange={(value: ScheduleType) =>
|
||||
onScheduleChange({
|
||||
scheduleType: value,
|
||||
weekdays: value === 'weekly' ? [0, 1, 2, 3, 4, 5, 6] : [],
|
||||
})
|
||||
}
|
||||
options={SCHEDULE_TYPE_OPTIONS.map((opt) => ({
|
||||
label: t(opt.label as any),
|
||||
value: opt.value,
|
||||
}))}
|
||||
size="small"
|
||||
style={{ minWidth: 120 }}
|
||||
value={scheduleType}
|
||||
/>
|
||||
<FormGroup title={t('agentCronJobs.schedule')} variant="filled">
|
||||
{/* Frequency Row */}
|
||||
<Flexbox align="center" className={styles.row} gap={24} horizontal>
|
||||
<Text className={styles.label}>{t('agentCronJobs.form.frequency')}</Text>
|
||||
<Select
|
||||
onChange={(value: ScheduleType) =>
|
||||
onScheduleChange({
|
||||
scheduleType: value,
|
||||
weekdays: value === 'weekly' ? [1, 2, 3, 4, 5] : [],
|
||||
})
|
||||
}
|
||||
options={SCHEDULE_TYPE_OPTIONS.map((opt) => ({
|
||||
label: t(opt.label as any),
|
||||
value: opt.value,
|
||||
}))}
|
||||
style={{ width: 140 }}
|
||||
value={scheduleType}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Flexbox>
|
||||
|
||||
{/* Weekdays - show only for weekly */}
|
||||
{scheduleType === 'weekly' && (
|
||||
<Select
|
||||
maxTagCount="responsive"
|
||||
mode="multiple"
|
||||
onChange={(values: number[]) => onScheduleChange({ weekdays: values })}
|
||||
options={WEEKDAY_OPTIONS.map((opt) => ({
|
||||
label: t(opt.label as any),
|
||||
value: opt.value,
|
||||
}))}
|
||||
placeholder="Select days"
|
||||
size="small"
|
||||
style={{ minWidth: 150 }}
|
||||
value={weekdays}
|
||||
/>
|
||||
)}
|
||||
<Divider style={{ margin: 0 }} />
|
||||
|
||||
{/* Trigger Time - show for daily and weekly */}
|
||||
{scheduleType !== 'hourly' && (
|
||||
{/* Time Row (for daily/weekly) */}
|
||||
{scheduleType !== 'hourly' && (
|
||||
<>
|
||||
<Flexbox align="center" className={styles.row} gap={24} horizontal>
|
||||
<Text className={styles.label}>{t('agentCronJobs.form.time')}</Text>
|
||||
<TimePicker
|
||||
format="HH:mm"
|
||||
minuteStep={30}
|
||||
minuteStep={15}
|
||||
onChange={(value) => {
|
||||
if (value) onScheduleChange({ triggerTime: value });
|
||||
}}
|
||||
size="small"
|
||||
style={{ minWidth: 120 }}
|
||||
value={triggerTime ?? dayjs().hour(0).minute(0)}
|
||||
style={{ width: 120 }}
|
||||
value={triggerTime ?? dayjs().hour(9).minute(0)}
|
||||
/>
|
||||
)}
|
||||
</Flexbox>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Hourly Interval - show only for hourly */}
|
||||
{scheduleType === 'hourly' && (
|
||||
<>
|
||||
<Tag variant={'borderless'}>Every</Tag>
|
||||
{/* Hourly Interval Row */}
|
||||
{scheduleType === 'hourly' && (
|
||||
<>
|
||||
<Flexbox align="center" className={styles.row} gap={24} horizontal>
|
||||
<Text className={styles.label}>{t('agentCronJobs.form.every')}</Text>
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
<InputNumber
|
||||
max={24}
|
||||
min={1}
|
||||
onChange={(value: number | null) =>
|
||||
onScheduleChange({ hourlyInterval: value ?? 1 })
|
||||
}
|
||||
size="small"
|
||||
style={{ width: 80 }}
|
||||
onChange={(value) => onScheduleChange({ hourlyInterval: value ?? 1 })}
|
||||
style={{ width: 70 }}
|
||||
value={hourlyInterval ?? 1}
|
||||
/>
|
||||
<Text type="secondary">hour(s) at</Text>
|
||||
<Text type="secondary">{t('agentCronJobs.form.hours')}</Text>
|
||||
<Text type="secondary">{t('agentCronJobs.form.at')}</Text>
|
||||
<Select
|
||||
onChange={(value: number) =>
|
||||
onScheduleChange({ triggerTime: dayjs().hour(0).minute(value) })
|
||||
}
|
||||
options={[
|
||||
{ label: ':00', value: 0 },
|
||||
{ label: ':15', value: 15 },
|
||||
{ label: ':30', value: 30 },
|
||||
{ label: ':45', value: 45 },
|
||||
]}
|
||||
size="small"
|
||||
style={{ width: 80 }}
|
||||
style={{ width: '80px' }}
|
||||
value={triggerTime?.minute() ?? 0}
|
||||
variant="outlined"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Timezone */}
|
||||
<Select
|
||||
onChange={(value: string) => onScheduleChange({ timezone: value })}
|
||||
options={TIMEZONE_OPTIONS}
|
||||
showSearch
|
||||
size="small"
|
||||
style={{ maxWidth: 300, minWidth: 200 }}
|
||||
value={timezone}
|
||||
/>
|
||||
</Flexbox>
|
||||
{/* Weekday Selector (only for weekly) */}
|
||||
{scheduleType === 'weekly' && (
|
||||
<>
|
||||
<Flexbox align="center" className={styles.row} gap={24} horizontal>
|
||||
<Text className={styles.label}>{t('agentCronJobs.weekdays')}</Text>
|
||||
<Flexbox gap={6} horizontal>
|
||||
{WEEKDAYS.map(({ key, label }) => (
|
||||
<div
|
||||
className={cx(
|
||||
styles.weekdayButton,
|
||||
weekdays.includes(key) && styles.weekdayButtonActive,
|
||||
)}
|
||||
key={key}
|
||||
onClick={() => toggleWeekday(key)}
|
||||
>
|
||||
{t(label as any)}
|
||||
</div>
|
||||
))}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Max Executions */}
|
||||
<Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
|
||||
<Tag variant={'borderless'}>{t('agentCronJobs.maxExecutions')}</Tag>
|
||||
{/* Timezone Row */}
|
||||
<Flexbox align="center" className={styles.row} gap={24} horizontal>
|
||||
<Text className={styles.label}>{t('agentCronJobs.form.timezone')}</Text>
|
||||
<Select
|
||||
onChange={(value: string) => onScheduleChange({ timezone: value })}
|
||||
options={TIMEZONE_OPTIONS}
|
||||
popupMatchSelectWidth={false}
|
||||
showSearch
|
||||
style={{ minWidth: '200px', width: 'fit-content' }}
|
||||
value={timezone}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Flexbox>
|
||||
|
||||
<Divider style={{ margin: 0 }} />
|
||||
|
||||
{/* Execution Limit Row */}
|
||||
<Flexbox align="center" className={styles.row} gap={24} horizontal>
|
||||
<Text className={styles.label}>{t('agentCronJobs.maxExecutions')}</Text>
|
||||
<Flexbox align="center" gap={12} horizontal>
|
||||
<InputNumber
|
||||
disabled={isUnlimited}
|
||||
min={1}
|
||||
onChange={(value: number | null) =>
|
||||
onScheduleChange({ maxExecutions: value ?? null })
|
||||
}
|
||||
placeholder={t('agentCronJobs.form.maxExecutions.placeholder')}
|
||||
size="small"
|
||||
style={{ width: 160 }}
|
||||
value={maxExecutions ?? null}
|
||||
onChange={(value) => onScheduleChange({ maxExecutions: value })}
|
||||
placeholder="100"
|
||||
style={{ width: 100 }}
|
||||
value={maxExecutions ?? undefined}
|
||||
/>
|
||||
<Text type="secondary">{t('agentCronJobs.form.times')}</Text>
|
||||
<Checkbox
|
||||
checked={isUnlimited}
|
||||
onChange={(checked) => onScheduleChange({ maxExecutions: checked ? null : 100 })}
|
||||
>
|
||||
{t('agentCronJobs.form.unlimited')}
|
||||
</Checkbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Card>
|
||||
</FormGroup>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
||||
import { message } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -10,7 +9,7 @@ import type {
|
||||
} from '@/database/schemas/agentCronJob';
|
||||
import { agentCronJobService } from '@/services/agentCronJob';
|
||||
|
||||
export const useAgentCronJobs = (agentId?: string) => {
|
||||
export const useAgentCronJobs = (agentId?: string, enabled: boolean = true) => {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
// Fetch cron jobs for the agent
|
||||
@@ -20,8 +19,8 @@ export const useAgentCronJobs = (agentId?: string) => {
|
||||
isLoading: loading,
|
||||
mutate,
|
||||
} = useSWR(
|
||||
ENABLE_BUSINESS_FEATURES && agentId ? `/api/agent-cron-jobs/${agentId}` : null,
|
||||
ENABLE_BUSINESS_FEATURES && agentId ? () => agentCronJobService.getByAgentId(agentId) : null,
|
||||
enabled && agentId ? `/api/agent-cron-jobs/${agentId}` : null,
|
||||
enabled && agentId ? () => agentCronJobService.getByAgentId(agentId) : null,
|
||||
{
|
||||
onError: (error) => {
|
||||
console.error('Failed to fetch cron jobs:', error);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { Typography } from 'antd';
|
||||
import { Clock } from 'lucide-react';
|
||||
@@ -10,6 +9,7 @@ import urlJoin from 'url-join';
|
||||
|
||||
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
|
||||
import CronJobCards from './CronJobCards';
|
||||
import { useAgentCronJobs } from './hooks/useAgentCronJobs';
|
||||
@@ -20,8 +20,9 @@ const AgentCronJobs = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
const agentId = useAgentStore((s) => s.activeAgentId);
|
||||
const router = useQueryRoute();
|
||||
const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
|
||||
|
||||
const { cronJobs, loading, deleteCronJob } = useAgentCronJobs(agentId);
|
||||
const { cronJobs, loading, deleteCronJob } = useAgentCronJobs(agentId, enableBusinessFeatures);
|
||||
|
||||
// Edit: Navigate to cron job detail page
|
||||
const handleEdit = useCallback(
|
||||
@@ -40,7 +41,7 @@ const AgentCronJobs = memo(() => {
|
||||
[deleteCronJob],
|
||||
);
|
||||
|
||||
if (!ENABLE_BUSINESS_FEATURES) return null;
|
||||
if (!enableBusinessFeatures) return null;
|
||||
|
||||
if (!agentId) {
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
||||
import { Button, Flexbox } from '@lobehub/ui';
|
||||
import { Divider } from 'antd';
|
||||
import { useTheme } from 'antd-style';
|
||||
@@ -15,6 +14,7 @@ import { useQueryRoute } from '@/hooks/useQueryRoute';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
|
||||
import AgentCronJobs from '../AgentCronJobs';
|
||||
import AgentSettings from '../AgentSettings';
|
||||
@@ -31,6 +31,7 @@ const ProfileEditor = memo(() => {
|
||||
const agentId = useAgentStore((s) => s.activeAgentId);
|
||||
const switchTopic = useChatStore((s) => s.switchTopic);
|
||||
const router = useQueryRoute();
|
||||
const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
|
||||
|
||||
const handleCreateCronJob = useCallback(() => {
|
||||
if (!agentId) return;
|
||||
@@ -95,7 +96,7 @@ const ProfileEditor = memo(() => {
|
||||
{t('startConversation')}
|
||||
</Button>
|
||||
<AgentPublishButton />
|
||||
{ENABLE_BUSINESS_FEATURES && (
|
||||
{enableBusinessFeatures && (
|
||||
<Button icon={Clock} onClick={handleCreateCronJob}>
|
||||
{t('agentCronJobs.addJob')}
|
||||
</Button>
|
||||
@@ -106,7 +107,7 @@ const ProfileEditor = memo(() => {
|
||||
{/* Main Content: Prompt Editor */}
|
||||
<EditorCanvas />
|
||||
{/* Agent Cron Jobs Display (only show if jobs exist) */}
|
||||
{ENABLE_BUSINESS_FEATURES && <AgentCronJobs />}
|
||||
{enableBusinessFeatures && <AgentCronJobs />}
|
||||
{/* Advanced Settings Modal */}
|
||||
<AgentSettings />
|
||||
</>
|
||||
|
||||
@@ -37,11 +37,20 @@ export default {
|
||||
'agentCronJobs.empty.description': 'Create your first scheduled task to automate your agent',
|
||||
'agentCronJobs.empty.title': 'No scheduled tasks yet',
|
||||
'agentCronJobs.enable': 'Enable',
|
||||
'agentCronJobs.form.at': 'at',
|
||||
'agentCronJobs.form.content.placeholder': 'Enter the prompt or instruction for the agent',
|
||||
'agentCronJobs.form.every': 'Every',
|
||||
'agentCronJobs.form.frequency': 'Frequency',
|
||||
'agentCronJobs.form.hours': 'hour(s)',
|
||||
'agentCronJobs.form.maxExecutions': 'Stop after',
|
||||
'agentCronJobs.form.maxExecutions.placeholder': 'Leave empty for unlimited',
|
||||
'agentCronJobs.form.name.placeholder': 'Enter task name',
|
||||
'agentCronJobs.form.time': 'Time',
|
||||
'agentCronJobs.form.timeRange.end': 'End Time',
|
||||
'agentCronJobs.form.timeRange.start': 'Start Time',
|
||||
'agentCronJobs.form.times': 'times',
|
||||
'agentCronJobs.form.timezone': 'Timezone',
|
||||
'agentCronJobs.form.unlimited': 'Run continuously',
|
||||
'agentCronJobs.form.validation.contentRequired': 'Task content is required',
|
||||
'agentCronJobs.form.validation.invalidTimeRange': 'Start time must be before end time',
|
||||
'agentCronJobs.form.validation.nameRequired': 'Task name is required',
|
||||
@@ -86,6 +95,13 @@ export default {
|
||||
'agentCronJobs.weekday.tuesday': 'Tuesday',
|
||||
'agentCronJobs.weekday.wednesday': 'Wednesday',
|
||||
'agentCronJobs.weekdays': 'Weekdays',
|
||||
'agentCronJobs.weekdays.fri': 'Fri',
|
||||
'agentCronJobs.weekdays.mon': 'Mon',
|
||||
'agentCronJobs.weekdays.sat': 'Sat',
|
||||
'agentCronJobs.weekdays.sun': 'Sun',
|
||||
'agentCronJobs.weekdays.thu': 'Thu',
|
||||
'agentCronJobs.weekdays.tue': 'Tue',
|
||||
'agentCronJobs.weekdays.wed': 'Wed',
|
||||
'agentInfoDescription.basic.avatar': 'Avatar',
|
||||
'agentInfoDescription.basic.description': 'Description',
|
||||
'agentInfoDescription.basic.name': 'Name',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
||||
import type { SWRResponse } from 'swr';
|
||||
import { type StateCreator } from 'zustand/vanilla';
|
||||
|
||||
@@ -33,7 +32,10 @@ export interface CronTopicGroupWithJobInfo {
|
||||
export interface CronSliceAction {
|
||||
createAgentCronJob: () => Promise<string | null>;
|
||||
internal_refreshCronTopics: () => Promise<void>;
|
||||
useFetchCronTopicsWithJobInfo: (agentId?: string) => SWRResponse<CronTopicGroupWithJobInfo[]>;
|
||||
useFetchCronTopicsWithJobInfo: (
|
||||
agentId?: string,
|
||||
enabled?: boolean,
|
||||
) => SWRResponse<CronTopicGroupWithJobInfo[]>;
|
||||
}
|
||||
|
||||
export const createCronSlice: StateCreator<
|
||||
@@ -69,9 +71,9 @@ export const createCronSlice: StateCreator<
|
||||
await mutate([FETCH_CRON_TOPICS_WITH_JOB_INFO_KEY, get().activeAgentId]);
|
||||
},
|
||||
|
||||
useFetchCronTopicsWithJobInfo: (agentId) =>
|
||||
useFetchCronTopicsWithJobInfo: (agentId, enabled = true) =>
|
||||
useClientDataSWR<CronTopicGroupWithJobInfo[]>(
|
||||
ENABLE_BUSINESS_FEATURES && agentId ? [FETCH_CRON_TOPICS_WITH_JOB_INFO_KEY, agentId] : null,
|
||||
enabled && agentId ? [FETCH_CRON_TOPICS_WITH_JOB_INFO_KEY, agentId] : null,
|
||||
async ([, id]: [string, string]) => {
|
||||
const [cronJobsResult, cronTopicsGroups] = await Promise.all([
|
||||
lambdaClient.agentCronJob.findByAgent.query({ agentId: id }),
|
||||
|
||||
Reference in New Issue
Block a user