From acaf9ab50cbffb0b359e60d90d4b9ea914629dbf Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 1 Apr 2026 03:36:55 -0500 Subject: [PATCH] refac --- src/lib/components/AutomationModal.svelte | 521 +----------------- .../automations/AutomationEditor.svelte | 503 +---------------- .../automations/ModelDropdown.svelte | 124 +++++ .../automations/ScheduleDropdown.svelte | 304 ++++++++++ .../automations/TerminalDropdown.svelte | 120 ++++ 5 files changed, 605 insertions(+), 967 deletions(-) create mode 100644 src/lib/components/automations/ModelDropdown.svelte create mode 100644 src/lib/components/automations/ScheduleDropdown.svelte create mode 100644 src/lib/components/automations/TerminalDropdown.svelte diff --git a/src/lib/components/AutomationModal.svelte b/src/lib/components/AutomationModal.svelte index 35991c4dc8..f829743d77 100644 --- a/src/lib/components/AutomationModal.svelte +++ b/src/lib/components/AutomationModal.svelte @@ -2,15 +2,13 @@ import { createEventDispatcher, getContext } from 'svelte'; import { toast } from 'svelte-sonner'; - import { models } from '$lib/stores'; import Modal from '$lib/components/common/Modal.svelte'; - import Dropdown from '$lib/components/common/Dropdown.svelte'; import XMark from '$lib/components/icons/XMark.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; - import Search from '$lib/components/icons/Search.svelte'; - import Cloud from '$lib/components/icons/Cloud.svelte'; - import { WEBUI_API_BASE_URL } from '$lib/constants'; + import ScheduleDropdown from '$lib/components/automations/ScheduleDropdown.svelte'; + import ModelDropdown from '$lib/components/automations/ModelDropdown.svelte'; + import TerminalDropdown from '$lib/components/automations/TerminalDropdown.svelte'; import { createAutomation, @@ -31,172 +29,23 @@ let model_id = ''; let is_active = true; - let frequency = 'DAILY'; - let interval = 1; - let hour = 9; - let minute = 0; - let selectedDays: string[] = []; - let monthDay = 1; - let onceDate = ''; - let onceTime = '09:00'; - let loading = false; - let showScheduleDropdown = false; - let showModelDropdown = false; - let showTerminalDropdown = false; - let modelSearch = ''; - let customRrule = ''; // Terminal state let terminalServers: TerminalServer[] = []; let terminalServerId = ''; let terminalCwd = ''; - $: terminalLabel = terminalServerId - ? terminalServers.find((s) => s.id === terminalServerId)?.name || 'Terminal' - : $i18n.t('Terminal'); - - $: modelLabel = model_id - ? $models.find((m) => m.id === model_id)?.name || model_id - : $i18n.t('Select model'); - - $: filteredModels = modelSearch - ? $models.filter( - (m) => - m.name.toLowerCase().includes(modelSearch.toLowerCase()) || - m.id.toLowerCase().includes(modelSearch.toLowerCase()) - ) - : $models; - - const FREQUENCIES = [ - { key: 'ONCE', label: 'Once' }, - { key: 'HOURLY', label: 'Hourly' }, - { key: 'DAILY', label: 'Daily' }, - { key: 'WEEKLY', label: 'Weekly' }, - { key: 'MONTHLY', label: 'Monthly' }, - { key: 'CUSTOM', label: 'Custom' } - ]; - - const DAYS = [ - { key: 'MO', label: 'Mo' }, - { key: 'TU', label: 'Tu' }, - { key: 'WE', label: 'We' }, - { key: 'TH', label: 'Th' }, - { key: 'FR', label: 'Fr' }, - { key: 'SA', label: 'Sa' }, - { key: 'SU', label: 'Su' } - ]; - - const buildVisualRrule = (): string => { - if (lastVisualFrequency === 'ONCE') { - const dt = onceDate.replace(/-/g, '') + 'T' + onceTime.replace(/:/g, '') + '00'; - return `DTSTART:${dt}\nRRULE:FREQ=DAILY;COUNT=1`; - } - let parts = [`FREQ=${lastVisualFrequency}`]; - if (interval > 1) parts.push(`INTERVAL=${interval}`); - if (lastVisualFrequency === 'WEEKLY' && selectedDays.length) { - parts.push(`BYDAY=${selectedDays.join(',')}`); - } - if (lastVisualFrequency === 'MONTHLY') { - parts.push(`BYMONTHDAY=${monthDay}`); - } - if (['DAILY', 'WEEKLY', 'MONTHLY'].includes(lastVisualFrequency)) { - parts.push(`BYHOUR=${hour}`); - } - parts.push(`BYMINUTE=${minute}`); - return `RRULE:${parts.join(';')}`; - }; - - let lastVisualFrequency = 'DAILY'; - let prevFrequency = 'DAILY'; - - $: if (frequency !== 'CUSTOM') { - lastVisualFrequency = frequency; - } - - $: if (frequency === 'ONCE' && !onceDate) { - const soon = new Date(Date.now() + 5 * 60_000); - onceDate = soon.toISOString().split('T')[0]; - onceTime = `${String(soon.getHours()).padStart(2, '0')}:${String(soon.getMinutes()).padStart(2, '0')}`; - } - - $: { - if (frequency === 'CUSTOM' && prevFrequency !== 'CUSTOM') { - customRrule = buildVisualRrule(); - } - prevFrequency = frequency; - } - - const buildRrule = (): string => { - if (frequency === 'CUSTOM') return customRrule; - if (frequency === 'ONCE') { - const dt = onceDate.replace(/-/g, '') + 'T' + onceTime.replace(/:/g, '') + '00'; - return `DTSTART:${dt}\nRRULE:FREQ=DAILY;COUNT=1`; - } - let parts = [`FREQ=${frequency}`]; - if (interval > 1) parts.push(`INTERVAL=${interval}`); - if (frequency === 'WEEKLY' && selectedDays.length) { - parts.push(`BYDAY=${selectedDays.join(',')}`); - } - if (frequency === 'MONTHLY') { - parts.push(`BYMONTHDAY=${monthDay}`); - } - if (['DAILY', 'WEEKLY', 'MONTHLY'].includes(frequency)) { - parts.push(`BYHOUR=${hour}`); - } - parts.push(`BYMINUTE=${minute}`); - return `RRULE:${parts.join(';')}`; - }; - - const parseRrule = (s: string) => { - // Detect ONCE (COUNT=1 with DTSTART) - if (s.includes('COUNT=1')) { - frequency = 'ONCE'; - const match = s.match(/DTSTART:(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})/); - if (match) { - onceDate = `${match[1]}-${match[2]}-${match[3]}`; - onceTime = `${match[4]}:${match[5]}`; - } - return; - } - const parts: Record = {}; - s.replace('RRULE:', '') - .split(';') - .forEach((p) => { - const [k, v] = p.split('='); - if (k && v) parts[k] = v; - }); - const freq = parts.FREQ || 'DAILY'; - if (!['HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY'].includes(freq)) { - frequency = 'CUSTOM'; - customRrule = s; - return; - } - frequency = freq; - interval = parseInt(parts.INTERVAL || '1'); - hour = parseInt(parts.BYHOUR || '9'); - minute = parseInt(parts.BYMINUTE || '0'); - selectedDays = parts.BYDAY ? parts.BYDAY.split(',') : []; - monthDay = parseInt(parts.BYMONTHDAY || '1'); - }; - - const scheduleLabel = (freq, intv, h, min, days, mDay) => { - if (freq === 'ONCE') return 'Once'; - if (freq === 'HOURLY') return 'Hourly'; - if (freq === 'DAILY') return 'Daily'; - if (freq === 'WEEKLY') return 'Weekly'; - if (freq === 'MONTHLY') return 'Monthly'; - if (freq === 'CUSTOM') return 'Custom'; - return 'Schedule'; - }; + // Schedule dropdown ref + let scheduleDropdown: ScheduleDropdown; const submitHandler = async () => { if (!name.trim() || !prompt.trim() || !model_id.trim()) { toast.error($i18n.t('Name, prompt, and model are required')); return; } - if (frequency === 'ONCE') { - const scheduled = new Date(`${onceDate}T${onceTime}`); + if (scheduleDropdown?.frequency === 'ONCE') { + const scheduled = new Date(`${scheduleDropdown.onceDate}T${scheduleDropdown.onceTime}`); if (scheduled <= new Date()) { toast.error($i18n.t('Scheduled time must be in the future')); return; @@ -209,7 +58,7 @@ data: { prompt: prompt.trim(), model_id: model_id.trim(), - rrule: buildRrule(), + rrule: scheduleDropdown.buildRrule(), ...(terminalServerId ? { terminal: { @@ -253,27 +102,19 @@ prompt = automation.data.prompt; model_id = automation.data.model_id; is_active = automation.is_active; - parseRrule(automation.data.rrule); terminalServerId = automation.data.terminal?.server_id || ''; terminalCwd = automation.data.terminal?.cwd || ''; + if (scheduleDropdown) { + scheduleDropdown.parseRrule(automation.data.rrule); + } } else { name = ''; prompt = ''; model_id = ''; is_active = true; - frequency = 'DAILY'; - interval = 1; - onceDate = ''; - onceTime = '09:00'; - hour = 9; - minute = 0; - selectedDays = []; - monthDay = 1; terminalServerId = ''; terminalCwd = ''; } - showScheduleDropdown = false; - showTerminalDropdown = false; }; $: if (show) { @@ -314,331 +155,25 @@
- - - + -
-
- {$i18n.t('Schedule')} -
+ -
- -
- - {#if frequency === 'CUSTOM'} -
- e.stopPropagation()} - /> -
- {:else if frequency !== 'HOURLY'} -
- {#if frequency === 'ONCE'} -
- e.stopPropagation()} - /> -
-
- e.stopPropagation()} - /> -
- {:else} -
- {$i18n.t('Time')} - { - const [h, m] = e.currentTarget.value.split(':').map(Number); - hour = h; - minute = m; - }} - class="bg-transparent text-center outline-hidden text-xs dark:color-scheme-dark" - on:click={(e) => e.stopPropagation()} - /> -
- {/if} - - {#if frequency === 'MONTHLY'} -
- {$i18n.t('Day')} - e.stopPropagation()} - /> -
- {/if} -
- - {#if frequency === 'WEEKLY'} -
- {#each DAYS as d} - - {/each} -
- {/if} - {/if} -
-
- - - - - -
-
- - e.stopPropagation()} - /> -
- -
-
- {$i18n.t('Models')} -
- - {#each filteredModels as model (model.id)} - - {:else} -
- {$i18n.t('No results found')} -
- {/each} -
-
-
- - - {#if terminalServers.length > 0} - - - -
-
- {$i18n.t('Terminal')} -
- - {#each terminalServers as server (server.id)} - - {/each} - - {#if terminalServerId} -
-
- {$i18n.t('Working Directory')} -
-
- e.stopPropagation()} - /> -
-
- {/if} -
-
- {/if} +
diff --git a/src/lib/components/automations/AutomationEditor.svelte b/src/lib/components/automations/AutomationEditor.svelte index 62e0ce3be1..e0af62fdd7 100644 --- a/src/lib/components/automations/AutomationEditor.svelte +++ b/src/lib/components/automations/AutomationEditor.svelte @@ -7,8 +7,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import localizedFormat from 'dayjs/plugin/localizedFormat'; - import { models, WEBUI_NAME, showSidebar } from '$lib/stores'; - import { WEBUI_API_BASE_URL } from '$lib/constants'; + import { WEBUI_NAME, showSidebar } from '$lib/stores'; import { updateAutomationById, @@ -22,15 +21,16 @@ } from '$lib/apis/automations'; import { getTerminalServers, type TerminalServer } from '$lib/apis/terminal/index'; - import Dropdown from '$lib/components/common/Dropdown.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 Search from '$lib/components/icons/Search.svelte'; - import Cloud from '$lib/components/icons/Cloud.svelte'; import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte'; + import ScheduleDropdown from '$lib/components/automations/ScheduleDropdown.svelte'; + import ModelDropdown from '$lib/components/automations/ModelDropdown.svelte'; + import TerminalDropdown from '$lib/components/automations/TerminalDropdown.svelte'; + dayjs.extend(relativeTime); dayjs.extend(localizedFormat); @@ -43,16 +43,6 @@ let model_id = ''; let is_active = true; - let frequency = 'DAILY'; - let interval = 1; - let hour = 9; - let minute = 0; - let selectedDays: string[] = []; - let monthDay = 1; - let onceDate = ''; - let onceTime = '09:00'; - let customRrule = ''; - let terminalServers: TerminalServer[] = []; let terminalServerId = ''; let terminalCwd = ''; @@ -60,145 +50,12 @@ let loading = false; let saving = false; let showDeleteConfirm = false; - let showScheduleDropdown = false; - let showModelDropdown = false; - let showTerminalDropdown = false; - let modelSearch = ''; let runs: AutomationRunModel[] = []; let runsLoading = false; let isDirty = false; - $: terminalLabel = terminalServerId - ? terminalServers.find((s) => s.id === terminalServerId)?.name || 'Terminal' - : $i18n.t('None'); - - $: modelLabel = model_id - ? $models.find((m) => m.id === model_id)?.name || model_id - : $i18n.t('Select model'); - - $: filteredModels = modelSearch - ? $models.filter( - (m) => - m.name.toLowerCase().includes(modelSearch.toLowerCase()) || - m.id.toLowerCase().includes(modelSearch.toLowerCase()) - ) - : $models; - - const FREQUENCIES = [ - { key: 'ONCE', label: 'Once' }, - { key: 'HOURLY', label: 'Hourly' }, - { key: 'DAILY', label: 'Daily' }, - { key: 'WEEKLY', label: 'Weekly' }, - { key: 'MONTHLY', label: 'Monthly' }, - { key: 'CUSTOM', label: 'Custom' } - ]; - - const DAYS = [ - { key: 'MO', label: 'Mo' }, - { key: 'TU', label: 'Tu' }, - { key: 'WE', label: 'We' }, - { key: 'TH', label: 'Th' }, - { key: 'FR', label: 'Fr' }, - { key: 'SA', label: 'Sa' }, - { key: 'SU', label: 'Su' } - ]; - - let lastVisualFrequency = 'DAILY'; - let prevFrequency = 'DAILY'; - - $: if (frequency !== 'CUSTOM') { - lastVisualFrequency = frequency; - } - - $: if (frequency === 'ONCE' && !onceDate) { - const soon = new Date(Date.now() + 5 * 60_000); - onceDate = soon.toISOString().split('T')[0]; - onceTime = `${String(soon.getHours()).padStart(2, '0')}:${String(soon.getMinutes()).padStart(2, '0')}`; - } - - $: { - if (frequency === 'CUSTOM' && prevFrequency !== 'CUSTOM') { - customRrule = buildVisualRrule(); - } - prevFrequency = frequency; - } - - const buildVisualRrule = (): string => { - if (lastVisualFrequency === 'ONCE') { - const dt = onceDate.replace(/-/g, '') + 'T' + onceTime.replace(/:/g, '') + '00'; - return `DTSTART:${dt}\nRRULE:FREQ=DAILY;COUNT=1`; - } - let parts = [`FREQ=${lastVisualFrequency}`]; - if (interval > 1) parts.push(`INTERVAL=${interval}`); - if (lastVisualFrequency === 'WEEKLY' && selectedDays.length) - parts.push(`BYDAY=${selectedDays.join(',')}`); - if (lastVisualFrequency === 'MONTHLY') parts.push(`BYMONTHDAY=${monthDay}`); - if (['DAILY', 'WEEKLY', 'MONTHLY'].includes(lastVisualFrequency)) parts.push(`BYHOUR=${hour}`); - parts.push(`BYMINUTE=${minute}`); - return `RRULE:${parts.join(';')}`; - }; - - const buildRrule = (): string => { - if (frequency === 'CUSTOM') return customRrule; - if (frequency === 'ONCE') { - const dt = onceDate.replace(/-/g, '') + 'T' + onceTime.replace(/:/g, '') + '00'; - return `DTSTART:${dt}\nRRULE:FREQ=DAILY;COUNT=1`; - } - let parts = [`FREQ=${frequency}`]; - if (interval > 1) parts.push(`INTERVAL=${interval}`); - if (frequency === 'WEEKLY' && selectedDays.length) - parts.push(`BYDAY=${selectedDays.join(',')}`); - if (frequency === 'MONTHLY') parts.push(`BYMONTHDAY=${monthDay}`); - if (['DAILY', 'WEEKLY', 'MONTHLY'].includes(frequency)) parts.push(`BYHOUR=${hour}`); - parts.push(`BYMINUTE=${minute}`); - return `RRULE:${parts.join(';')}`; - }; - - const parseRrule = (s: string) => { - if (s.includes('COUNT=1')) { - frequency = 'ONCE'; - const match = s.match(/DTSTART:(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})/); - if (match) { - onceDate = `${match[1]}-${match[2]}-${match[3]}`; - onceTime = `${match[4]}:${match[5]}`; - } - return; - } - const parts: Record = {}; - s.replace('RRULE:', '') - .split(';') - .forEach((p) => { - const [k, v] = p.split('='); - if (k && v) parts[k] = v; - }); - const freq = parts.FREQ || 'DAILY'; - if (!['HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY'].includes(freq)) { - frequency = 'CUSTOM'; - customRrule = s; - return; - } - frequency = freq; - interval = parseInt(parts.INTERVAL || '1'); - hour = parseInt(parts.BYHOUR || '9'); - minute = parseInt(parts.BYMINUTE || '0'); - selectedDays = parts.BYDAY ? parts.BYDAY.split(',') : []; - monthDay = parseInt(parts.BYMONTHDAY || '1'); - }; - - $: scheduleLabel = (() => { - if (frequency === 'ONCE') return 'Once'; - if (frequency === 'HOURLY') return interval > 1 ? `Every ${interval}h` : 'Hourly'; - if (frequency === 'DAILY') { - const ampm = hour >= 12 ? 'PM' : 'AM'; - const h12 = hour % 12 || 12; - return `Daily at ${h12}:${String(minute).padStart(2, '0')} ${ampm}`; - } - if (frequency === 'WEEKLY') return 'Weekly'; - if (frequency === 'MONTHLY') return 'Monthly'; - if (frequency === 'CUSTOM') return 'Custom'; - return 'Schedule'; - })(); + let scheduleDropdown: ScheduleDropdown; const formatRunTime = (ts: number): string => { const now = Date.now(); @@ -238,7 +95,7 @@ data: { prompt: prompt.trim(), model_id: model_id.trim(), - rrule: buildRrule(), + rrule: scheduleDropdown.buildRrule(), ...(terminalServerId ? { terminal: { @@ -317,10 +174,13 @@ prompt = automation.data.prompt; model_id = automation.data.model_id; is_active = automation.is_active; - parseRrule(automation.data.rrule); terminalServerId = automation.data.terminal?.server_id || ''; terminalCwd = automation.data.terminal?.cwd || ''; + if (scheduleDropdown) { + scheduleDropdown.parseRrule(automation.data.rrule); + } + try { terminalServers = await getTerminalServers(localStorage.token); } catch { @@ -447,342 +307,36 @@