mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-09 19:27:45 +02:00
[eric] workflows+dashboard: card width matches chat, Cmd/Ctrl+A select-all, compact scheduling + consistent cancel/title
This commit is contained in:
@@ -91,6 +91,21 @@ export function useDashboardShortcuts({
|
||||
return () => window.removeEventListener('keydown', handleDelete);
|
||||
}, [selection, dispatch]);
|
||||
|
||||
// Cmd/Ctrl+A selects every card so it can be deleted in one go. Skipped
|
||||
// inside text fields so Cmd+A there still selects text, not cards.
|
||||
useEffect(() => {
|
||||
const handleSelectAll = (e: KeyboardEvent) => {
|
||||
if (!isActive) return;
|
||||
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'a') return;
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) return;
|
||||
e.preventDefault();
|
||||
selection.selectAll();
|
||||
};
|
||||
window.addEventListener('keydown', handleSelectAll);
|
||||
return () => window.removeEventListener('keydown', handleSelectAll);
|
||||
}, [selection, isActive]);
|
||||
|
||||
// Cmd+F to open card search palette
|
||||
useEffect(() => {
|
||||
const handleSearch = (e: KeyboardEvent) => {
|
||||
|
||||
@@ -70,6 +70,18 @@ export function useDashboardSelection(
|
||||
|
||||
const deselectAll = useCallback(() => setSelectedIds(new Map()), []);
|
||||
|
||||
// Cmd/Ctrl+A: select every card on the canvas so the user can wipe the
|
||||
// board in one keystroke. Mirrors the per-type id keys the marquee uses.
|
||||
const selectAll = useCallback(() => {
|
||||
const next = new Map<string, CardType>();
|
||||
for (const card of Object.values(cards)) next.set(card.session_id, 'agent');
|
||||
for (const vc of Object.values(viewCards)) next.set(vc.output_id, 'view');
|
||||
for (const bc of Object.values(browserCards)) next.set(bc.browser_id, 'browser');
|
||||
for (const n of Object.values(notes)) next.set(n.note_id, 'note');
|
||||
for (const wc of Object.values(workflowCards)) next.set(wc.workflow_id, 'workflow');
|
||||
setSelectedIds(next);
|
||||
}, [cards, viewCards, browserCards, notes, workflowCards]);
|
||||
|
||||
const selectCard = useCallback(
|
||||
(id: string, type: CardType, shiftKey: boolean) => {
|
||||
setSelectedIds((prev) => {
|
||||
@@ -285,6 +297,7 @@ export function useDashboardSelection(
|
||||
isSelected,
|
||||
selectCard,
|
||||
deselectAll,
|
||||
selectAll,
|
||||
handleCanvasMouseDown,
|
||||
handleCanvasMouseMove,
|
||||
handleCanvasMouseUp,
|
||||
|
||||
@@ -133,9 +133,10 @@ export default function SchedulingView({ workflow, steps }: Props) {
|
||||
}, [pending, dispatch, workflow.id, workflow.updated_at]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.25, minHeight: '100%' }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.25 }}>
|
||||
{/* Inline header replacement. Image #49: subtitle on LEFT, Cancel
|
||||
on RIGHT. The default History/Run row is hidden for 'scheduling'. */}
|
||||
on RIGHT. Cancel matches the subtitle's weight/size/color so the
|
||||
row reads as peers, not a heavy CTA; it just reddens on hover. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<InlineSubtitle workflow={workflow} />
|
||||
<Box sx={{ flex: 1 }} />
|
||||
@@ -143,13 +144,12 @@ export default function SchedulingView({ workflow, steps }: Props) {
|
||||
onClick={onCancel}
|
||||
role="button"
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.45,
|
||||
fontSize: '0.82rem', fontWeight: 600,
|
||||
color: c.text.secondary, cursor: 'pointer',
|
||||
px: 0.75, py: 0.5,
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.4,
|
||||
fontSize: '0.82rem', fontWeight: 500,
|
||||
color: c.text.muted, cursor: 'pointer',
|
||||
'&:hover': { color: c.status.error },
|
||||
}}>
|
||||
<DeleteOutlineRounded sx={{ fontSize: 16 }} />
|
||||
<DeleteOutlineRounded sx={{ fontSize: 15 }} />
|
||||
Cancel task scheduling
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -160,7 +160,6 @@ export default function SchedulingView({ workflow, steps }: Props) {
|
||||
{error && (
|
||||
<Typography sx={{ fontSize: '0.82rem', color: c.status.error }}>{error}</Typography>
|
||||
)}
|
||||
<Box sx={{ flex: 1 }} />
|
||||
{/* Real ChatInput (same one the toolbar / agent chat use) so the
|
||||
composer matches Image #54 / #64 exactly: live model picker,
|
||||
mode picker, thinking level, paperclip + mic, the works. We
|
||||
|
||||
@@ -463,14 +463,14 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
placeholder="New workflow"
|
||||
onChange={(e) => dispatch(updateWorkflowCard({ workflowId, patch: { draft: { ...(card?.draft || {}), title: e.target.value } } }))}
|
||||
sx={{
|
||||
flex: 1, fontWeight: 700, fontSize: '1rem', color: c.text.primary,
|
||||
flex: 1, fontWeight: 600, fontSize: '0.95rem', color: c.text.primary,
|
||||
letterSpacing: '-0.005em',
|
||||
'& input::placeholder': { color: c.text.muted, opacity: 1 },
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: '1rem', color: c.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: '-0.005em' }}>
|
||||
<Typography sx={{ fontWeight: 600, fontSize: '0.95rem', color: c.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: '-0.005em' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<StatusPill view={card.view} workflow={workflow} runs={runs} />
|
||||
|
||||
@@ -18,7 +18,7 @@ export const DEFAULT_VIEW_CARD_W = 1280;
|
||||
export const DEFAULT_VIEW_CARD_H = 800;
|
||||
export const DEFAULT_BROWSER_CARD_W = 1280;
|
||||
export const DEFAULT_BROWSER_CARD_H = 800;
|
||||
export const DEFAULT_WORKFLOW_CARD_W = 440;
|
||||
export const DEFAULT_WORKFLOW_CARD_W = 480;
|
||||
export const DEFAULT_WORKFLOW_CARD_H = 520;
|
||||
export const DEFAULT_WORKFLOWS_HUB_W = 1200;
|
||||
export const DEFAULT_WORKFLOWS_HUB_H = 640;
|
||||
|
||||
Reference in New Issue
Block a user