Files
openswarm/frontend/src/app/pages/Workflows/scheduleDetect.ts
T

104 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Lightweight text-to-schedule detector. Runs on agent replies (and user
// prompts) to surface a "Schedule this?" chip when the conversation has
// time-shaped language. Cheap regex pass, no LLM call. Returns the best
// matching preset or null. Conservative on purpose: a false positive
// shows a quietly-dismissable chip; a false negative just means the user
// uses the regular Schedule button.
import type { ScheduleConfig } from '@/shared/state/workflowsSlice';
import { defaultSchedule } from './scheduleUtils';
export interface DetectedSchedule {
schedule: ScheduleConfig;
presetLabel: string;
}
const HOUR_WORDS: Record<string, number> = {
morning: 9, noon: 12, afternoon: 14, evening: 18, night: 21, midnight: 0,
};
// Match "9am" / "9 a.m." / "10:30 PM" / "at 7am" — but ONLY when there's
// either an explicit am/pm suffix or an "at " prefix. Plain digits with
// no time context ("3 new messages", "May 16", "$50 offer") used to slip
// through and we'd misread them as the schedule hour. Anchoring on
// `(am|pm)` OR `at ` blocks that.
const HOUR_RE = /\b(?:at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm|a\.m\.|p\.m\.)?|(\d{1,2})(?::(\d{2}))?\s*(am|pm|a\.m\.|p\.m\.))\b/i;
const DAY_RE = /\b(sun|mon|tue|wed|thu|fri|sat)(?:day)?s?\b/gi;
const DAY_MAP: Record<string, number> = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
export function detectSchedule(text: string): DetectedSchedule | null {
if (!text) return null;
const t = text.toLowerCase();
// Require either an explicit frequency keyword or a clear weekday +
// time pattern. Avoids false positives on stray "tomorrow at 9."
const isDaily = /\b(every ?day|each day|daily)\b/.test(t);
const isWeekdays = /\b(weekdays?|each weekday|every weekday|mon(?:day)?\s*(?:to|-|through|–)\s*fri(?:day)?)\b/.test(t);
const isWeekly = /\b(every week|weekly|each week|once a week)\b/.test(t);
const isMonthly = /\b(every month|monthly|each month|once a month)\b/.test(t);
const dayMatches = Array.from(t.matchAll(DAY_RE)).map((m) => DAY_MAP[m[1].toLowerCase().slice(0, 3)]);
const hasExplicitDays = dayMatches.length > 0;
if (!isDaily && !isWeekdays && !isWeekly && !isMonthly && !hasExplicitDays) return null;
// Extract hour:minute.
let hour = 9;
let minute = 0;
let presetTimeWord: string | null = null;
for (const word of Object.keys(HOUR_WORDS)) {
if (t.includes(word)) { hour = HOUR_WORDS[word]; presetTimeWord = word; break; }
}
const hm = t.match(HOUR_RE);
if (hm) {
// The two branches of HOUR_RE give us hour/minute/ampm in either
// capture group 1-3 (the "at H" branch) or 4-6 (the "Ham/pm" branch).
const rawStr = hm[1] || hm[4];
const minStr = hm[2] || hm[5];
const ampm = (hm[3] || hm[6] || '').toLowerCase();
const raw = rawStr ? parseInt(rawStr, 10) : NaN;
const m = minStr ? parseInt(minStr, 10) : 0;
let h = raw;
if (ampm.startsWith('p') && h < 12) h += 12;
if (ampm.startsWith('a') && h === 12) h = 0;
if (Number.isFinite(h) && h >= 0 && h < 24) {
if (!presetTimeWord) { hour = h; minute = m; }
}
}
const base = defaultSchedule();
if (isMonthly) {
return {
schedule: { ...base, enabled: true, repeat_unit: 'month', repeat_every: 1, hour, minute },
presetLabel: `Every month at ${formatHour(hour, minute)}`,
};
}
if (isWeekdays) {
return {
schedule: { ...base, enabled: true, repeat_unit: 'week', repeat_every: 1, on_days: [1, 2, 3, 4, 5], hour, minute },
presetLabel: `Weekdays at ${formatHour(hour, minute)}`,
};
}
if (isDaily) {
return {
schedule: { ...base, enabled: true, repeat_unit: 'day', repeat_every: 1, hour, minute },
presetLabel: `Every day at ${formatHour(hour, minute)}`,
};
}
if (hasExplicitDays || isWeekly) {
const days = Array.from(new Set(dayMatches.length ? dayMatches : [new Date().getDay()]));
days.sort();
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const label = days.length === 1 ? `Every ${dayNames[days[0]]} at ${formatHour(hour, minute)}` : `${days.map((d) => dayNames[d]).join('/')} at ${formatHour(hour, minute)}`;
return {
schedule: { ...base, enabled: true, repeat_unit: 'week', repeat_every: 1, on_days: days, hour, minute },
presetLabel: label,
};
}
return null;
}
function formatHour(h: number, m: number): string {
const suffix = h < 12 ? 'am' : 'pm';
const h12 = ((h + 11) % 12) + 1;
return m === 0 ? `${h12}${suffix}` : `${h12}:${String(m).padStart(2, '0')}${suffix}`;
}