SurfSense/surfsense_web/lib/automations/describe-cron.ts
CREDO23 2e572d7818 feat(web): create_automation HITL approval card in chat
Closes the create loop in chat: the agent describes user intent → the
drafter sub-LLM produces an AutomationCreate JSON → this card surfaces
a structured preview → approve persists; reject cancels. Edits flow
through chat refinement (re-call with a refined intent), not in-card,
so the card stays simple and the multi-turn checkpointer carries the
context.

Tool UI (components/tool-ui/automation/):
- create-automation.tsx — entry dispatcher + ApprovalCard chrome
  (pending/processing/complete/rejected via useHitlPhase) + SavedCard
  (links to the detail page) + InvalidCard (lists drafter validation
  issues) + ErrorCard (verbatim message). Rejection result is hidden
  because the approval card itself shows the rejected phase inline.
- automation-draft-preview.tsx — structured preview body: name +
  description + goal, triggers (humanised cron + tz + static-input
  keys), plan steps (step_id → action), and a collapsible raw JSON
  for power users.

Wiring:
- components/tool-ui/index.ts — re-export.
- features/chat-messages/timeline/tool-registry/registry.ts —
  register create_automation → CreateAutomationToolUI (dynamic import,
  same pattern as other connector tools).
- contracts/enums/toolIcons.tsx — Workflow icon + "Create automation"
  display name so fallback chrome (and timeline headers) are honest.

Shared util:
- lib/automations/describe-cron.ts — lifted from the route slice's
  lib/ folder since both the dashboard slice and the new approval card
  now render schedule descriptions. Slice imports updated; the now-
  empty slice lib/ folder is gone.

Backend prompt fragments:
- main_agent/system_prompt/.../create_automation/description.md and
  the tool's docstring no longer promise in-card edits. They make the
  refinement path explicit: if the user wants changes after seeing the
  draft, they reply in chat and the agent calls the tool again with a
  refined intent.

v1 deliberately excludes:
- In-card edit form / right-side edit panel — defer until we see real
  demand. The chat refinement loop covers the common case.
- approve_always / persistent allow rules — automations are a single
  artifact, not a repeated mutation, so the "trust this kind of call"
  affordance doesn't apply.
2026-05-28 01:32:04 +02:00

67 lines
2 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.

/**
* Minimal cron describer for the 5-field patterns the SurfSense drafter LLM
* actually produces (daily, weekdays, weekly, monthly, hourly). Falls back
* to the raw expression when unrecognized so the user still sees something
* honest instead of a guess.
*
* Lives under ``lib/automations/`` because both the dashboard slice and the
* chat ``create_automation`` approval card render schedule descriptions —
* keeping the helper outside either feature avoids a layering violation.
*/
const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
export function describeCron(cron: string): string {
const parts = cron.trim().split(/\s+/);
if (parts.length !== 5) return cron;
const [minute, hour, dom, month, dow] = parts;
// Daily at H:MM ("0 9 * * *")
if (month === "*" && dom === "*" && dow === "*" && /^\d+$/.test(minute) && /^\d+$/.test(hour)) {
return `Daily at ${formatTime(hour, minute)}`;
}
// Weekdays at H:MM ("0 9 * * 1-5")
if (month === "*" && dom === "*" && dow === "1-5" && /^\d+$/.test(minute) && /^\d+$/.test(hour)) {
return `MonFri at ${formatTime(hour, minute)}`;
}
// Specific weekday(s) ("0 9 * * 1" or "0 9 * * 1,3,5")
if (
month === "*" &&
dom === "*" &&
/^\d+$/.test(minute) &&
/^\d+$/.test(hour) &&
/^[\d,]+$/.test(dow)
) {
const days = dow
.split(",")
.map((d) => DAY_NAMES[Number(d) % 7])
.filter(Boolean)
.join(", ");
if (days) return `${days} at ${formatTime(hour, minute)}`;
}
// Monthly on day N ("0 9 1 * *")
if (
month === "*" &&
dow === "*" &&
/^\d+$/.test(dom) &&
/^\d+$/.test(hour) &&
/^\d+$/.test(minute)
) {
return `Day ${dom} of each month at ${formatTime(hour, minute)}`;
}
// Hourly ("0 * * * *")
if (month === "*" && dom === "*" && dow === "*" && hour === "*" && /^\d+$/.test(minute)) {
return minute === "0" ? "Every hour" : `Every hour at :${minute.padStart(2, "0")}`;
}
return cron;
}
function formatTime(hour: string, minute: string): string {
return `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`;
}