mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 10:17:43 +02:00
[eric] workflows: subtitle row + status pill + image-audit polish across all states
This commit is contained in:
@@ -971,18 +971,18 @@ const AgentCard: React.FC<Props> = ({
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.5,
|
||||
color: c.accent.primary,
|
||||
bgcolor: c.accent.primary + '12',
|
||||
border: `1px solid ${c.accent.primary}40`,
|
||||
fontSize: '0.78rem', fontWeight: 600,
|
||||
px: 1, py: 0.45,
|
||||
color: '#fff',
|
||||
bgcolor: c.accent.primary,
|
||||
border: `1px solid ${c.accent.primary}`,
|
||||
fontSize: '0.78rem', fontWeight: 700,
|
||||
px: 1.1, py: 0.5,
|
||||
borderRadius: `${c.radius.md}px`,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: c.accent.primary + '22' },
|
||||
'&:hover': { filter: 'brightness(1.05)' },
|
||||
}}
|
||||
>
|
||||
<AutoAwesomeIcon sx={{ fontSize: 14 }} />
|
||||
Make workflow
|
||||
Convert to workflow
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -21,6 +21,8 @@ import DeleteOutlineRounded from '@mui/icons-material/DeleteOutlineRounded';
|
||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||
import PlayArrowRounded from '@mui/icons-material/PlayArrowRounded';
|
||||
import BuildRounded from '@mui/icons-material/BuildRounded';
|
||||
import TuneRounded from '@mui/icons-material/TuneRounded';
|
||||
import KeyboardArrowDownRounded from '@mui/icons-material/KeyboardArrowDownRounded';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { clearFixSeed, setCardSidecar, updateWorkflow, updateWorkflowCard, type Workflow } from '@/shared/state/workflowsSlice';
|
||||
@@ -37,11 +39,43 @@ interface Props {
|
||||
isFixMode?: boolean;
|
||||
}
|
||||
|
||||
function InlineSubtitle({ workflow }: { workflow: Workflow }) {
|
||||
const c = useClaudeTokens();
|
||||
const modelsByProvider = useAppSelector((s) => s.models.byProvider);
|
||||
const runs = useAppSelector((s) => s.workflows.runs[workflow.id]);
|
||||
const modelLabel = React.useMemo(() => {
|
||||
if (!workflow?.model) return '';
|
||||
for (const list of Object.values(modelsByProvider || {})) {
|
||||
for (const m of (list as Array<{ value: string; label?: string }>) || []) {
|
||||
if (m.value === workflow.model) return m.label || workflow.model;
|
||||
}
|
||||
}
|
||||
return workflow.model;
|
||||
}, [workflow?.model, modelsByProvider]);
|
||||
const duration = React.useMemo(() => {
|
||||
if (!runs || runs.length === 0) return '';
|
||||
const last = runs.find((r) => r.finished_at);
|
||||
if (!last || !last.finished_at) return '';
|
||||
const ms = new Date(last.finished_at).getTime() - new Date(last.started_at).getTime();
|
||||
if (ms <= 0) return '';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
|
||||
return `${Math.floor(ms / 60_000)}m`;
|
||||
}, [runs]);
|
||||
return (
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1.25, fontSize: '0.82rem', color: c.text.muted, minWidth: 0, overflow: 'hidden' }}>
|
||||
{modelLabel && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{modelLabel}</Box>}
|
||||
{workflow.mode && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{workflow.mode}</Box>}
|
||||
{duration && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{duration}</Box>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type Turn =
|
||||
| { kind: 'user'; text: string }
|
||||
| { kind: 'assistant'; text: string }
|
||||
| { kind: 'proposal'; stepIdx: number; before: string; after: string; explanation: string; applied: boolean }
|
||||
| { kind: 'fix-prefix'; stepIdx: number; stepLabel: string; error: string };
|
||||
| { kind: 'fix-prefix'; stepIdx: number; stepLabel: string; error: string; expanded: boolean };
|
||||
|
||||
export default function EditAgentView({ workflow, steps, isFixMode = false }: Props) {
|
||||
const c = useClaudeTokens();
|
||||
@@ -54,7 +88,7 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr
|
||||
|
||||
const [draftSteps, setDraftSteps] = useState<Workflow['steps']>(steps);
|
||||
const [turns, setTurns] = useState<Turn[]>(() => isFixMode && fixSeed ? [
|
||||
{ kind: 'fix-prefix', stepIdx: fixSeed.stepIdx, stepLabel: fixSeed.stepLabel, error: fixSeed.error },
|
||||
{ kind: 'fix-prefix', stepIdx: fixSeed.stepIdx, stepLabel: fixSeed.stepLabel, error: fixSeed.error, expanded: false },
|
||||
] : [
|
||||
{ kind: 'assistant', text: 'How would you like to modify the workflow (e.g. filter out spam emails before summarizing)' },
|
||||
]);
|
||||
@@ -221,10 +255,25 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.25, minHeight: '100%' }}>
|
||||
{/* Inline header replacement. The card's default action bar is
|
||||
hidden for edit_agent / fix_agent views. */}
|
||||
{/* Inline header replacement. Matches Image #38: subtitle on the
|
||||
LEFT, Settings + Discard + Save flush right. The card's default
|
||||
action bar is hidden for edit_agent / fix_agent views. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<InlineSubtitle workflow={workflow} />
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<Tooltip title="Permissions, actions, cost cap">
|
||||
<Box
|
||||
onClick={() => dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'edit', editFacet: 'Actions' } }))}
|
||||
role="button"
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 28, height: 28, borderRadius: 999,
|
||||
color: c.text.secondary, cursor: 'pointer',
|
||||
'&:hover': { color: c.text.primary, bgcolor: c.bg.elevated },
|
||||
}}>
|
||||
<TuneRounded sx={{ fontSize: 16 }} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<HeaderBtn
|
||||
label="Discard"
|
||||
icon={<DeleteOutlineRounded sx={{ fontSize: 16 }} />}
|
||||
@@ -249,13 +298,23 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{turns.map((t, idx) => {
|
||||
if (t.kind === 'fix-prefix') {
|
||||
const PREVIEW_MAX = 110;
|
||||
const needsExpand = (t.error || '').length > PREVIEW_MAX;
|
||||
const shown = !needsExpand || t.expanded
|
||||
? t.error
|
||||
: (t.error || '').slice(0, PREVIEW_MAX).trimEnd() + '…';
|
||||
return (
|
||||
<Box key={idx} sx={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 1.25,
|
||||
p: 1.25, borderRadius: `${c.radius.lg}px`,
|
||||
bgcolor: c.status.errorBg,
|
||||
border: `1px solid ${c.status.error}30`,
|
||||
}}>
|
||||
<Box
|
||||
key={idx}
|
||||
onClick={needsExpand ? () => setTurns((all) => all.map((tt, i) => i === idx && tt.kind === 'fix-prefix' ? { ...tt, expanded: !tt.expanded } : tt)) : undefined}
|
||||
sx={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 1.25,
|
||||
p: 1.25, borderRadius: `${c.radius.lg}px`,
|
||||
bgcolor: c.status.errorBg,
|
||||
border: `1px solid ${c.status.error}30`,
|
||||
cursor: needsExpand ? 'pointer' : 'default',
|
||||
'&:hover': needsExpand ? { bgcolor: c.status.error + '14' } : {},
|
||||
}}>
|
||||
<Box sx={{
|
||||
width: 32, height: 32, borderRadius: `${c.radius.md}px`,
|
||||
bgcolor: c.status.error + '22', color: c.status.error,
|
||||
@@ -264,11 +323,22 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr
|
||||
<BuildRounded sx={{ fontSize: 16 }} />
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography sx={{ fontSize: '0.92rem', fontWeight: 700, color: c.text.primary, lineHeight: 1.3 }}>
|
||||
Fixing Step {t.stepIdx + 1}: {t.stepLabel}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, mt: 0.25, lineHeight: 1.45 }}>
|
||||
{t.error}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Typography sx={{ flex: 1, fontSize: '0.92rem', fontWeight: 700, color: c.text.primary, lineHeight: 1.3 }}>
|
||||
Fixing Step {t.stepIdx + 1}: {t.stepLabel}
|
||||
</Typography>
|
||||
{needsExpand && (
|
||||
<KeyboardArrowDownRounded sx={{
|
||||
fontSize: 18,
|
||||
color: c.text.muted,
|
||||
transform: t.expanded ? 'rotate(180deg)' : 'none',
|
||||
transition: 'transform 0.18s ease',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
)}
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, mt: 0.25, lineHeight: 1.45, whiteSpace: 'pre-wrap' }}>
|
||||
{shown}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -17,12 +17,45 @@ import { useAppDispatch } from '@/shared/hooks';
|
||||
import { updateWorkflow, updateWorkflowCard, type ScheduleConfig, type Workflow } from '@/shared/state/workflowsSlice';
|
||||
import StepList from './StepList';
|
||||
import { API_BASE, getAuthToken } from '@/shared/config';
|
||||
import { useAppSelector as _useAppSelector } from '@/shared/hooks';
|
||||
|
||||
interface Props {
|
||||
workflow: Workflow;
|
||||
steps: Workflow['steps'];
|
||||
}
|
||||
|
||||
function InlineSubtitle({ workflow }: { workflow: Workflow }) {
|
||||
const c = useClaudeTokens();
|
||||
const modelsByProvider = _useAppSelector((s) => s.models.byProvider);
|
||||
const runs = _useAppSelector((s) => s.workflows.runs[workflow.id]);
|
||||
const modelLabel = React.useMemo(() => {
|
||||
if (!workflow?.model) return '';
|
||||
for (const list of Object.values(modelsByProvider || {})) {
|
||||
for (const m of (list as Array<{ value: string; label?: string }>) || []) {
|
||||
if (m.value === workflow.model) return m.label || workflow.model;
|
||||
}
|
||||
}
|
||||
return workflow.model;
|
||||
}, [workflow?.model, modelsByProvider]);
|
||||
const duration = React.useMemo(() => {
|
||||
if (!runs || runs.length === 0) return '';
|
||||
const last = runs.find((r) => r.finished_at);
|
||||
if (!last || !last.finished_at) return '';
|
||||
const ms = new Date(last.finished_at).getTime() - new Date(last.started_at).getTime();
|
||||
if (ms <= 0) return '';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
|
||||
return `${Math.floor(ms / 60_000)}m`;
|
||||
}, [runs]);
|
||||
return (
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1.25, fontSize: '0.82rem', color: c.text.muted, minWidth: 0, overflow: 'hidden' }}>
|
||||
{modelLabel && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{modelLabel}</Box>}
|
||||
{workflow.mode && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{workflow.mode}</Box>}
|
||||
{duration && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{duration}</Box>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SchedulingView({ workflow, steps }: Props) {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -82,9 +115,11 @@ export default function SchedulingView({ workflow, steps }: Props) {
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.25, minHeight: '100%' }}>
|
||||
{/* Inline header replacement. The default History/Run row is hidden
|
||||
for 'scheduling' view; this Cancel button sits in its place. */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
{/* Inline header replacement. Image #49: subtitle on LEFT, Cancel
|
||||
on RIGHT. The default History/Run row is hidden for 'scheduling'. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<InlineSubtitle workflow={workflow} />
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<Box
|
||||
onClick={onCancel}
|
||||
role="button"
|
||||
|
||||
@@ -94,10 +94,10 @@ export default function StepList(props: Props) {
|
||||
position: 'relative',
|
||||
cursor: hasExpandableBody && !isActive ? 'pointer' : 'default',
|
||||
borderRadius: `${c.radius.md}px`,
|
||||
px: isActive ? 0.5 : 0,
|
||||
py: isActive ? 0.5 : 0,
|
||||
mx: isActive ? -0.5 : 0,
|
||||
bgcolor: isActive ? c.bg.elevated : 'transparent',
|
||||
px: (isActive || (isExpanded && hasExpandableBody)) ? 0.5 : 0,
|
||||
py: (isActive || (isExpanded && hasExpandableBody)) ? 0.5 : 0,
|
||||
mx: (isActive || (isExpanded && hasExpandableBody)) ? -0.5 : 0,
|
||||
bgcolor: (isActive || (isExpanded && hasExpandableBody)) ? c.bg.elevated : 'transparent',
|
||||
transition: 'background 0.18s ease',
|
||||
'&:hover': hasExpandableBody && !isActive ? { bgcolor: c.bg.elevated } : {},
|
||||
}}>
|
||||
|
||||
@@ -451,9 +451,13 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography sx={{ flex: 1, fontWeight: 700, fontSize: '1rem', color: c.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: '-0.005em' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: '1rem', color: c.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: '-0.005em' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<StatusPill view={card.view} workflow={workflow} runs={runs} />
|
||||
<Box sx={{ flex: 1 }} />
|
||||
</>
|
||||
)}
|
||||
{runs && runs.length > 0 && <RunSparkline runs={runs} />}
|
||||
<IconButton
|
||||
@@ -471,7 +475,8 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
The flex spacer is the empty left side; History is a quiet text link, Run is the
|
||||
accent pill. */}
|
||||
{isDraft && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6, px: 2, pb: 1.25, pt: 0, flexShrink: 0, opacity: 0.45, pointerEvents: 'none' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6, px: 2, pb: 1.25, pt: 0, flexShrink: 0 }}>
|
||||
<SubtitleRow workflow={null} runs={null} />
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<TabBtn label="History" icon={<HistoryIcon sx={{ fontSize: 16 }} />} active={false} onClick={() => {}} />
|
||||
<TabBtn label="Run" icon={<PlayArrowIcon sx={{ fontSize: 16 }} />} active={false} accent onClick={() => {}} />
|
||||
@@ -479,6 +484,7 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
)}
|
||||
{!isDraft && workflow && !isHeaderlessView(card.view) && card.view !== 'running' && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6, px: 2, pb: 1.25, pt: 0, flexShrink: 0 }}>
|
||||
<SubtitleRow workflow={workflow} runs={runs} />
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<TabBtn
|
||||
label="History"
|
||||
@@ -677,6 +683,77 @@ function isHeaderlessView(view: string): boolean {
|
||||
return view === 'edit_agent' || view === 'fix_agent' || view === 'scheduling';
|
||||
}
|
||||
|
||||
function StatusPill({ view, workflow, runs }: { view: string; workflow: Workflow | undefined; runs: import('@/shared/state/workflowsSlice').WorkflowRun[] | undefined }) {
|
||||
const c = useClaudeTokens();
|
||||
// Pills appear on the title row to mirror Image #34 (completed source
|
||||
// chat), #40 (running), #42 (completed workflow), #41 (running while
|
||||
// watching). Saved / Preview / Edit / Scheduling have no pill.
|
||||
let label = '';
|
||||
let color = c.text.muted;
|
||||
let bg = c.bg.elevated;
|
||||
if (view === 'running') {
|
||||
label = 'running';
|
||||
color = c.status.success;
|
||||
bg = c.status.successBg;
|
||||
} else if (view === 'completed') {
|
||||
label = 'completed';
|
||||
color = c.text.secondary;
|
||||
bg = c.bg.elevated;
|
||||
} else {
|
||||
// Image #46: failed view has NO pill in the title row; the red X
|
||||
// on the failed step row carries the signal. Preview/Saved/Edit/
|
||||
// Scheduling are likewise pill-less.
|
||||
void workflow; void runs;
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'inline-flex', alignItems: 'center',
|
||||
fontSize: '0.74rem', fontWeight: 600,
|
||||
px: 0.8, py: 0.18, borderRadius: `${c.radius.md}px`,
|
||||
color, bgcolor: bg,
|
||||
ml: 0.25,
|
||||
}}>{label}</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function SubtitleRow({ workflow, runs }: { workflow: Workflow | null; runs: import('@/shared/state/workflowsSlice').WorkflowRun[] | null }) {
|
||||
const c = useClaudeTokens();
|
||||
const modelsByProvider = useAppSelector((s) => s.models.byProvider);
|
||||
// Match Image #34/#35/#36/#38/#40: "Claude Opus 4.6 agent 28s".
|
||||
// Spaces between fields, all in muted text. Duration is the most
|
||||
// recent finished run's elapsed time; falls back to nothing when no
|
||||
// run has completed yet (PreviewView).
|
||||
const modelLabel = React.useMemo(() => {
|
||||
if (!workflow?.model) return '';
|
||||
for (const list of Object.values(modelsByProvider || {})) {
|
||||
for (const m of (list as any[]) || []) {
|
||||
if (m.value === workflow.model) return m.label || workflow.model;
|
||||
}
|
||||
}
|
||||
return workflow.model;
|
||||
}, [workflow?.model, modelsByProvider]);
|
||||
const modeLabel = workflow?.mode || '';
|
||||
const duration = React.useMemo(() => {
|
||||
if (!runs || runs.length === 0) return '';
|
||||
const last = runs.find((r) => r.finished_at);
|
||||
if (!last || !last.finished_at) return '';
|
||||
const ms = new Date(last.finished_at).getTime() - new Date(last.started_at).getTime();
|
||||
if (ms <= 0) return '';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
|
||||
const m = Math.floor(ms / 60_000);
|
||||
return `${m}m`;
|
||||
}, [runs]);
|
||||
return (
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1.25, fontSize: '0.82rem', color: c.text.muted, minWidth: 0, overflow: 'hidden' }}>
|
||||
{modelLabel && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{modelLabel}</Box>}
|
||||
{modeLabel && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{modeLabel}</Box>}
|
||||
{duration && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{duration}</Box>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function RunningHeader({ workflowId }: { workflowId: string }) {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -714,6 +791,7 @@ function RunningHeader({ workflowId }: { workflowId: string }) {
|
||||
}, [dispatch, workflow, isPaused]);
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6, px: 2, pb: 1.25, pt: 0, flexShrink: 0 }}>
|
||||
<SubtitleRow workflow={workflow || null} runs={runs || null} />
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<Box
|
||||
onClick={onStop}
|
||||
|
||||
@@ -305,30 +305,32 @@ export function CompletedView({ workflow, steps, runs, mode = 'card' }: {
|
||||
stepStatuses={statuses}
|
||||
/>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
{/* Success card. Soft green tint + rocket icon. Per Image #42 the
|
||||
subtitle copy ends with "...see exactly what the agent did." */}
|
||||
<Box sx={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 1.25,
|
||||
p: 1.5, borderRadius: `${c.radius.lg}px`,
|
||||
bgcolor: c.status.successBg,
|
||||
border: `1px solid ${c.status.success}30`,
|
||||
}}>
|
||||
{/* Success card. Hidden in sidecar-linked mode (Image #43) so the
|
||||
compacted card stays tight. Soft green tint + rocket icon. */}
|
||||
{!isLinked && (
|
||||
<Box sx={{
|
||||
width: 32, height: 32, borderRadius: `${c.radius.md}px`,
|
||||
bgcolor: c.status.success + '22', color: c.status.success,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||
display: 'flex', alignItems: 'flex-start', gap: 1.25,
|
||||
p: 1.5, borderRadius: `${c.radius.lg}px`,
|
||||
bgcolor: c.status.successBg,
|
||||
border: `1px solid ${c.status.success}30`,
|
||||
}}>
|
||||
<RocketLaunchRounded sx={{ fontSize: 16 }} />
|
||||
<Box sx={{
|
||||
width: 32, height: 32, borderRadius: `${c.radius.md}px`,
|
||||
bgcolor: c.status.success + '22', color: c.status.success,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||
}}>
|
||||
<RocketLaunchRounded sx={{ fontSize: 16 }} />
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography sx={{ fontSize: '0.95rem', fontWeight: 700, color: c.text.primary, lineHeight: 1.3 }}>
|
||||
Workflow Success!
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, mt: 0.25, lineHeight: 1.45 }}>
|
||||
If you're curious, you can click the green button below to see exactly what the agent did.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography sx={{ fontSize: '0.95rem', fontWeight: 700, color: c.text.primary, lineHeight: 1.3 }}>
|
||||
Workflow Success!
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, mt: 0.25, lineHeight: 1.45 }}>
|
||||
If you're curious, you can click the green button below to see exactly what the agent did.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25 }}>
|
||||
<PillButton
|
||||
label="Edit"
|
||||
@@ -338,7 +340,9 @@ export function CompletedView({ workflow, steps, runs, mode = 'card' }: {
|
||||
onClick={onEdit}
|
||||
/>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<GhostTextBtn label="Done" onClick={onDone} />
|
||||
{/* Image #43: Done is hidden in sidecar mode; Stop Viewing alone
|
||||
fills the right slot. Default mode keeps Done + View Agent. */}
|
||||
{!isLinked && <GhostTextBtn label="Done" onClick={onDone} />}
|
||||
{isLinked ? (
|
||||
<PillButton
|
||||
label="Stop Viewing"
|
||||
|
||||
@@ -7,7 +7,6 @@ import InputBase from '@mui/material/InputBase';
|
||||
import HistoryIcon from '@mui/icons-material/HistoryToggleOffRounded';
|
||||
import CalendarTodayRounded from '@mui/icons-material/CalendarTodayRounded';
|
||||
import EditOutlined from '@mui/icons-material/EditOutlined';
|
||||
import TuneRounded from '@mui/icons-material/TuneRounded';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
@@ -239,13 +238,15 @@ function describeSchedule(workflow: Workflow): string {
|
||||
const h12 = ((s.hour + 11) % 12) + 1;
|
||||
const ampm = s.hour < 12 ? 'am' : 'pm';
|
||||
const time = s.minute === 0 ? `${h12}${ampm}` : `${h12}:${String(s.minute).padStart(2, '0')}${ampm}`;
|
||||
if (s.repeat_unit === 'day') return s.repeat_every === 1 ? `Every day at ${time}` : `Every ${s.repeat_every} days at ${time}`;
|
||||
if (s.repeat_unit === 'month') return s.repeat_every === 1 ? `Every month at ${time}` : `Every ${s.repeat_every} months at ${time}`;
|
||||
if (s.repeat_unit === 'day') return s.repeat_every === 1 ? `Daily at ${time}` : `Every ${s.repeat_every} days at ${time}`;
|
||||
if (s.repeat_unit === 'month') return s.repeat_every === 1 ? `Monthly at ${time}` : `Every ${s.repeat_every} months at ${time}`;
|
||||
if (s.on_days.length === 5 && [1,2,3,4,5].every((d) => s.on_days.includes(d))) return `Weekdays at ${time}`;
|
||||
if (s.on_days.length === 2 && [0,6].every((d) => s.on_days.includes(d))) return `Weekends at ${time}`;
|
||||
if (s.on_days.length === 1) {
|
||||
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
return `Every ${labels[s.on_days[0]]} at ${time}`;
|
||||
// Image #50: "Mondays at 3pm" (plural day, no "Every" prefix). Reads
|
||||
// more naturally than "Every Mon at 3pm".
|
||||
const plurals = ['Sundays', 'Mondays', 'Tuesdays', 'Wednesdays', 'Thursdays', 'Fridays', 'Saturdays'];
|
||||
return `${plurals[s.on_days[0]]} at ${time}`;
|
||||
}
|
||||
return `Weekly at ${time}`;
|
||||
}
|
||||
@@ -262,12 +263,6 @@ export function SavedView({ workflow, steps, runs, activeRunId }: { workflow: Wo
|
||||
const openScheduling = useCallback(() => {
|
||||
dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'scheduling' } }));
|
||||
}, [dispatch, workflow.id]);
|
||||
const openFacetEditor = useCallback(() => {
|
||||
// Legacy General/Actions/Schedule facet picker. The new chat-based
|
||||
// EditAgentView replaces it for step iteration; this is the escape
|
||||
// hatch for permissions, cost cap, action allowlists, etc.
|
||||
dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'edit', editFacet: 'Actions' } }));
|
||||
}, [dispatch, workflow.id]);
|
||||
const onToggleStep = useCallback((stepId: string) => {
|
||||
dispatch(toggleExpandedStep({ workflowId: workflow.id, stepId }));
|
||||
}, [dispatch, workflow.id]);
|
||||
@@ -298,37 +293,22 @@ export function SavedView({ workflow, steps, runs, activeRunId }: { workflow: Wo
|
||||
<CalendarTodayRounded sx={{ fontSize: 15, color: c.text.muted, flexShrink: 0 }} />
|
||||
<Box component="span" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{scheduleLine}</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Tooltip title="Permissions, actions, cost cap">
|
||||
<Box
|
||||
onClick={openFacetEditor}
|
||||
role="button"
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 28, height: 28, borderRadius: 999,
|
||||
color: c.text.secondary, cursor: 'pointer',
|
||||
'&:hover': { color: c.text.primary, bgcolor: c.bg.elevated },
|
||||
}}>
|
||||
<TuneRounded sx={{ fontSize: 16 }} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Box
|
||||
onClick={openEditAgent}
|
||||
role="button"
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.45,
|
||||
fontSize: '0.82rem', fontWeight: 600,
|
||||
px: 1.25, py: 0.5,
|
||||
borderRadius: 999,
|
||||
cursor: 'pointer',
|
||||
color: c.text.secondary,
|
||||
bgcolor: 'transparent',
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
'&:hover': { bgcolor: c.bg.elevated, borderColor: c.border.strong || c.border.medium, color: c.text.primary },
|
||||
}}>
|
||||
<EditOutlined sx={{ fontSize: 15 }} />
|
||||
Edit
|
||||
</Box>
|
||||
<Box
|
||||
onClick={openEditAgent}
|
||||
role="button"
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.45,
|
||||
fontSize: '0.82rem', fontWeight: 600,
|
||||
px: 1.25, py: 0.5,
|
||||
borderRadius: 999,
|
||||
cursor: 'pointer',
|
||||
color: c.text.secondary,
|
||||
bgcolor: 'transparent',
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
'&:hover': { bgcolor: c.bg.elevated, borderColor: c.border.strong || c.border.medium, color: c.text.primary },
|
||||
}}>
|
||||
<EditOutlined sx={{ fontSize: 15 }} />
|
||||
Edit
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user