diff --git a/apps/frontend/src/components/automations-form.tsx b/apps/frontend/src/components/automations-form.tsx index 72ef96365..4a595ceac 100644 --- a/apps/frontend/src/components/automations-form.tsx +++ b/apps/frontend/src/components/automations-form.tsx @@ -6,6 +6,7 @@ import type { McpServerStatus } from '@nao/shared'; import type { LlmProvider } from '@nao/shared/types'; import type { FormEvent, ReactNode, RefObject } from 'react'; import type { PromptHandle } from 'prompt-mentions'; +import type { ScheduleConfig, ScheduleFrequency } from '@/lib/cron-schedule'; import McpIcon from '@/components/icons/model-context-protocol.svg'; import SlackIcon from '@/components/icons/slack.svg'; import { ChatPrompt, DATABASE_MENTION_TRIGGER, SKILL_MENTION_TRIGGER } from '@/components/chat-input-prompt'; @@ -26,6 +27,14 @@ import { Input } from '@/components/ui/input'; import { LlmProviderIcon } from '@/components/ui/llm-provider-icon'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { cn } from '@/lib/utils'; +import { + buildScheduleCron, + DAY_OF_WEEK_LABELS, + defaultScheduleConfig, + describeCron, + describeSchedule, + parseScheduleCron, +} from '@/lib/cron-schedule'; import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'; import { useSession } from '@/lib/auth-client'; import { trpc } from '@/main'; @@ -122,14 +131,9 @@ type AutomationDetails = { lastRunAt?: Date | string | null; }; -type ScheduleOption = 'hourly' | 'daily' | 'weekdays' | 'weekly' | 'monthly' | 'custom'; +type ScheduleMode = 'friendly' | 'advanced'; -type SchedulePreset = { - value: Exclude; - label: string; - cron: string; - description: string; -}; +type ScheduleSelectOption = ScheduleFrequency | 'advanced'; type AvailableModel = { provider: LlmProvider; @@ -139,9 +143,6 @@ type AvailableModel = { const defaultModelValue = 'default'; const MODE_MENTION_TRIGGER = '#'; -const DEFAULT_SCHEDULE_CRON = '0 9 * * 1'; -const DEFAULT_SCHEDULE_DESCRIPTION = 'Every Monday at 9am'; -const CUSTOM_SCHEDULE_DESCRIPTION = 'Custom schedule'; const defaultValue: AutomationFormValue = { title: '', @@ -157,14 +158,20 @@ const defaultValue: AutomationFormValue = { webhookEnabled: false, }; -const schedulePresets: SchedulePreset[] = [ - { value: 'hourly', label: 'Hourly', cron: '0 * * * *', description: 'Hourly' }, - { value: 'daily', label: 'Daily', cron: '0 9 * * *', description: 'Daily at 9am' }, - { value: 'weekdays', label: 'Weekdays', cron: '0 9 * * 1-5', description: 'Weekdays at 9am' }, - { value: 'weekly', label: 'Weekly', cron: DEFAULT_SCHEDULE_CRON, description: DEFAULT_SCHEDULE_DESCRIPTION }, - { value: 'monthly', label: 'Monthly', cron: '0 9 1 * *', description: 'Monthly on the 1st at 9am' }, +const scheduleFrequencyOptions: Array<{ value: ScheduleFrequency; label: string }> = [ + { value: 'hourly', label: 'Hourly' }, + { value: 'daily', label: 'Daily' }, + { value: 'weekdays', label: 'Weekdays' }, + { value: 'weekly', label: 'Weekly' }, + { value: 'monthly', label: 'Monthly' }, ]; +const dayOfMonthOptions = Array.from({ length: 31 }, (_, index) => index + 1); +const weekdayToggleOptions: Array<{ value: number; label: string }> = [1, 2, 3, 4, 5, 6, 0].map((value) => ({ + value, + label: DAY_OF_WEEK_LABELS[value].charAt(0), +})); + export function AutomationForm({ id, automationId, @@ -211,8 +218,10 @@ export function AutomationForm({ (savedValue); - const [scheduleOption, setScheduleOption] = useState(() => inferScheduleOption(savedValue)); + const [scheduleMode, setScheduleMode] = useState(() => inferScheduleMode(savedValue.cron)); const [hasSchedule, setHasSchedule] = useState(() => savedValue.cron.trim().length > 0); const [promptError, setPromptError] = useState(false); const [triggerError, setTriggerError] = useState(false); @@ -329,7 +338,7 @@ function useAutomationFormController({ const nextValue = deserializeAutomationValue(initialValueSnapshot); setSavedValue(nextValue); setValue(nextValue); - setScheduleOption(inferScheduleOption(nextValue)); + setScheduleMode(inferScheduleMode(nextValue.cron)); setHasSchedule(nextValue.cron.trim().length > 0); setPromptError(false); setTriggerError(false); @@ -387,7 +396,7 @@ function useAutomationFormController({ setValue({ ...value, cron, - scheduleDescription: CUSTOM_SCHEDULE_DESCRIPTION, + scheduleDescription: describeCron(cron), }); } @@ -463,17 +472,17 @@ function useAutomationFormController({ function handleAddSchedule() { setTriggerError(false); - setScheduleOption('weekly'); + setScheduleMode('friendly'); setHasSchedule(true); handleControlValueChange({ ...value, - cron: DEFAULT_SCHEDULE_CRON, - scheduleDescription: DEFAULT_SCHEDULE_DESCRIPTION, + cron: buildScheduleCron(defaultScheduleConfig), + scheduleDescription: describeSchedule(defaultScheduleConfig), }); } function handleRemoveSchedule() { - setScheduleOption('custom'); + setScheduleMode('friendly'); setHasSchedule(false); handleControlValueChange({ ...value, @@ -491,22 +500,25 @@ function useAutomationFormController({ handleControlValueChange({ ...value, webhookEnabled: false }); } - function handleScheduleOptionChange(option: ScheduleOption) { + function applyScheduleConfig(config: ScheduleConfig) { setTriggerError(false); - setScheduleOption(option); - if (option === 'custom') { - handleControlValueChange({ ...value, scheduleDescription: CUSTOM_SCHEDULE_DESCRIPTION }); - return; - } - - const preset = getSchedulePreset(option); + setScheduleMode('friendly'); handleControlValueChange({ ...value, - cron: preset.cron, - scheduleDescription: preset.description, + cron: buildScheduleCron(config), + scheduleDescription: describeSchedule(config), }); } + function handleScheduleSelectChange(option: ScheduleSelectOption) { + if (option === 'advanced') { + setScheduleMode('advanced'); + return; + } + const currentConfig = parseScheduleCron(value.cron) ?? defaultScheduleConfig; + applyScheduleConfig({ ...currentConfig, frequency: option }); + } + function handleModelChange(modelValue: string) { if (modelValue === defaultModelValue) { handleControlValueChange({ ...value, modelProvider: undefined, modelId: undefined }); @@ -520,6 +532,7 @@ function useAutomationFormController({ } return { + applyScheduleConfig, availableModels: availableModels.data, clearEmailRecipientsError, controlsDisabled, @@ -533,14 +546,15 @@ function useAutomationFormController({ handlePromptChange, handleRemoveSchedule, handleRemoveWebhook, - handleScheduleOptionChange, + handleScheduleSelectChange, handleSubmit, handleValueChange, hasSchedule, mcpServers: mcpServersQuery.data, promptError, promptRef, - scheduleOption, + scheduleConfig: parseScheduleCron(value.cron) ?? defaultScheduleConfig, + scheduleMode, selectedModelName, selectedModelValue, setCustomCron, @@ -577,8 +591,10 @@ function AutomationTitleField({ function TriggersSection({ cron, hasSchedule, - scheduleOption, - onScheduleOptionChange, + scheduleMode, + scheduleConfig, + onScheduleSelectChange, + onScheduleConfigChange, onCustomCronChange, onAddSchedule, onRemoveSchedule, @@ -591,8 +607,10 @@ function TriggersSection({ }: { cron: string; hasSchedule: boolean; - scheduleOption: ScheduleOption; - onScheduleOptionChange: (option: ScheduleOption) => void; + scheduleMode: ScheduleMode; + scheduleConfig: ScheduleConfig; + onScheduleSelectChange: (option: ScheduleSelectOption) => void; + onScheduleConfigChange: (config: ScheduleConfig) => void; onCustomCronChange: (cron: string) => void; onAddSchedule: () => void; onRemoveSchedule: () => void; @@ -617,8 +635,10 @@ function TriggersSection({ {hasSchedule && ( void; + scheduleMode: ScheduleMode; + scheduleConfig: ScheduleConfig; + onScheduleSelectChange: (option: ScheduleSelectOption) => void; + onScheduleConfigChange: (config: ScheduleConfig) => void; onCustomCronChange: (cron: string) => void; onRemove: () => void; disabled: boolean; }) { + const selectValue: ScheduleSelectOption = scheduleMode === 'advanced' ? 'advanced' : scheduleConfig.frequency; + return (
-
-
+
+
On schedule + + {scheduleMode === 'advanced' ? ( + + ) : ( + + )}
-
- onChange(next as ScheduleSelectOption)} disabled={disabled}> + + + + + {scheduleFrequencyOptions.map((option) => ( + + {option.label} + + ))} + Advanced (cron) + + + ); +} + +function FriendlyScheduleControls({ + config, + onChange, + disabled, +}: { + config: ScheduleConfig; + onChange: (config: ScheduleConfig) => void; + disabled: boolean; +}) { + if (config.frequency === 'hourly') { + return ( + <> + at minute + onChange({ ...config, minute })} + disabled={disabled} + /> + + ); + } + + return ( + <> + {config.frequency === 'weekly' && ( + <> + on + onChange({ ...config, daysOfWeek })} disabled={disabled} - > - - - - - {schedulePresets.map((preset) => ( - - {preset.label} - - ))} - Custom - - + /> + + )} + {config.frequency === 'monthly' && ( + <> + on day + onChange({ ...config, dayOfMonth })} + disabled={disabled} + /> + + )} + at + onChange({ ...config, hour, minute })} + disabled={disabled} + /> + + ); +} + +function TimeField({ + hour, + minute, + onChange, + disabled, +}: { + hour: number; + minute: number; + onChange: (hour: number, minute: number) => void; + disabled: boolean; +}) { + return ( + { + const match = /^(\d{2}):(\d{2})$/.exec(event.target.value); + if (match) { + onChange(Number(match[1]), Number(match[2])); + } + }} + /> + ); +} + +function MinuteInput({ + minute, + onChange, + disabled, +}: { + minute: number; + onChange: (minute: number) => void; + disabled: boolean; +}) { + return ( + { + const parsed = Number.parseInt(event.target.value, 10); + if (!Number.isNaN(parsed)) { + onChange(Math.min(Math.max(parsed, 0), 59)); + } + }} + /> + ); +} + +function DaysOfWeekToggle({ + daysOfWeek, + onChange, + disabled, +}: { + daysOfWeek: number[]; + onChange: (daysOfWeek: number[]) => void; + disabled: boolean; +}) { + const selected = new Set(daysOfWeek); + + function toggleDay(day: number) { + const next = new Set(selected); + if (next.has(day)) { + if (next.size === 1) { + return; + } + next.delete(day); + } else { + next.add(day); + } + onChange([...next].sort((left, right) => left - right)); + } + + return ( +
+ {weekdayToggleOptions.map((option) => { + const isSelected = selected.has(option.value); + return ( -
-
- - {scheduleOption === 'custom' && ( - onCustomCronChange(event.target.value)} - placeholder='0 9 * * 1' - className='h-8' - /> - )} + ); + })}
); } +function DayOfMonthSelect({ + dayOfMonth, + onChange, + disabled, +}: { + dayOfMonth: number; + onChange: (dayOfMonth: number) => void; + disabled: boolean; +}) { + return ( + + ); +} + +function AdvancedScheduleInput({ + cron, + onChange, + disabled, +}: { + cron: string; + onChange: (cron: string) => void; + disabled: boolean; +}) { + return ( + onChange(event.target.value)} + placeholder='0 9 * * 1' + className='h-8 w-44 font-mono' + disabled={disabled} + aria-label='Cron expression' + /> + ); +} + function WebhookTriggerRow({ url, onRemove, @@ -1788,19 +2034,15 @@ function mergeValue(value: Partial | undefined): Automation }; } -function getScheduleOption(cron: string): ScheduleOption { - return schedulePresets.find((preset) => preset.cron === cron)?.value ?? 'custom'; -} - -function inferScheduleOption(value: AutomationFormValue): ScheduleOption { - if (!value.cron) { - return 'weekly'; +function inferScheduleMode(cron: string): ScheduleMode { + if (!cron.trim()) { + return 'friendly'; } - return value.scheduleDescription === CUSTOM_SCHEDULE_DESCRIPTION ? 'custom' : getScheduleOption(value.cron); + return parseScheduleCron(cron) ? 'friendly' : 'advanced'; } -function getSchedulePreset(value: Exclude): SchedulePreset { - return schedulePresets.find((preset) => preset.value === value) ?? schedulePresets[0]; +function padTwo(value: number): string { + return value.toString().padStart(2, '0'); } function areAutomationValuesEqual(left: AutomationFormValue, right: AutomationFormValue): boolean { diff --git a/apps/frontend/src/lib/cron-schedule.test.ts b/apps/frontend/src/lib/cron-schedule.test.ts new file mode 100644 index 000000000..d99e2984c --- /dev/null +++ b/apps/frontend/src/lib/cron-schedule.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'vitest'; + +import { buildScheduleCron, describeCron, parseScheduleCron } from './cron-schedule'; +import type { ScheduleConfig } from './cron-schedule'; + +const base: ScheduleConfig = { frequency: 'daily', minute: 0, hour: 9, daysOfWeek: [1], dayOfMonth: 1 }; + +describe('buildScheduleCron', () => { + it('builds hourly, daily, weekdays, weekly and monthly crons', () => { + expect(buildScheduleCron({ ...base, frequency: 'hourly', minute: 30 })).toBe('30 * * * *'); + expect(buildScheduleCron({ ...base, frequency: 'daily', hour: 9, minute: 0 })).toBe('0 9 * * *'); + expect(buildScheduleCron({ ...base, frequency: 'weekdays', hour: 8, minute: 15 })).toBe('15 8 * * 1-5'); + expect(buildScheduleCron({ ...base, frequency: 'weekly', hour: 8, minute: 0, daysOfWeek: [1] })).toBe( + '0 8 * * 1', + ); + expect(buildScheduleCron({ ...base, frequency: 'weekly', hour: 8, minute: 0, daysOfWeek: [5, 1, 3] })).toBe( + '0 8 * * 1,3,5', + ); + expect(buildScheduleCron({ ...base, frequency: 'monthly', hour: 9, minute: 0, dayOfMonth: 1 })).toBe( + '0 9 1 * *', + ); + }); +}); + +describe('parseScheduleCron', () => { + it('round-trips friendly cron expressions across every relevant field', () => { + const cases: Array<{ config: ScheduleConfig; fields: Array }> = [ + { config: { ...base, frequency: 'hourly', minute: 37 }, fields: ['frequency', 'minute'] }, + { config: { ...base, frequency: 'daily', hour: 14, minute: 45 }, fields: ['frequency', 'hour', 'minute'] }, + { config: { ...base, frequency: 'weekdays', hour: 6, minute: 0 }, fields: ['frequency', 'hour', 'minute'] }, + { + config: { ...base, frequency: 'weekly', hour: 8, minute: 0, daysOfWeek: [5] }, + fields: ['frequency', 'hour', 'minute', 'daysOfWeek'], + }, + { + config: { ...base, frequency: 'weekly', hour: 8, minute: 0, daysOfWeek: [1, 3, 5] }, + fields: ['frequency', 'hour', 'minute', 'daysOfWeek'], + }, + { + config: { ...base, frequency: 'monthly', hour: 9, minute: 0, dayOfMonth: 15 }, + fields: ['frequency', 'hour', 'minute', 'dayOfMonth'], + }, + { + config: { ...base, frequency: 'monthly', hour: 23, minute: 5, dayOfMonth: 31 }, + fields: ['frequency', 'hour', 'minute', 'dayOfMonth'], + }, + ]; + + for (const { config, fields } of cases) { + const parsed = parseScheduleCron(buildScheduleCron(config)); + expect(parsed).not.toBeNull(); + for (const field of fields) { + expect(parsed?.[field]).toEqual(config[field]); + } + } + }); + + it('parses a weekly multi-day list', () => { + expect(parseScheduleCron('0 8 * * 1,3,5')?.daysOfWeek).toEqual([1, 3, 5]); + }); + + it('keeps 1-5 as Weekdays but comma lists as Weekly', () => { + expect(parseScheduleCron('0 8 * * 1-5')?.frequency).toBe('weekdays'); + expect(parseScheduleCron('0 8 * * 1,2,3,4,5')?.frequency).toBe('weekly'); + }); + + it('normalizes Sunday expressed as 7 to 0', () => { + expect(parseScheduleCron('0 8 * * 7')?.daysOfWeek).toEqual([0]); + }); + + it('returns null for expressions outside the friendly shapes', () => { + expect(parseScheduleCron('*/5 * * * *')).toBeNull(); + expect(parseScheduleCron('0 9-17 * * *')).toBeNull(); + expect(parseScheduleCron('0 9 * 6 *')).toBeNull(); + expect(parseScheduleCron('not a cron')).toBeNull(); + }); +}); + +describe('describeCron', () => { + it('describes friendly crons and falls back for custom ones', () => { + expect(describeCron('0 9 * * *')).toBe('Daily at 09:00'); + expect(describeCron('0 8 * * 1')).toBe('Every Monday at 08:00'); + expect(describeCron('0 8 * * 1,3,5')).toBe('Every Mon, Wed, Fri at 08:00'); + expect(describeCron('37 * * * *')).toBe('Hourly at :37'); + expect(describeCron('0 9 1 * *')).toBe('Monthly on the 1st at 09:00'); + expect(describeCron('*/5 * * * *')).toBe('Custom schedule'); + }); +}); diff --git a/apps/frontend/src/lib/cron-schedule.ts b/apps/frontend/src/lib/cron-schedule.ts new file mode 100644 index 000000000..0dadde7a8 --- /dev/null +++ b/apps/frontend/src/lib/cron-schedule.ts @@ -0,0 +1,190 @@ +export type ScheduleFrequency = 'hourly' | 'daily' | 'weekdays' | 'weekly' | 'monthly'; + +export type ScheduleConfig = { + frequency: ScheduleFrequency; + minute: number; + hour: number; + daysOfWeek: number[]; + dayOfMonth: number; +}; + +export const DAY_OF_WEEK_LABELS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + +export const defaultScheduleConfig: ScheduleConfig = { + frequency: 'weekly', + minute: 0, + hour: 9, + daysOfWeek: [1], + dayOfMonth: 1, +}; + +export function buildScheduleCron(config: ScheduleConfig): string { + const minute = clamp(config.minute, 0, 59); + const hour = clamp(config.hour, 0, 23); + + switch (config.frequency) { + case 'hourly': + return `${minute} * * * *`; + case 'daily': + return `${minute} ${hour} * * *`; + case 'weekdays': + return `${minute} ${hour} * * 1-5`; + case 'weekly': + return `${minute} ${hour} * * ${formatDaysOfWeek(config.daysOfWeek)}`; + case 'monthly': + return `${minute} ${hour} ${clamp(config.dayOfMonth, 1, 31)} * *`; + } +} + +/** + * Parse a cron expression back into a friendly schedule config. Returns null + * when the expression does not match one of the friendly shapes, signalling the + * UI to fall back to the advanced (raw cron) editor. + */ +export function parseScheduleCron(cron: string): ScheduleConfig | null { + const fields = cron.trim().split(/\s+/); + if (fields.length !== 5) { + return null; + } + + const [minuteField, hourField, dayOfMonthField, monthField, dayOfWeekField] = fields; + if (monthField !== '*') { + return null; + } + + const minute = parseNumericField(minuteField, 0, 59); + if (minute === null) { + return null; + } + + if (hourField === '*') { + return dayOfMonthField === '*' && dayOfWeekField === '*' + ? { ...defaultScheduleConfig, frequency: 'hourly', minute } + : null; + } + + const hour = parseNumericField(hourField, 0, 23); + if (hour === null) { + return null; + } + + if (dayOfMonthField === '*' && dayOfWeekField === '*') { + return { ...defaultScheduleConfig, frequency: 'daily', minute, hour }; + } + + if (dayOfMonthField === '*' && dayOfWeekField === '1-5') { + return { ...defaultScheduleConfig, frequency: 'weekdays', minute, hour }; + } + + if (dayOfMonthField === '*') { + const daysOfWeek = parseDaysOfWeek(dayOfWeekField); + if (daysOfWeek === null) { + return null; + } + return { ...defaultScheduleConfig, frequency: 'weekly', minute, hour, daysOfWeek }; + } + + if (dayOfWeekField === '*') { + const dayOfMonth = parseNumericField(dayOfMonthField, 1, 31); + if (dayOfMonth === null) { + return null; + } + return { ...defaultScheduleConfig, frequency: 'monthly', minute, hour, dayOfMonth }; + } + + return null; +} + +export function describeSchedule(config: ScheduleConfig): string { + const time = formatTime(config.hour, config.minute); + + switch (config.frequency) { + case 'hourly': + return `Hourly at :${padTwo(config.minute)}`; + case 'daily': + return `Daily at ${time}`; + case 'weekdays': + return `Weekdays at ${time}`; + case 'weekly': + return `${describeDaysOfWeek(config.daysOfWeek)} at ${time}`; + case 'monthly': + return `Monthly on the ${formatOrdinal(config.dayOfMonth)} at ${time}`; + } +} + +export function describeCron(cron: string): string { + const config = parseScheduleCron(cron); + return config ? describeSchedule(config) : 'Custom schedule'; +} + +export function formatTime(hour: number, minute: number): string { + return `${padTwo(hour)}:${padTwo(minute)}`; +} + +export function normalizeDaysOfWeek(days: number[]): number[] { + const unique = new Set(days.map((day) => (day === 7 ? 0 : day)).filter((day) => day >= 0 && day <= 6)); + return [...unique].sort((left, right) => left - right); +} + +function formatDaysOfWeek(days: number[]): string { + const normalized = normalizeDaysOfWeek(days); + return (normalized.length > 0 ? normalized : defaultScheduleConfig.daysOfWeek).join(','); +} + +function parseDaysOfWeek(field: string): number[] | null { + const tokens = field.split(','); + const days = new Set(); + for (const token of tokens) { + const value = parseNumericField(token, 0, 7); + if (value === null) { + return null; + } + days.add(value === 7 ? 0 : value); + } + return [...days].sort((left, right) => left - right); +} + +function describeDaysOfWeek(days: number[]): string { + const normalized = normalizeDaysOfWeek(days); + if (normalized.length === 0) { + return 'Weekly'; + } + if (normalized.length === 7) { + return 'Every day'; + } + if (normalized.length === 1) { + return `Every ${DAY_OF_WEEK_LABELS[normalized[0]]}`; + } + return `Every ${normalized.map((day) => DAY_OF_WEEK_LABELS[day].slice(0, 3)).join(', ')}`; +} + +function parseNumericField(field: string, min: number, max: number): number | null { + if (!/^\d+$/.test(field)) { + return null; + } + const value = Number(field); + return value >= min && value <= max ? value : null; +} + +function formatOrdinal(day: number): string { + const remainderTen = day % 10; + const remainderHundred = day % 100; + if (remainderTen === 1 && remainderHundred !== 11) { + return `${day}st`; + } + if (remainderTen === 2 && remainderHundred !== 12) { + return `${day}nd`; + } + if (remainderTen === 3 && remainderHundred !== 13) { + return `${day}rd`; + } + return `${day}th`; +} + +function padTwo(value: number): string { + return value.toString().padStart(2, '0'); +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} diff --git a/apps/frontend/src/routeTree.gen.ts b/apps/frontend/src/routeTree.gen.ts index 15ea7f420..9b0ea4762 100644 --- a/apps/frontend/src/routeTree.gen.ts +++ b/apps/frontend/src/routeTree.gen.ts @@ -306,7 +306,6 @@ const SidebarLayoutStoriesPreviewChatIdStorySlugRoute = } as any) export interface FileRoutesByFullPath { - '/': typeof SidebarLayoutChatLayoutIndexRoute '/consent': typeof ConsentRoute '/embed': typeof EmbedRouteWithChildren '/forgot-password': typeof ForgotPasswordRoute @@ -331,9 +330,10 @@ export interface FileRoutesByFullPath { '/shared-chat/$shareId': typeof SidebarLayoutSharedChatShareIdRoute '/embed/chart/$chartEmbedId': typeof EmbedChartChartEmbedIdRoute '/embed/story/$storyId': typeof EmbedStoryStoryIdRoute - '/feed/': typeof SidebarLayoutFeedIndexRoute + '/': typeof SidebarLayoutChatLayoutIndexRoute + '/feed': typeof SidebarLayoutFeedIndexRoute '/settings/': typeof SidebarLayoutSettingsIndexRoute - '/stories/': typeof SidebarLayoutStoriesIndexRoute + '/stories': typeof SidebarLayoutStoriesIndexRoute '/settings/project/agent': typeof SidebarLayoutSettingsProjectAgentRoute '/settings/project/budgets': typeof SidebarLayoutSettingsProjectBudgetsRoute '/settings/project/mcp-endpoint': typeof SidebarLayoutSettingsProjectMcpEndpointRoute @@ -350,7 +350,6 @@ export interface FileRoutesByFullPath { '/stories/preview/$chatId/$storySlug': typeof SidebarLayoutStoriesPreviewChatIdStorySlugRoute } export interface FileRoutesByTo { - '/': typeof SidebarLayoutChatLayoutIndexRoute '/consent': typeof ConsentRoute '/embed': typeof EmbedRouteWithChildren '/forgot-password': typeof ForgotPasswordRoute @@ -373,6 +372,7 @@ export interface FileRoutesByTo { '/shared-chat/$shareId': typeof SidebarLayoutSharedChatShareIdRoute '/embed/chart/$chartEmbedId': typeof EmbedChartChartEmbedIdRoute '/embed/story/$storyId': typeof EmbedStoryStoryIdRoute + '/': typeof SidebarLayoutChatLayoutIndexRoute '/feed': typeof SidebarLayoutFeedIndexRoute '/settings': typeof SidebarLayoutSettingsIndexRoute '/stories': typeof SidebarLayoutStoriesIndexRoute @@ -441,7 +441,6 @@ export interface FileRoutesById { export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: - | '/' | '/consent' | '/embed' | '/forgot-password' @@ -466,9 +465,10 @@ export interface FileRouteTypes { | '/shared-chat/$shareId' | '/embed/chart/$chartEmbedId' | '/embed/story/$storyId' - | '/feed/' + | '/' + | '/feed' | '/settings/' - | '/stories/' + | '/stories' | '/settings/project/agent' | '/settings/project/budgets' | '/settings/project/mcp-endpoint' @@ -485,7 +485,6 @@ export interface FileRouteTypes { | '/stories/preview/$chatId/$storySlug' fileRoutesByTo: FileRoutesByTo to: - | '/' | '/consent' | '/embed' | '/forgot-password' @@ -508,6 +507,7 @@ export interface FileRouteTypes { | '/shared-chat/$shareId' | '/embed/chart/$chartEmbedId' | '/embed/story/$storyId' + | '/' | '/feed' | '/settings' | '/stories' @@ -630,7 +630,7 @@ declare module '@tanstack/react-router' { '/_sidebar-layout': { id: '/_sidebar-layout' path: '' - fullPath: '/' + fullPath: '' preLoaderRoute: typeof SidebarLayoutRouteImport parentRoute: typeof rootRouteImport } @@ -644,14 +644,14 @@ declare module '@tanstack/react-router' { '/_sidebar-layout/_chat-layout': { id: '/_sidebar-layout/_chat-layout' path: '' - fullPath: '/' + fullPath: '' preLoaderRoute: typeof SidebarLayoutChatLayoutRouteImport parentRoute: typeof SidebarLayoutRoute } '/_sidebar-layout/stories/': { id: '/_sidebar-layout/stories/' path: '/stories' - fullPath: '/stories/' + fullPath: '/stories' preLoaderRoute: typeof SidebarLayoutStoriesIndexRouteImport parentRoute: typeof SidebarLayoutRoute } @@ -665,7 +665,7 @@ declare module '@tanstack/react-router' { '/_sidebar-layout/feed/': { id: '/_sidebar-layout/feed/' path: '/feed' - fullPath: '/feed/' + fullPath: '/feed' preLoaderRoute: typeof SidebarLayoutFeedIndexRouteImport parentRoute: typeof SidebarLayoutRoute }