mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 20:27:44 +02:00
[aidan] ui: sidebar, scheduling time
This commit is contained in:
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user