From ce44b90eae63b6f66d9a8bfe67d3be7b513e8459 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 17 Jul 2026 00:54:13 -0400 Subject: [PATCH] refac --- .../automations/AutomationEditor.svelte | 529 +++++++---------- .../AutomationItemHeaderActions.svelte | 70 +++ .../AutomationListHeaderActions.svelte | 9 + src/lib/components/icons/Play.svelte | 20 + src/routes/(app)/automations/+layout.svelte | 131 +++++ src/routes/(app)/automations/+page.svelte | 533 ++++++++---------- .../(app)/automations/[id]/+page.svelte | 18 +- 7 files changed, 686 insertions(+), 624 deletions(-) create mode 100644 src/lib/components/automations/AutomationItemHeaderActions.svelte create mode 100644 src/lib/components/automations/AutomationListHeaderActions.svelte create mode 100644 src/lib/components/icons/Play.svelte create mode 100644 src/routes/(app)/automations/+layout.svelte diff --git a/src/lib/components/automations/AutomationEditor.svelte b/src/lib/components/automations/AutomationEditor.svelte index dd01662345..dc07676eee 100644 --- a/src/lib/components/automations/AutomationEditor.svelte +++ b/src/lib/components/automations/AutomationEditor.svelte @@ -4,73 +4,53 @@ import { goto } from '$app/navigation'; import dayjs from 'dayjs'; - import relativeTime from 'dayjs/plugin/relativeTime'; import localizedFormat from 'dayjs/plugin/localizedFormat'; + import type i18nType from '$lib/i18n'; - import { WEBUI_NAME, mobile, showSidebar } from '$lib/stores'; + import { WEBUI_NAME } from '$lib/stores'; import { - updateAutomationById, + getAutomationById, toggleAutomationById, runAutomationById, deleteAutomationById, getAutomationRuns, - type AutomationForm, type AutomationResponse, type AutomationRunModel } from '$lib/apis/automations'; + import AutomationModal from '$lib/components/AutomationModal.svelte'; + import AutomationItemHeaderActions from '$lib/components/automations/AutomationItemHeaderActions.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; - import Tooltip from '$lib/components/common/Tooltip.svelte'; import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; - import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; - import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte'; - import SidebarIcon from '$lib/components/icons/Sidebar.svelte'; + import ArrowRight from '$lib/components/icons/ArrowRight.svelte'; - import ScheduleDropdown from '$lib/components/automations/ScheduleDropdown.svelte'; - import ModelDropdown from '$lib/components/automations/ModelDropdown.svelte'; - - dayjs.extend(relativeTime); dayjs.extend(localizedFormat); - const i18n = getContext('i18n'); + const i18n: typeof i18nType = getContext('i18n'); + const automationsLayout: any = getContext('automationsLayout'); export let automation: AutomationResponse; - let name = ''; - let prompt = ''; - let model_id = ''; - let is_active = true; + let is_active = automation.is_active; let loading = false; - let saving = false; let showDeleteConfirm = false; + let showEditModal = false; let runs: AutomationRunModel[] = []; let runsLoading = false; let hasMoreRuns = true; let runsPage = 0; - let isDirty = false; - let scheduleDropdown: ScheduleDropdown; - - const formatRunTime = (ts: number): string => { - const now = Date.now(); - const diff = now - ts / 1_000_000; - - const seconds = Math.floor(diff / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - const weeks = Math.floor(days / 7); - const years = Math.floor(days / 365); - - if (years > 0) return $i18n.t('{{COUNT}}y', { COUNT: years, context: 'time_ago' }); - if (weeks > 0) return $i18n.t('{{COUNT}}w', { COUNT: weeks, context: 'time_ago' }); - if (days > 0) return $i18n.t('{{COUNT}}d', { COUNT: days, context: 'time_ago' }); - if (hours > 0) return $i18n.t('{{COUNT}}h', { COUNT: hours, context: 'time_ago' }); - if (minutes > 0) return $i18n.t('{{COUNT}}m', { COUNT: minutes, context: 'time_ago' }); - return $i18n.t('1m', { context: 'time_ago' }); + const formatTime = (ts: number | null): string => { + if (!ts) return '-'; + return new Date(ts / 1_000_000).toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); }; const formatNextRun = (ts: number | null): string => { @@ -80,36 +60,57 @@ return d.format('L LT'); }; - const saveHandler = async () => { - if (!name.trim() || !prompt.trim() || !model_id.trim()) { - toast.error($i18n.t('Name, prompt, and model are required')); - return; - } - saving = true; - try { - const form: AutomationForm = { - name: name.trim(), - data: { - prompt: prompt.trim(), - model_id: model_id.trim(), - rrule: scheduleDropdown.buildRrule() - }, - is_active - }; - const updated = await updateAutomationById(localStorage.token, automation.id, form); - if (updated) { - automation = updated; - isDirty = false; - toast.success($i18n.t('Automation updated')); + const formatSchedule = (rrule: string): string => { + if (rrule.includes('COUNT=1')) { + const match = rrule.match(/DTSTART:(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})/); + if (match) { + const d = new Date(`${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}`); + return `${$i18n.t('Once')} · ${d.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric' + })} ${d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}`; } - } catch (e: any) { - toast.error(e?.detail ?? `${e}` ?? 'Failed to save'); - } finally { - saving = false; + return $i18n.t('Once'); } + + const parts: Record = {}; + rrule + .replace('RRULE:', '') + .split(';') + .forEach((part) => { + const [key, value] = part.split('='); + if (key && value) parts[key] = value; + }); + + const freq = parts.FREQ || ''; + const hour = parseInt(parts.BYHOUR || '0'); + const minute = (parts.BYMINUTE || '0').padStart(2, '0'); + const interval = parseInt(parts.INTERVAL || '1'); + const ampm = hour >= 12 ? 'PM' : 'AM'; + const hour12 = hour % 12 || 12; + const time = `${hour12}:${minute} ${ampm}`; + + if (freq === 'MINUTELY') + return interval === 1 + ? $i18n.t('Every minute') + : $i18n.t('Every {{count}} minutes', { count: interval }); + if (freq === 'HOURLY') + return interval === 1 + ? $i18n.t('Hourly') + : $i18n.t('Every {{count}} hours', { count: interval }); + if (freq === 'DAILY') return `${$i18n.t('Daily at')} ${time}`; + if (freq === 'WEEKLY') + return parts.BYDAY + ? `${parts.BYDAY} ${$i18n.t('at')} ${time}` + : `${$i18n.t('Weekly at')} ${time}`; + if (freq === 'MONTHLY') + return `${$i18n.t('Monthly')} ${parts.BYMONTHDAY ?? '1'} ${$i18n.t('at')} ${time}`; + + return rrule; }; const toggleHandler = async () => { + const previousState = automation.is_active; const res = await toggleAutomationById(localStorage.token, automation.id).catch((err) => { toast.error(`${err}`); return null; @@ -117,6 +118,8 @@ if (res) { is_active = res.is_active; automation = res; + } else { + is_active = previousState; } }; @@ -133,6 +136,19 @@ loading = false; }; + const editSavedHandler = async () => { + const updated = await getAutomationById(localStorage.token, automation.id).catch((err) => { + toast.error(`${err}`); + return null; + }); + + if (updated) { + automation = updated; + is_active = updated.is_active; + await loadRuns(); + } + }; + const deleteHandler = async () => { const res = await deleteAutomationById(localStorage.token, automation.id).catch((err) => { toast.error(`${err}`); @@ -173,10 +189,6 @@ runsLoading = false; }; - const markDirty = () => { - isDirty = true; - }; - const onScroll = (e: Event) => { const target = e.target as HTMLElement; if (target.scrollTop + target.clientHeight >= target.scrollHeight - 50) { @@ -187,21 +199,29 @@ }; onMount(async () => { - name = automation.name; - prompt = automation.data.prompt; - model_id = automation.data.model_id; is_active = automation.is_active; - if (scheduleDropdown) { - scheduleDropdown.parseRrule(automation.data.rrule); - } - await loadRuns(); }); + + $: if (automation) { + automationsLayout?.setHeader({ + itemName: automation.name, + actions: AutomationItemHeaderActions, + actionProps: { + isActive: is_active, + loading, + toggleHandler, + runNowHandler, + editHandler: () => (showEditModal = true), + deleteHandler: () => (showDeleteConfirm = true) + } + }); + } - {name || $i18n.t('Automation')} • {$WEBUI_NAME} + {automation.name || $i18n.t('Automation')} • {$WEBUI_NAME} -
-
- -
-
-
- {#if $mobile} - - - - {/if} - - - - -
-
+ -
- - - - - {#if isDirty} - - {/if} - - -
+
+
+
+ + {$i18n.t('Status')} + + + + {is_active ? $i18n.t('Active') : $i18n.t('Paused')} +
- -
- -
-
-
{$i18n.t('Instructions')}
-
-
-
-