mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-10 03:37:44 +02:00
This commit is contained in:
@@ -405,6 +405,104 @@ async def delete_workflow(workflow_id: str):
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@workflows.router.post("/{workflow_id}/propose-edit")
|
||||
async def propose_edit(workflow_id: str, body: dict):
|
||||
"""Aux-LLM-propose a single-step edit from a natural-language request.
|
||||
|
||||
Powers the Edit Agent chat (Image #38). Frontend hands us the user's
|
||||
message, the current draft steps, and optional failure-context (when
|
||||
we're inside Fix-with-Agent). We respond with a reply string PLUS,
|
||||
optionally, a `step_idx` + `new_text` that the FE shows as a
|
||||
proposal card. The user clicks Apply to merge into their local draft;
|
||||
nothing is persisted until they click Save in the header.
|
||||
"""
|
||||
wf = storage.get_workflow(workflow_id)
|
||||
if not wf:
|
||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||
message = (body or {}).get("message", "").strip()
|
||||
steps_in = (body or {}).get("steps") or []
|
||||
context = (body or {}).get("context") or None
|
||||
if not message or not isinstance(steps_in, list):
|
||||
raise HTTPException(status_code=400, detail="Missing message or steps")
|
||||
try:
|
||||
from backend.apps.agents.providers.registry import resolve_aux_model, get_anthropic_client_for_model
|
||||
from backend.apps.settings.settings import load_settings as _ls
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Aux model unavailable")
|
||||
settings = _ls()
|
||||
try:
|
||||
aux_model, _ = await resolve_aux_model(settings, preferred_tier="haiku")
|
||||
client = get_anthropic_client_for_model(settings, aux_model)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Aux model unavailable")
|
||||
import json, re
|
||||
steps_lines = "\n".join(
|
||||
f"{i+1}. {(s.get('label') or '').strip() or (s.get('text') or '')[:60]}: {(s.get('text') or '')}"
|
||||
for i, s in enumerate(steps_in)
|
||||
)
|
||||
fix_context = ""
|
||||
if context and isinstance(context, dict):
|
||||
fs = context.get("failed_step")
|
||||
err = context.get("error")
|
||||
if fs is not None and err:
|
||||
fix_context = (
|
||||
f"\n\nFAILURE CONTEXT: Step {int(fs) + 1} failed on the most recent run. "
|
||||
f"The error was: {err}\n"
|
||||
f"Your proposed edit should specifically address that failure if possible."
|
||||
)
|
||||
prompt = (
|
||||
"You are an Edit Agent helping the user iterate on a saved automation "
|
||||
"workflow. The workflow's current steps are listed below. The user has "
|
||||
"asked for a modification.\n\n"
|
||||
"Respond with STRICT JSON, no prose, no fence. Schema:\n"
|
||||
' {"reply": string, '
|
||||
'"step_idx": int | null, '
|
||||
'"new_text": string | null, '
|
||||
'"explanation": string | null}\n\n'
|
||||
"Rules:\n"
|
||||
"- `reply` is a short conversational acknowledgement (1-2 sentences).\n"
|
||||
"- If the user is asking a question or for clarification, set step_idx=null and new_text=null.\n"
|
||||
"- If the user is asking to change a specific step, set step_idx (0-based) and new_text to the FULL replacement prompt for that step.\n"
|
||||
"- `explanation` describes the change in user-facing terms.\n"
|
||||
"- Never invent new steps. Never remove steps. Only edit existing ones.\n\n"
|
||||
f"Workflow steps:\n{steps_lines}{fix_context}\n\n"
|
||||
f"User: {message}"
|
||||
)
|
||||
try:
|
||||
resp = await client.messages.create(
|
||||
model=aux_model,
|
||||
max_tokens=400,
|
||||
messages=[
|
||||
{"role": "user", "content": prompt},
|
||||
{"role": "assistant", "content": "{"},
|
||||
],
|
||||
)
|
||||
out = ""
|
||||
if isinstance(resp.content, list):
|
||||
for block in resp.content:
|
||||
if getattr(block, "type", None) == "text":
|
||||
out += getattr(block, "text", "")
|
||||
raw = "{" + out.strip() if not out.strip().startswith("{") else out.strip()
|
||||
m = re.search(r"\{.*\}", raw, flags=re.DOTALL)
|
||||
if m:
|
||||
raw = m.group(0)
|
||||
data = json.loads(raw)
|
||||
except Exception as e:
|
||||
logger.warning("propose-edit: aux LLM failed: %s", e)
|
||||
raise HTTPException(status_code=400, detail="Couldn't generate a proposal")
|
||||
reply = str(data.get("reply") or "").strip()[:600]
|
||||
step_idx = data.get("step_idx")
|
||||
new_text = data.get("new_text")
|
||||
explanation = str(data.get("explanation") or "").strip()[:600]
|
||||
out: dict = {"reply": reply}
|
||||
if isinstance(step_idx, int) and 0 <= step_idx < len(steps_in) and isinstance(new_text, str) and new_text.strip():
|
||||
out["step_idx"] = step_idx
|
||||
out["new_text"] = new_text.strip()
|
||||
if explanation:
|
||||
out["explanation"] = explanation
|
||||
return out
|
||||
|
||||
|
||||
@workflows.router.post("/{workflow_id}/parse-schedule")
|
||||
async def parse_schedule(workflow_id: str, body: dict):
|
||||
"""Aux-LLM-parse natural language into a ScheduleConfig.
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
// Image #38: Edit Agent chat shell embedded inside the workflow card.
|
||||
// Header is { Discard | Save }. Body shows a soft frame around the step
|
||||
// list, then the conversation (agent reply bubbles + tool-call cards +
|
||||
// user bubbles), then a composer at the bottom. Submitting a message
|
||||
// hits /workflows/{id}/edit-step which uses aux LLM to propose a step
|
||||
// edit. The user reviews the proposal, then either applies it to the
|
||||
// draft (local) or asks the agent to try again. Save persists the
|
||||
// accumulated draft via PATCH; Discard reverts to the saved state.
|
||||
//
|
||||
// Test Agent (Image #39) integration lands in slice 5; the "Test" button
|
||||
// here is wired but the spawned sibling currently runs the saved workflow
|
||||
// (not the unsaved draft) until the run endpoint accepts step overrides.
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import TextareaAutosize from '@mui/material/TextareaAutosize';
|
||||
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 { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { clearFixSeed, updateWorkflow, updateWorkflowCard, type Workflow } from '@/shared/state/workflowsSlice';
|
||||
import StepList from './StepList';
|
||||
import { API_BASE, getAuthToken } from '@/shared/config';
|
||||
|
||||
interface Props {
|
||||
workflow: Workflow;
|
||||
steps: Workflow['steps'];
|
||||
isFixMode?: boolean;
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
export default function EditAgentView({ workflow, steps, isFixMode = false }: Props) {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const card = useAppSelector((s) => s.workflows.openCards[workflow.id]);
|
||||
const fixSeed = card?.fixSeed || null;
|
||||
|
||||
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: 'assistant', text: 'How would you like to modify the workflow (e.g. filter out spam emails before summarizing)' },
|
||||
]);
|
||||
const [draft, setDraft] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [showSaveModal, setShowSaveModal] = useState(false);
|
||||
|
||||
useEffect(() => () => { dispatch(clearFixSeed(workflow.id)); }, [dispatch, workflow.id]);
|
||||
|
||||
const dirty = React.useMemo(() => {
|
||||
if (draftSteps.length !== steps.length) return true;
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
if ((draftSteps[i]?.text || '') !== (steps[i]?.text || '')) return true;
|
||||
if ((draftSteps[i]?.label || '') !== (steps[i]?.label || '')) return true;
|
||||
}
|
||||
return false;
|
||||
}, [draftSteps, steps]);
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
const text = draft.trim();
|
||||
if (!text || busy) return;
|
||||
setBusy(true);
|
||||
setTurns((t) => [...t, { kind: 'user', text }]);
|
||||
setDraft('');
|
||||
try {
|
||||
const tok = (() => { try { return getAuthToken(); } catch { return ''; } })();
|
||||
const res = await fetch(`${API_BASE}/workflows/${encodeURIComponent(workflow.id)}/propose-edit`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...(tok ? { Authorization: `Bearer ${tok}` } : {}) },
|
||||
body: JSON.stringify({
|
||||
message: text,
|
||||
steps: draftSteps.map((s) => ({ id: s.id, text: s.text, label: s.label || null })),
|
||||
context: isFixMode && fixSeed ? { failed_step: fixSeed.stepIdx, error: fixSeed.error } : null,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setTurns((t) => [...t, { kind: 'assistant', text: `Sorry, that didn't go through. (${res.status})` }]);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data?.reply) setTurns((t) => [...t, { kind: 'assistant', text: data.reply as string }]);
|
||||
if (typeof data?.step_idx === 'number' && typeof data?.new_text === 'string') {
|
||||
setTurns((t) => [...t, {
|
||||
kind: 'proposal',
|
||||
stepIdx: data.step_idx,
|
||||
before: draftSteps[data.step_idx]?.text || '',
|
||||
after: data.new_text,
|
||||
explanation: data.explanation || '',
|
||||
applied: false,
|
||||
}]);
|
||||
}
|
||||
} catch (e) {
|
||||
setTurns((t) => [...t, { kind: 'assistant', text: (e as Error)?.message || 'Network error.' }]);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [draft, busy, workflow.id, draftSteps, isFixMode, fixSeed]);
|
||||
|
||||
const onApplyProposal = useCallback((turnIdx: number) => {
|
||||
setTurns((all) => {
|
||||
const next = all.slice();
|
||||
const turn = next[turnIdx];
|
||||
if (turn?.kind !== 'proposal' || turn.applied) return all;
|
||||
setDraftSteps((ds) => {
|
||||
const updated = ds.slice();
|
||||
if (updated[turn.stepIdx]) {
|
||||
updated[turn.stepIdx] = { ...updated[turn.stepIdx], text: turn.after };
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
next[turnIdx] = { ...turn, applied: true };
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onDiscard = useCallback(() => {
|
||||
if (dirty) {
|
||||
// No second confirm; the agent's proposals weren't persisted yet.
|
||||
// Reverting just clears local state.
|
||||
}
|
||||
dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'saved' } }));
|
||||
}, [dirty, dispatch, workflow.id]);
|
||||
|
||||
const onSaveClick = useCallback(() => {
|
||||
if (!dirty) {
|
||||
dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'saved' } }));
|
||||
return;
|
||||
}
|
||||
setShowSaveModal(true);
|
||||
}, [dirty, dispatch, workflow.id]);
|
||||
|
||||
const onConfirmSave = useCallback(async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await dispatch(updateWorkflow({
|
||||
id: workflow.id,
|
||||
patch: { steps: draftSteps },
|
||||
ifMatch: workflow.updated_at || null,
|
||||
}));
|
||||
dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'saved' } }));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
setShowSaveModal(false);
|
||||
}
|
||||
}, [dispatch, workflow.id, workflow.updated_at, draftSteps]);
|
||||
|
||||
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. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<HeaderBtn
|
||||
label="Discard"
|
||||
icon={<DeleteOutlineRounded sx={{ fontSize: 16 }} />}
|
||||
onClick={onDiscard}
|
||||
tone="muted"
|
||||
/>
|
||||
<HeaderBtn
|
||||
label={busy ? 'Saving…' : 'Save'}
|
||||
icon={<SaveOutlinedIcon sx={{ fontSize: 16 }} />}
|
||||
onClick={onSaveClick}
|
||||
tone="filled"
|
||||
disabled={busy}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
p: 1.5, borderRadius: `${c.radius.lg}px`,
|
||||
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.elevated,
|
||||
}}>
|
||||
<StepList steps={draftSteps} />
|
||||
</Box>
|
||||
{/* Conversation. Bubbles + tool-call style cards. */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{turns.map((t, idx) => {
|
||||
if (t.kind === 'fix-prefix') {
|
||||
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 sx={{
|
||||
width: 32, height: 32, borderRadius: `${c.radius.md}px`,
|
||||
bgcolor: c.status.error + '22', color: c.status.error,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||
}}>
|
||||
<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}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (t.kind === 'assistant') {
|
||||
return (
|
||||
<Box key={idx} sx={{
|
||||
p: 1.25, 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 }}>
|
||||
{t.text}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (t.kind === 'user') {
|
||||
return (
|
||||
<Box key={idx} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Box sx={{
|
||||
px: 1.5, py: 0.85, borderRadius: `${c.radius.lg}px`,
|
||||
bgcolor: c.bg.secondary,
|
||||
maxWidth: '85%',
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '0.92rem', color: c.text.primary, lineHeight: 1.45 }}>
|
||||
{t.text}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
// proposal
|
||||
return (
|
||||
<Box key={idx} sx={{
|
||||
p: 1.25, borderRadius: `${c.radius.lg}px`,
|
||||
bgcolor: c.accent.primary + '10',
|
||||
border: `1px solid ${c.accent.primary}30`,
|
||||
display: 'flex', flexDirection: 'column', gap: 0.6,
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '0.82rem', fontWeight: 700, color: c.accent.primary }}>
|
||||
Proposed change to Step {t.stepIdx + 1}
|
||||
</Typography>
|
||||
{t.explanation && (
|
||||
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, lineHeight: 1.45 }}>
|
||||
{t.explanation}
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{
|
||||
p: 1, borderRadius: `${c.radius.md}px`,
|
||||
bgcolor: c.bg.surface, border: `1px solid ${c.border.subtle}`,
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '0.78rem', fontWeight: 700, color: c.text.muted, mb: 0.35 }}>AFTER</Typography>
|
||||
<Typography sx={{ fontSize: '0.86rem', color: c.text.primary, whiteSpace: 'pre-wrap', lineHeight: 1.45 }}>
|
||||
{t.after}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
{t.applied ? (
|
||||
<Typography sx={{ fontSize: '0.82rem', color: c.status.success, fontWeight: 700 }}>Applied to draft</Typography>
|
||||
) : (
|
||||
<Box
|
||||
onClick={() => onApplyProposal(idx)}
|
||||
role="button"
|
||||
sx={{
|
||||
fontSize: '0.82rem', fontWeight: 700,
|
||||
color: '#fff', bgcolor: c.accent.primary,
|
||||
px: 1.25, py: 0.4, borderRadius: 999, cursor: 'pointer',
|
||||
'&:hover': { filter: 'brightness(1.05)' },
|
||||
}}>
|
||||
Apply to draft
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<Box sx={{
|
||||
p: 1, borderRadius: `${c.radius.lg}px`,
|
||||
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.surface,
|
||||
display: 'flex', flexDirection: 'column', gap: 0.5,
|
||||
}}>
|
||||
<TextareaAutosize
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
void onSubmit();
|
||||
}
|
||||
}}
|
||||
minRows={1}
|
||||
maxRows={5}
|
||||
placeholder="Agent, @ for context, / for commands"
|
||||
style={{
|
||||
width: '100%', resize: 'none', boxSizing: 'border-box',
|
||||
fontFamily: 'inherit', fontSize: '0.92rem', color: c.text.primary,
|
||||
border: 'none', outline: 'none', background: 'transparent',
|
||||
padding: '6px 4px', lineHeight: 1.45,
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Pill label="Agent" />
|
||||
<Pill label="Claude Opus 4.6" />
|
||||
<Pill label="High" />
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<Box
|
||||
onClick={onSubmit}
|
||||
role="button"
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.3,
|
||||
fontSize: '0.78rem', fontWeight: 700,
|
||||
color: '#fff', bgcolor: c.accent.primary,
|
||||
px: 1.2, py: 0.4, borderRadius: 999,
|
||||
cursor: busy || !draft.trim() ? 'not-allowed' : 'pointer',
|
||||
opacity: busy || !draft.trim() ? 0.5 : 1,
|
||||
'&:hover': { filter: 'brightness(1.05)' },
|
||||
}}>
|
||||
<PlayArrowRounded sx={{ fontSize: 14 }} />
|
||||
{busy ? 'Working…' : 'Send'}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Dialog open={showSaveModal} onClose={() => setShowSaveModal(false)}>
|
||||
<Box sx={{ p: 2.5, minWidth: 380, display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<Typography sx={{ fontSize: '1rem', fontWeight: 700, color: c.text.primary }}>
|
||||
Save changes to workflow?
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.9rem', color: c.text.secondary, lineHeight: 1.5 }}>
|
||||
You're replacing the saved steps with your edits. The next scheduled run will use the new version.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.4, mt: 0.5 }}>
|
||||
{draftSteps.map((s, i) => {
|
||||
const changed = (s.text || '') !== (steps[i]?.text || '');
|
||||
return (
|
||||
<Typography key={s.id} sx={{
|
||||
fontSize: '0.82rem',
|
||||
color: changed ? c.accent.primary : c.text.muted,
|
||||
fontWeight: changed ? 600 : 500,
|
||||
}}>
|
||||
{changed ? '●' : '○'} Step {i + 1}: {s.label || (s.text || '').slice(0, 60)}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 0.5 }}>
|
||||
<Box onClick={() => setShowSaveModal(false)} role="button" sx={{ fontSize: '0.86rem', color: c.text.secondary, px: 1, py: 0.6, cursor: 'pointer', '&:hover': { color: c.text.primary } }}>
|
||||
Keep editing
|
||||
</Box>
|
||||
<Box
|
||||
onClick={onConfirmSave}
|
||||
role="button"
|
||||
sx={{
|
||||
fontSize: '0.86rem', fontWeight: 700, color: '#fff', bgcolor: c.status.success,
|
||||
px: 1.4, py: 0.55, borderRadius: 999, cursor: 'pointer',
|
||||
'&:hover': { filter: 'brightness(1.05)' },
|
||||
}}>
|
||||
{busy ? 'Saving…' : 'Save & close'}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderBtn({ label, icon, onClick, tone, disabled }: { label: string; icon: React.ReactNode; onClick: () => void; tone: 'muted' | 'filled'; disabled?: boolean }) {
|
||||
const c = useClaudeTokens();
|
||||
const filled = tone === 'filled';
|
||||
return (
|
||||
<Box
|
||||
onClick={disabled ? undefined : onClick}
|
||||
role="button"
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.4,
|
||||
fontSize: '0.82rem', fontWeight: 700,
|
||||
px: 1.1, py: 0.45, borderRadius: 999,
|
||||
color: filled ? '#fff' : c.text.secondary,
|
||||
bgcolor: filled ? c.text.primary : 'transparent',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
'&:hover': filled ? { filter: 'brightness(1.05)' } : { color: c.text.primary, bgcolor: c.bg.elevated },
|
||||
}}>
|
||||
{icon}
|
||||
{label}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Pill({ label }: { label: string }) {
|
||||
const c = useClaudeTokens();
|
||||
return (
|
||||
<Box sx={{
|
||||
fontSize: '0.74rem', fontWeight: 600, color: c.text.secondary,
|
||||
px: 0.8, py: 0.25, borderRadius: 999,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
}}>{label}</Box>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import WorkflowEditViews from './WorkflowEditViews';
|
||||
import { HistoryDetail, HistoryList, PreviewView, SavedView } from './WorkflowCardSubviews';
|
||||
import { CompletedView, FailedView, RunningView } from './WorkflowCardLiveViews';
|
||||
import SchedulingView from './SchedulingView';
|
||||
import EditAgentView from './EditAgentView';
|
||||
import StopRounded from '@mui/icons-material/StopRounded';
|
||||
import PauseRounded from '@mui/icons-material/PauseRounded';
|
||||
import { StatusDot, RunSparkline, LastFiredHint, isStaleSinceLastRun } from './workflowVisuals';
|
||||
@@ -612,12 +613,7 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
<SchedulingView workflow={workflow} steps={steps} />
|
||||
)}
|
||||
{(card.view === 'edit_agent' || card.view === 'fix_agent') && workflow && (
|
||||
<WorkflowEditViews
|
||||
workflow={workflow}
|
||||
facet={card.editFacet || 'General'}
|
||||
onChangeFacet={(f) => dispatch(updateWorkflowCard({ workflowId, patch: { editFacet: f } }))}
|
||||
onDirtyChange={setEditDirty}
|
||||
/>
|
||||
<EditAgentView workflow={workflow} steps={steps} isFixMode={card.view === 'fix_agent'} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user