From 0f1e1f35f1c1b8a051fecf6347cfdef913889c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 12 Mar 2026 07:24:57 +0000 Subject: [PATCH] redesign automation page with simple list + detail panels Replace 8-category layout with flat task list and detail view using WorkspaceView. Searchable list with mode badges, run/delete actions, create dialog. Remove 13 unused component files. Co-Authored-By: Claude Opus 4.6 --- .../Dashboard/Automation/AutomationDetail.tsx | 217 +++++++++++++ .../Automation/AutomationEditChat.tsx | 79 ----- .../Dashboard/Automation/AutomationList.tsx | 202 ++++++++++++ .../Automation/AutomationRightPanel.tsx | 56 ---- .../Automation/AutomationSidebar.tsx | 51 --- .../Screens/Dashboard/Automation/CLAUDE.md | 281 ++--------------- .../Automation/CapabilityDetailView.tsx | 290 ------------------ .../Dashboard/Automation/CapabilityList.tsx | 85 ----- .../Screens/Dashboard/Automation/NewCron.tsx | 94 ------ .../Dashboard/Automation/NewPipeline.tsx | 94 ------ .../Dashboard/Automation/NewProcess.tsx | 94 ------ .../Dashboard/Automation/NewService.tsx | 94 ------ .../Screens/Dashboard/Automation/NewSkill.tsx | 94 ------ .../Screens/Dashboard/Automation/NewTask.tsx | 94 ------ .../Screens/Dashboard/Automation/NewTool.tsx | 94 ------ .../Dashboard/Automation/NewWorkflow.tsx | 94 ------ .../Screens/Dashboard/Automation/index.tsx | 87 +----- 17 files changed, 460 insertions(+), 1640 deletions(-) create mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/AutomationDetail.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/AutomationEditChat.tsx create mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/AutomationList.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/AutomationRightPanel.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/AutomationSidebar.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/CapabilityDetailView.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/CapabilityList.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/NewCron.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/NewPipeline.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/NewProcess.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/NewService.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/NewSkill.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/NewTask.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/NewTool.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/Automation/NewWorkflow.tsx diff --git a/src/apps/officer-web/Screens/Dashboard/Automation/AutomationDetail.tsx b/src/apps/officer-web/Screens/Dashboard/Automation/AutomationDetail.tsx new file mode 100644 index 00000000..f6f34945 --- /dev/null +++ b/src/apps/officer-web/Screens/Dashboard/Automation/AutomationDetail.tsx @@ -0,0 +1,217 @@ +import { useState, useEffect } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeRaw from 'rehype-raw'; +import { Play, Trash2, Terminal, Bot, Workflow } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; +import { useClient } from 'hooks/useClient'; +import { useAuth } from 'hooks/useAuth'; +import { usePanelChannel } from 'hooks/usePanelChannel'; +import { Card } from '@/components/Card'; +import { TaskRunnerModal } from 'officerdev'; +import type { TaskSummary } from 'officerdev'; + +type TaskDetail = { + id: number; + dirName: string; + name: string; + description: string; + scope: string; + mode: string; + language: string | null; + body: string | null; + inputs: Record | null; + config: { steps?: Array<{ task: string; foreach?: string }> } | null; + version: number | null; + userId: number | null; +}; + +const modeLabels: Record = { + script: { label: 'Script', icon: Terminal, color: 'bg-emerald-500/10 text-emerald-600' }, + agentic: { label: 'Agentic', icon: Bot, color: 'bg-violet-500/10 text-violet-600' }, + pipeline: { label: 'Pipeline', icon: Workflow, color: 'bg-amber-500/10 text-amber-600' }, +}; + +export const AutomationDetail = () => { + const client = useClient(); + const qc = useQueryClient(); + const { user } = useAuth(); + const [selected, setSelected] = usePanelChannel('automation:selected-task', null); + const [deleteConfirm, setDeleteConfirm] = useState(false); + const [runTask, setRunTask] = useState(null); + + // Reset delete confirm when selection changes + useEffect(() => { setDeleteConfirm(false); }, [selected?.dirName]); + + const { data: detail } = useQuery({ + queryKey: ['tasks', selected?.dirName], + queryFn: () => client.get(`/tasks/${selected!.dirName}`), + enabled: !!selected?.dirName, + }); + + const handleDelete = async () => { + if (!selected) return; + try { + await client.delete(`/tasks/${selected.dirName}`); + await qc.invalidateQueries({ queryKey: ['tasks'] }); + setDeleteConfirm(false); + setSelected(null); + toast.success('Automation deleted'); + } catch { + toast.error('Failed to delete automation'); + } + }; + + if (!selected) { + return ( +
+

Select an automation to view details

+
+ ); + } + + const canModify = selected.scope === 'user' || user?.role === 'Super Admin'; + const mode = modeLabels[selected.mode] ?? modeLabels.agentic!; + const ModeIcon = mode.icon; + + const hasSteps = detail?.config?.steps && detail.config.steps.length > 0; + const hasInputs = detail?.inputs && Object.keys(detail.inputs).length > 0; + + return ( + <> + + {/* Header */} +
+ + {detail?.name ?? selected.name} + + + + {mode.label} + + + {canModify && ( + + )} +
+ + {/* Content */} +
+ {/* Description */} + {detail?.description && ( +

{detail.description}

+ )} + + {/* Meta badges */} +
+ {detail?.scope && detail.scope !== 'user' && ( + + {detail.scope} + + )} + {detail?.language && ( + + {detail.language} + + )} + {detail?.version && ( + + v{detail.version} + + )} +
+ + {/* Pipeline steps */} + {hasSteps && ( +
+

Pipeline Steps

+
+ {detail!.config!.steps!.map((step, i) => ( +
+ {i + 1} + {step.task} + {step.foreach && (foreach: {step.foreach})} +
+ ))} +
+
+ )} + + {/* Inputs */} + {hasInputs && ( +
+

Inputs

+
+ {Object.entries(detail!.inputs!).map(([key, def]) => { + const d = def as { type?: string; description?: string; default?: string }; + return ( +
+ {key} + {d.type && {d.type}} + {d.description && {d.description}} +
+ ); + })} +
+
+ )} + + {/* Body (agent instructions) */} + {detail?.body ? ( +
+ + {detail.body} + +
+ ) : detail && !hasSteps ? ( +

No instructions

+ ) : null} +
+
+ + {/* Delete confirmation */} + + + + Delete Automation + + Are you sure you want to delete "{selected.name}"? This cannot be undone. + + +
+ + +
+
+
+ + {/* Run modal */} + {runTask && ( + { if (!open) setRunTask(null); }} + task={runTask} + /> + )} + + ); +}; diff --git a/src/apps/officer-web/Screens/Dashboard/Automation/AutomationEditChat.tsx b/src/apps/officer-web/Screens/Dashboard/Automation/AutomationEditChat.tsx deleted file mode 100644 index 70f11a15..00000000 --- a/src/apps/officer-web/Screens/Dashboard/Automation/AutomationEditChat.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useState } from 'react'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { toast } from 'sonner'; -import { Trash2, X } from 'lucide-react'; -import { useClient } from 'hooks/useClient'; -import { usePanelChannel } from 'hooks/usePanelChannel'; -import { CapabilityChat } from '../CapabilityPage'; -import type { CapabilityDetail } from '../CapabilityPage'; -import type { AutomationSelection } from './AutomationRightPanel'; - -export const AutomationEditChat = () => { - const client = useClient(); - const qc = useQueryClient(); - const [selection, setSelection] = usePanelChannel('automation:selected-capability', null); - const [chatKey, setChatKey] = useState(0); - - const { data: detail } = useQuery({ - queryKey: [selection?.queryKey, selection?.dirName], - queryFn: () => client.get(`${selection!.endpoint}/${selection!.dirName}`), - enabled: !!selection, - }); - - const closeChat = () => { - if (!selection) return; - setSelection({ ...selection, editing: false }); - }; - - const deleteChat = async () => { - if (!selection) return; - try { - await client.delete(`${selection.endpoint}/${selection.dirName}/chat`); - await qc.invalidateQueries({ queryKey: [selection.queryKey, selection.dirName] }); - setChatKey((k) => k + 1); - } catch { - toast.error('Failed to delete chat'); - } - }; - - if (!selection || !detail?.filePath) { - return ( -
-

Select a resource and click edit to chat

-
- ); - } - - return ( -
-
- {detail.name ?? 'Chat'} - - -
- 0 ? null : detail.chatSessionId} - isNew={selection.isNew} - description={selection.description} - onResponseEnd={() => { - qc.invalidateQueries({ queryKey: [selection.queryKey, selection.dirName] }); - qc.invalidateQueries({ queryKey: [selection.queryKey] }); - }} - /> -
- ); -}; diff --git a/src/apps/officer-web/Screens/Dashboard/Automation/AutomationList.tsx b/src/apps/officer-web/Screens/Dashboard/Automation/AutomationList.tsx new file mode 100644 index 00000000..b0a69b8b --- /dev/null +++ b/src/apps/officer-web/Screens/Dashboard/Automation/AutomationList.tsx @@ -0,0 +1,202 @@ +import { useState } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import { Plus, Search, Play, Terminal, Bot, Workflow } from 'lucide-react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { useClient } from 'hooks/useClient'; +import { usePanelChannel } from 'hooks/usePanelChannel'; +import { TaskRunnerModal } from 'officerdev'; +import type { TaskSummary } from 'officerdev'; + +const modeIcons = { + script: Terminal, + agentic: Bot, + pipeline: Workflow, +} as const; + +const modeColors = { + script: 'bg-emerald-500/10 text-emerald-600', + agentic: 'bg-violet-500/10 text-violet-600', + pipeline: 'bg-amber-500/10 text-amber-600', +} as const; + +export const AutomationList = () => { + const client = useClient(); + const qc = useQueryClient(); + const [selected, setSelected] = usePanelChannel('automation:selected-task', null); + const [search, setSearch] = useState(''); + const [showCreate, setShowCreate] = useState(false); + const [createName, setCreateName] = useState(''); + const [createDesc, setCreateDesc] = useState(''); + const [creating, setCreating] = useState(false); + const [runTask, setRunTask] = useState(null); + + const { data: tasks = [] } = useQuery({ + queryKey: ['tasks'], + queryFn: () => client.get('/tasks'), + staleTime: 60_000, + }); + + const q = search.toLowerCase(); + const filtered = search + ? tasks.filter((t) => t.name.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q)) + : tasks; + + const handleCreate = async () => { + const name = createName.trim(); + if (!name) return; + setCreating(true); + try { + await client.post('/tasks', { name, description: createDesc.trim() || undefined }); + await qc.invalidateQueries({ queryKey: ['tasks'] }); + setShowCreate(false); + setCreateName(''); + setCreateDesc(''); + toast.success('Automation created'); + } catch { + toast.error('Failed to create automation'); + } finally { + setCreating(false); + } + }; + + return ( + <> +
+ {/* Header */} +
+ Automations + +
+ + {/* Search */} +
+
+ + setSearch(ev.target.value)} + placeholder="Search..." + className="w-full rounded border border-duck-dark/15 dark:border-foreground/15 bg-background/80 pl-7 pr-2 py-1.5 text-sm text-duck-dark dark:text-foreground placeholder:text-duck-dark/30 dark:placeholder:text-foreground/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30" + /> +
+
+ + {/* Task list */} +
+
+ {filtered.map((task) => { + const ModeIcon = modeIcons[task.mode] ?? Bot; + const modeColor = modeColors[task.mode] ?? modeColors.agentic; + const isSelected = selected?.dirName === task.dirName; + return ( +
+ + +
+ ); + })} + {tasks.length === 0 && ( +

+ No automations yet +

+ )} + {tasks.length > 0 && filtered.length === 0 && ( +

+ No matches +

+ )} +
+
+
+ + {/* Create dialog */} + + + + Create Automation + Give your automation a name and optional description. + +
{ ev.preventDefault(); handleCreate(); }} + className="flex flex-col gap-3 mt-1" + > +
+ + setCreateName(ev.target.value)} + placeholder="e.g. Batch Resize Images" + autoFocus + className="rounded border border-duck-dark/20 dark:border-foreground/20 bg-background px-3 py-2 text-sm text-duck-dark dark:text-foreground placeholder:text-duck-dark/30 dark:placeholder:text-foreground/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30" + /> +
+
+ +