swarm-ui: a schedules page, one card per scheduled prompt
/schedules lists the controller's scheduled prompts as cards: name as the title, the agents it fires for, the message, its cadence and next fire. Edit turns a card into its own form; pause, resume and remove act in place, and + schedule opens an empty form. The form sends only what changed, so a rename never moves the fire times, and warns when the start or the repeat does. Filters reuse the shared card filter bar. Adds a TextAreaField to the ui kit, and an upcoming mode on RelativeTime so a next-fire time reads "in 5m". Needs the controller's /api/schedules routes.
This commit is contained in:
parent
7f597f5c43
commit
63ee6d12be
14 changed files with 931 additions and 3 deletions
|
|
@ -9,14 +9,24 @@
|
|||
// package's matrix-rain effect — and resyncs immediately on becoming
|
||||
// visible again instead of waiting out the rest of a stale interval.
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { fmtAgo } from "../../util.js";
|
||||
import { fmtAgo, fmtUntil } from "../../util.js";
|
||||
|
||||
// Cheap at the handful of rows/chips this renders for today, and coarse
|
||||
// enough to be correct without visibly ticking every frame — the same
|
||||
// choice a wall clock makes.
|
||||
const TICK_MS = 1000;
|
||||
|
||||
export function RelativeTime({ epochMs }: { epochMs: number }) {
|
||||
// `upcoming`: the instant is expected to be ahead (a schedule's next fire),
|
||||
// so one still ahead reads "in 5m" instead of clamping to "0s ago". Off by
|
||||
// default, so a caller showing an age never sees a skewed clock turn it
|
||||
// into a countdown.
|
||||
export function RelativeTime({
|
||||
epochMs,
|
||||
upcoming = false,
|
||||
}: {
|
||||
epochMs: number;
|
||||
upcoming?: boolean;
|
||||
}) {
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -43,5 +53,8 @@ export function RelativeTime({ epochMs }: { epochMs: number }) {
|
|||
};
|
||||
}, []);
|
||||
|
||||
return <>{fmtAgo((now - epochMs) / 1000)}</>;
|
||||
const ageSeconds = (now - epochMs) / 1000;
|
||||
return (
|
||||
<>{upcoming && ageSeconds < 0 ? fmtUntil(-ageSeconds) : fmtAgo(ageSeconds)}</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
/* `min-height` from `.ui-form-control` is a single-line floor; a textarea's
|
||||
own `rows` sets its height, so only the resize axis needs saying here. */
|
||||
.ui-text-area {
|
||||
resize: vertical;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// <TextAreaField> — `TextField`'s multi-line sibling, for free text that
|
||||
// runs past one line (a scheduled prompt's body is the motivating caller).
|
||||
// Same label chrome and `.ui-form-control` look as the rest of the kit;
|
||||
// the box only grows vertically, since a horizontal drag would break out
|
||||
// of whatever column the page gave the field.
|
||||
import { FormField } from "../form-field/FormField.js";
|
||||
import "./TextAreaField.css";
|
||||
|
||||
export function TextAreaField({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onInput,
|
||||
rows = 4,
|
||||
required,
|
||||
placeholder,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
onInput: (value: string) => void;
|
||||
rows?: number;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
return (
|
||||
<FormField label={label} htmlFor={id}>
|
||||
<textarea
|
||||
id={id}
|
||||
class="ui-form-control ui-text-area"
|
||||
value={value}
|
||||
rows={rows}
|
||||
required={required}
|
||||
placeholder={placeholder}
|
||||
onInput={(e) => onInput((e.target as HTMLTextAreaElement).value)}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue