[aidan] ui: sidebar, scheduling time

This commit is contained in:
abccodes
2026-06-21 02:36:50 -07:00
parent 4d94626e53
commit 087f531c9c
3 changed files with 91 additions and 30 deletions
@@ -8,7 +8,6 @@ import Snackbar from '@mui/material/Snackbar';
import Icon from '@mui/material/Icon';
import { styled } from '@mui/material/styles';
import AddRounded from '@mui/icons-material/AddRounded';
import CalendarMonthRounded from '@mui/icons-material/CalendarMonthRounded';
import ChatBubbleTeardrop from './ChatBubbleTeardrop';
@@ -770,7 +769,10 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
...popIn(3),
}}
>
<CalendarMonthRounded sx={{ fontSize: 22 }} />
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9">
<circle cx="6" cy="6" r="2.5" /><circle cx="6" cy="18" r="2.5" /><circle cx="18" cy="12" r="2.5" />
<path d="M8.2 7.1l7.6 3.8M8.2 16.9l7.6-3.8" />
</svg>
</Box>
</WarmTooltip>
@@ -0,0 +1,83 @@
import React, { useEffect, useRef, useState } from 'react';
import { WC } from './uiKit';
// Combobox for the run limit: pick a preset OR type any count. Self-rendered
// (no native <select>, no portal) because a native popup over the canvas card
// is a separate compositor layer and gets dismissed before you can click it.
const OPTIONS: Array<{ label: string; val: number | null }> = [
{ label: 'Forever', val: null },
{ label: 'Once', val: 1 },
{ label: '3 times', val: 3 },
{ label: '10 times', val: 10 },
];
const RepeatField: React.FC<{ value: number | null; onChange: (n: number | null) => void }> = ({ value, onChange }) => {
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState('');
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const onDoc = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); setEditing(false); }
};
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
const display = value == null ? '' : `${value} time${value === 1 ? '' : 's'}`;
const commit = (raw: string) => {
const digits = raw.replace(/[^0-9]/g, '').slice(0, 4);
setDraft(digits);
onChange(digits === '' ? null : Math.max(1, parseInt(digits, 10)));
};
const pick = (val: number | null) => {
onChange(val);
setDraft(val == null ? '' : String(val));
setEditing(false);
setOpen(false);
};
return (
<div ref={ref} style={{ position: 'relative', width: 134 }}>
<div style={{ display: 'flex', alignItems: 'center', background: '#FFFFFF', border: `1px solid ${open ? WC.accent : 'rgba(33,30,27,0.12)'}`, borderRadius: 8, height: 32, padding: '0 2px 0 9px' }}>
<input
value={editing ? draft : display}
placeholder="Forever"
inputMode="numeric"
onFocus={() => { setEditing(true); setDraft(value == null ? '' : String(value)); setOpen(true); }}
onChange={(e) => commit(e.target.value)}
style={{ flex: 1, minWidth: 0, border: 'none', outline: 'none', background: 'transparent', fontSize: 13, color: value == null && !editing ? WC.muted : WC.ink, fontFamily: "'JetBrains Mono',monospace" }}
/>
<button
onMouseDown={(e) => { e.preventDefault(); setOpen((o) => !o); }}
style={{ border: 'none', background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', padding: 5, color: WC.muted }}
aria-label="Repeat options"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 150ms ease-in-out' }}><path d="M6 9l6 6 6-6" /></svg>
</button>
</div>
{open && (
<div style={{ position: 'absolute', top: 37, left: 0, right: 0, background: '#FFFFFF', border: `1px solid ${WC.line}`, borderRadius: 9, boxShadow: '0 8px 22px rgba(33,30,27,0.16)', padding: 4, zIndex: 40 }}>
{OPTIONS.map((o) => {
const active = (o.val == null && value == null) || o.val === value;
return (
<div
key={o.label}
onMouseDown={(e) => { e.preventDefault(); pick(o.val); }}
style={{ padding: '7px 9px', borderRadius: 6, fontSize: 13, cursor: 'pointer', color: WC.ink, background: active ? WC.selBg : 'transparent', fontWeight: active ? 600 : 400 }}
>
{o.label}
</div>
);
})}
</div>
)}
</div>
);
};
export default RepeatField;
@@ -7,10 +7,10 @@ import {
freqOf, patchForFreq, intervalMinutes, timeInputValue, parseTimeInput, ordinal, nextRunText, type Freq,
} from './model';
import { useWorkflowPatch } from './useWorkflowPatch';
import RepeatField from './RepeatField';
const FREQS: Array<[Freq, string]> = [['daily', 'Daily'], ['weekly', 'Weekly'], ['monthly', 'Monthly'], ['interval', 'Interval']];
const DAY_LABELS: Array<[string, number]> = [['S', 0], ['M', 1], ['T', 2], ['W', 3], ['T', 4], ['F', 5], ['S', 6]];
const REPEAT_PRESETS = [1, 3, 10];
const ScheduleCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => {
const patch = useWorkflowPatch();
@@ -42,13 +42,8 @@ const ScheduleCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => {
const dom = sched.day_of_month ?? 1;
const maxRuns = sched.max_runs;
const repeatSel = maxRuns == null ? 'forever' : REPEAT_PRESETS.includes(maxRuns) ? String(maxRuns) : 'custom';
// Picking a finite limit resets the lifetime counter so "run 3 times" always means 3 from now.
const onRepeat = (val: string) => {
if (val === 'forever') return patchSched({ max_runs: null });
if (val === 'custom') return patchSched({ max_runs: maxRuns && !REPEAT_PRESETS.includes(maxRuns) ? maxRuns : 2, runs_count: 0 });
patchSched({ max_runs: parseInt(val, 10), runs_count: 0 });
};
const setMaxRuns = (n: number | null) => patchSched(n == null ? { max_runs: null } : { max_runs: n, runs_count: 0 });
return (
<div style={{ background: WC.paper, border: '1px solid rgba(33,30,27,0.08)', borderRadius: 13, padding: 16 }}>
@@ -92,7 +87,7 @@ const ScheduleCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => {
type="time"
value={timeInputValue(sched)}
onChange={(e) => { const t = parseTimeInput(e.target.value); if (t) patchSched(t); }}
style={{ background: '#FFFFFF', border: '1px solid rgba(33,30,27,0.12)', borderRadius: 8, padding: '6px 9px', fontSize: 13, fontFamily: "'JetBrains Mono',monospace", color: WC.ink }}
style={{ width: 134, boxSizing: 'border-box', height: 32, background: '#FFFFFF', border: '1px solid rgba(33,30,27,0.12)', borderRadius: 8, padding: '0 9px', fontSize: 13, fontFamily: "'JetBrains Mono',monospace", color: WC.ink }}
/>
</div>
) : (
@@ -124,26 +119,7 @@ const ScheduleCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => {
<div style={{ marginTop: 12, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
<span style={{ fontSize: 13, color: WC.ink3 }}>Repeat</span>
<div style={{ display: 'flex', gap: 6 }}>
<select
value={repeatSel}
onChange={(e) => onRepeat(e.target.value)}
style={{ background: '#FFFFFF', border: '1px solid rgba(33,30,27,0.12)', borderRadius: 8, padding: '6px 8px', fontSize: 13, color: WC.ink, cursor: 'pointer' }}
>
<option value="forever">Forever</option>
<option value="1">Once</option>
<option value="3">3 times</option>
<option value="10">10 times</option>
<option value="custom">Custom</option>
</select>
{repeatSel === 'custom' && (
<input
type="number" min={1} value={maxRuns ?? 1}
onChange={(e) => patchSched({ max_runs: Math.max(1, parseInt(e.target.value, 10) || 1), runs_count: 0 })}
style={{ width: 56, background: '#FFFFFF', border: '1px solid rgba(33,30,27,0.12)', borderRadius: 8, padding: '6px 9px', fontSize: 13, fontFamily: "'JetBrains Mono',monospace", color: WC.ink, textAlign: 'right' }}
/>
)}
</div>
<RepeatField value={maxRuns} onChange={setMaxRuns} />
</div>
<div style={{ marginTop: 13, paddingTop: 13, borderTop: `1px solid ${WC.line}`, display: 'flex', alignItems: 'center', gap: 8 }}>