mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 13:17:40 +02:00
[eric] workflows: ignore saves card, scheduling chat typeable + default model, accent color, drop step box
This commit is contained in:
@@ -190,7 +190,7 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
);
|
||||
}, [outputList, viewSearch]);
|
||||
|
||||
const shortcutLabel = shortcut
|
||||
const shortcutLabel = (shortcut || '')
|
||||
.split('+')
|
||||
.map((p) => {
|
||||
if (p === 'Meta') return '⌘';
|
||||
|
||||
@@ -72,7 +72,16 @@ export function useDashboardInteractions({
|
||||
setTimeout(() => {
|
||||
const rect = getCardRect(id, type);
|
||||
if (rect) canvas.actions.fitToCards([rect], 1.15, true, type === 'browser' ? 0.8 : undefined);
|
||||
setTimeout(() => (document.activeElement as HTMLElement)?.blur?.(), 150);
|
||||
setTimeout(() => {
|
||||
// Don't blur an input/textarea/contentEditable the user is typing in
|
||||
// (e.g. a workflow card's embedded chat); the click that selected the
|
||||
// card also focused the field, and blurring it kills the cursor.
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (!active) return;
|
||||
const tag = active.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || active.isContentEditable) return;
|
||||
active.blur?.();
|
||||
}, 150);
|
||||
}, 100);
|
||||
}, [selection, getCardRect, canvas.actions, dispatch, expandedSessionIds]);
|
||||
|
||||
@@ -152,7 +161,16 @@ export function useDashboardInteractions({
|
||||
setTimeout(() => {
|
||||
const rect = getCardRect(id, type);
|
||||
if (rect) canvas.actions.fitToCards([rect], 1.15, true);
|
||||
setTimeout(() => (document.activeElement as HTMLElement)?.blur?.(), 150);
|
||||
setTimeout(() => {
|
||||
// Don't blur an input/textarea/contentEditable the user is typing in
|
||||
// (e.g. a workflow card's embedded chat); the click that selected the
|
||||
// card also focused the field, and blurring it kills the cursor.
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (!active) return;
|
||||
const tag = active.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || active.isContentEditable) return;
|
||||
active.blur?.();
|
||||
}, 150);
|
||||
}, 100);
|
||||
}, [getCardRect, canvas.actions, dispatch]);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// ScheduleConfig in a confirmation modal (the "always ask permission"
|
||||
// stand-in for the schedule_workflow tool call), and PATCH on confirm.
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
@@ -63,6 +63,23 @@ export default function SchedulingView({ workflow, steps }: Props) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pending, setPending] = useState<ScheduleConfig | null>(null);
|
||||
|
||||
// The composer behaves like any normal chat: it defaults to the user's
|
||||
// configured default model/mode (e.g. their subscription model), not the
|
||||
// workflow's stored run model, and its pickers actually work.
|
||||
const defaultModel = _useAppSelector((s) => s.settings.data.default_model);
|
||||
const defaultMode = _useAppSelector((s) => s.settings.data.default_mode);
|
||||
const settingsLoaded = _useAppSelector((s) => s.settings.loaded);
|
||||
const [chatModel, setChatModel] = useState(defaultModel || 'sonnet');
|
||||
const [chatMode, setChatMode] = useState(defaultMode || 'agent');
|
||||
const settingsApplied = useRef(false);
|
||||
useEffect(() => {
|
||||
if (settingsLoaded && !settingsApplied.current) {
|
||||
setChatModel(defaultModel || 'sonnet');
|
||||
setChatMode(defaultMode || 'agent');
|
||||
settingsApplied.current = true;
|
||||
}
|
||||
}, [settingsLoaded, defaultModel, defaultMode]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'saved' } }));
|
||||
}, [dispatch, workflow.id]);
|
||||
@@ -136,21 +153,10 @@ export default function SchedulingView({ workflow, steps }: Props) {
|
||||
Cancel task scheduling
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
p: 1.5, borderRadius: `${c.radius.lg}px`,
|
||||
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.elevated,
|
||||
}}>
|
||||
<StepList steps={steps} />
|
||||
</Box>
|
||||
<Box sx={{
|
||||
p: 1.5, borderRadius: `${c.radius.lg}px`,
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '0.92rem', color: c.text.primary, lineHeight: 1.45 }}>
|
||||
When should this workflow run (e.g. every Wednesday at 1pm)
|
||||
</Typography>
|
||||
</Box>
|
||||
<StepList steps={steps} />
|
||||
<Typography sx={{ fontSize: '0.92rem', color: c.text.secondary, lineHeight: 1.45, mt: 0.5 }}>
|
||||
When should this workflow run (e.g. every Wednesday at 1pm)
|
||||
</Typography>
|
||||
{error && (
|
||||
<Typography sx={{ fontSize: '0.82rem', color: c.status.error }}>{error}</Typography>
|
||||
)}
|
||||
@@ -164,10 +170,10 @@ export default function SchedulingView({ workflow, steps }: Props) {
|
||||
<Box sx={{ mx: -0.5 }}>
|
||||
<ChatInput
|
||||
onSend={(msg) => { void onSubmit(msg); }}
|
||||
mode={workflow.mode || 'agent'}
|
||||
onModeChange={() => { /* schedule chat doesn't persist mode */ }}
|
||||
model={workflow.model || 'sonnet'}
|
||||
onModelChange={() => { /* schedule chat doesn't persist model */ }}
|
||||
mode={chatMode}
|
||||
onModeChange={setChatMode}
|
||||
model={chatModel}
|
||||
onModelChange={setChatModel}
|
||||
embedded
|
||||
autoFocus
|
||||
sessionId={`schedule-${workflow.id}`}
|
||||
|
||||
@@ -10,7 +10,6 @@ import EditOutlined from '@mui/icons-material/EditOutlined';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
closeWorkflowCard,
|
||||
createWorkflow,
|
||||
toggleExpandedStep,
|
||||
updateWorkflow,
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
type Workflow,
|
||||
type WorkflowRun,
|
||||
} from '@/shared/state/workflowsSlice';
|
||||
import { removeWorkflowCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { CostChip, humanDuration, routingFor, StreakBadge } from './workflowVisuals';
|
||||
import StepList from './StepList';
|
||||
|
||||
@@ -114,6 +112,12 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft,
|
||||
const liveDraft = (card?.draft ?? initialDraft ?? {}) as Partial<Workflow>;
|
||||
const title = (liveDraft.title as string) || 'New workflow';
|
||||
const description = (liveDraft.description as string) || '';
|
||||
// The new workflow runs with the user's configured default model/mode (their
|
||||
// subscription, etc.), falling back to whatever the source chat used. Without
|
||||
// this the backend picks its own default, which surprised users who'd set a
|
||||
// subscription default but saw the workflow created on an API-key model.
|
||||
const defaultModel = useAppSelector((s) => s.settings.data.default_model);
|
||||
const defaultMode = useAppSelector((s) => s.settings.data.default_mode);
|
||||
// Steps render compact (label + chevron, capped + "... N more"), same as
|
||||
// the saved card. The raw prompt drills down on click. Keeping them short
|
||||
// is what leaves room for the schedule prompt + buttons to stay on-card.
|
||||
@@ -122,58 +126,64 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft,
|
||||
dispatch(toggleExpandedStep({ workflowId, stepId }));
|
||||
}, [dispatch, workflowId]);
|
||||
|
||||
const onDiscard = useCallback(() => {
|
||||
dispatch(closeWorkflowCard(workflowId));
|
||||
dispatch(removeWorkflowCard(workflowId));
|
||||
}, [dispatch, workflowId]);
|
||||
|
||||
const onChangeDescription = useCallback((value: string) => {
|
||||
dispatch(updateWorkflowCard({ workflowId, patch: { draft: { ...liveDraft, description: value } } }));
|
||||
}, [dispatch, workflowId, liveDraft]);
|
||||
|
||||
// The Save flow auto-creates the workflow, then prompts the user to schedule it
|
||||
// (Image #7). Ignore = save without schedule. Schedule = open the scheduling
|
||||
// composer (slice 3 wires this to the natural-language input).
|
||||
// Both buttons persist the workflow; the only difference is where they land.
|
||||
// Ignore = save and show the saved card. Schedule = save then open the
|
||||
// natural-language scheduling composer. (Ignore used to delete the card,
|
||||
// which surprised people; the schedule prompt is optional, the workflow isn't.)
|
||||
const saveWorkflow = useCallback(async (): Promise<Workflow | null> => {
|
||||
const result = await dispatch(createWorkflow({
|
||||
title,
|
||||
description,
|
||||
steps: steps.map((s) => ({ id: s.id, text: s.text })),
|
||||
source_session_id: sourceSessionId,
|
||||
use_synced_prompt: true,
|
||||
// The user's configured default wins over whatever model the source chat
|
||||
// happened to run on, so a converted workflow behaves like a fresh chat.
|
||||
model: defaultModel || (liveDraft.model as string),
|
||||
mode: defaultMode || (liveDraft.mode as string),
|
||||
} as Partial<Workflow>));
|
||||
const wf = (result as unknown as { payload: Workflow }).payload;
|
||||
if (wf?.id) { onSaved(wf); return wf; }
|
||||
return null;
|
||||
}, [dispatch, title, description, steps, sourceSessionId, onSaved, liveDraft, defaultModel, defaultMode]);
|
||||
|
||||
const onIgnore = useCallback(async () => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try { await saveWorkflow(); } finally { setBusy(false); }
|
||||
}, [busy, saveWorkflow]);
|
||||
|
||||
const onSaveThenSchedule = useCallback(async () => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const result = await dispatch(createWorkflow({
|
||||
title,
|
||||
description,
|
||||
steps: steps.map((s) => ({ id: s.id, text: s.text })),
|
||||
source_session_id: sourceSessionId,
|
||||
use_synced_prompt: true,
|
||||
} as Partial<Workflow>));
|
||||
const wf = (result as unknown as { payload: Workflow }).payload;
|
||||
if (wf?.id) {
|
||||
onSaved(wf);
|
||||
// Route to the new SchedulingView (Image #49), not the legacy
|
||||
// facet editor. The old `view: 'edit', editFacet: 'Schedule'`
|
||||
// path opens a different design entirely.
|
||||
dispatch(updateWorkflowCard({ workflowId: wf.id, patch: { view: 'scheduling' } }));
|
||||
}
|
||||
const wf = await saveWorkflow();
|
||||
if (wf?.id) dispatch(updateWorkflowCard({ workflowId: wf.id, patch: { view: 'scheduling' } }));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [busy, dispatch, title, description, steps, sourceSessionId, onSaved]);
|
||||
}, [busy, saveWorkflow, dispatch]);
|
||||
|
||||
void onChangeDescription;
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.25, minHeight: '100%' }}>
|
||||
<StepList steps={steps} expandable expandedIds={expandedIds} onToggleExpand={onToggleStep} />
|
||||
<Box sx={{ flex: 1 }} />
|
||||
{/* Schedule prompt card. Soft warning-gold tint + calendar icon, matching
|
||||
Image #35. Gold is the same token the HITL/human-intervention UI uses. */}
|
||||
{/* Schedule prompt card. Soft accent tint + calendar icon. Accent is the
|
||||
same color the human-intervention (AskUserQuestion) popup uses. */}
|
||||
<Box sx={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 1.25,
|
||||
p: 1.5, borderRadius: `${c.radius.lg}px`,
|
||||
bgcolor: c.status.warning + '10',
|
||||
border: `1px solid ${c.status.warning}30`,
|
||||
bgcolor: c.accent.primary + '10',
|
||||
border: `1px solid ${c.accent.primary}30`,
|
||||
}}>
|
||||
<Box sx={{
|
||||
width: 32, height: 32, borderRadius: `${c.radius.md}px`,
|
||||
bgcolor: c.status.warning + '22', color: c.status.warning,
|
||||
bgcolor: c.accent.primary + '22', color: c.accent.primary,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||
}}>
|
||||
<CalendarMonthRounded sx={{ fontSize: 18 }} />
|
||||
@@ -189,11 +199,12 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft,
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 1.5 }}>
|
||||
<Box
|
||||
onClick={onDiscard}
|
||||
onClick={onIgnore}
|
||||
role="button"
|
||||
sx={{
|
||||
fontSize: '0.86rem', fontWeight: 500, color: c.text.secondary,
|
||||
cursor: 'pointer', px: 0.75, py: 0.5,
|
||||
cursor: busy ? 'wait' : 'pointer', px: 0.75, py: 0.5,
|
||||
opacity: busy ? 0.6 : 1,
|
||||
'&:hover': { color: c.text.primary },
|
||||
}}>
|
||||
Ignore
|
||||
@@ -205,10 +216,10 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft,
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.5,
|
||||
fontSize: '0.88rem', fontWeight: 700,
|
||||
px: 1.75, py: 0.6, borderRadius: 999,
|
||||
color: '#fff', bgcolor: c.status.warning,
|
||||
color: '#fff', bgcolor: c.accent.primary,
|
||||
cursor: busy ? 'wait' : 'pointer',
|
||||
opacity: busy ? 0.6 : 1,
|
||||
'&:hover': { bgcolor: c.status.warning, filter: 'brightness(1.06)' },
|
||||
'&:hover': { bgcolor: c.accent.primary, filter: 'brightness(1.06)' },
|
||||
}}>
|
||||
Schedule Workflow
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user