diff --git a/src/renderer/components/automation.tsx b/src/renderer/components/automation.tsx index e160ead9..18718ad8 100644 --- a/src/renderer/components/automation.tsx +++ b/src/renderer/components/automation.tsx @@ -1,1697 +1,6 @@ -import { useCallback, useEffect, useMemo, useState } from "react" -import { motion, AnimatePresence } from "framer-motion" -import { - ArrowRight, - ArrowsClockwise, - CaretRight, - Eye, - Funnel, - Gear, - Lightning, - Pause, - Play, - Plus, - Robot, - Timer, - Trash, - X, -} from "@phosphor-icons/react" -import type { - AutomationAction, - AutomationRuleRecord, - AutomationStatus, - AutomationTrigger, - DepartmentId, - WorkflowPipelineRecord, - WorkflowStatus, - WorkflowStep, -} from "@shared/types" -import { timeAgo } from "../lib/formatters" -import { DEPARTMENT_OPTIONS } from "@shared/constants" -import { ActionButton, Select } from "./ui" -import { useT } from "../i18n" - -const ACCENT = "var(--accent)" -const LINE = "var(--line)" - -const triggerConfig: Record = { - task_status_changed: { label: "Task Status Changed", description: "When a task transitions to a new status", icon: ArrowsClockwise }, - task_created: { label: "Task Created", description: "When a new task is added to the system", icon: Plus }, - task_blocked: { label: "Task Blocked", description: "When a task becomes blocked by a dependency", icon: Pause }, - task_reassigned: { label: "Task Reassigned", description: "When a task is reassigned to a different agent", icon: ArrowsClockwise }, - run_completed: { label: "Run Completed", description: "When an agent run finishes successfully", icon: Play }, - run_failed: { label: "Run Failed", description: "When an agent run encounters an error", icon: Trash }, - approval_created: { label: "Approval Created", description: "When a new approval request needs review", icon: Eye }, - approval_resolved: { label: "Approval Resolved", description: "When a pending approval is decided", icon: Eye }, - budget_threshold: { label: "Budget Threshold", description: "When spending exceeds a configured limit", icon: Funnel }, - agent_idle: { label: "Agent Idle", description: "When an agent has no tasks and becomes idle", icon: Robot }, - comment_posted: { label: "Comment Posted", description: "When a new comment is added to a task", icon: Lightning }, - document_created: { label: "Document Created", description: "When a new document is generated", icon: Lightning }, - schedule: { label: "Scheduled", description: "Runs on a cron-like schedule automatically", icon: Timer }, - hire_approved: { label: "Hire Approved", description: "When a new agent hire is approved", icon: Robot }, - sprint_started: { label: "Sprint Started", description: "When a sprint transitions to active", icon: Play }, - sprint_ended: { label: "Sprint Ended", description: "When a sprint reaches completion", icon: ArrowsClockwise }, -} - -const actionConfig: Record = { - assign_task: { label: "Assign Task", description: "Auto-assign a task to the best available agent" }, - create_task: { label: "Create Task", description: "Generate a new task from a template" }, - notify_agent: { label: "Notify Agent", description: "Send a notification to a specific agent" }, - send_message: { label: "Send Message", description: "Deliver a message via inter-agent messaging" }, - trigger_heartbeat: { label: "Trigger Heartbeat", description: "Wake an agent and trigger their heartbeat loop" }, - create_approval: { label: "Create Approval", description: "Request human approval before proceeding" }, - update_task_status: { label: "Update Task Status", description: "Move a task to a new status column" }, - escalate_to_manager: { label: "Escalate to Manager", description: "Route to the responsible manager for review" }, - cross_department_notify: { label: "Cross-Dept Notify", description: "Alert another department about this event" }, - schedule_meeting: { label: "Schedule Meeting", description: "Create a meeting with relevant participants" }, - create_document: { label: "Create Document", description: "Generate a document from a template" }, - auto_approve: { label: "Auto Approve", description: "Automatically approve matching requests" }, - reassign_task: { label: "Reassign Task", description: "Move task to a different agent" }, - run_task: { label: "Run Task", description: "Immediately execute the task with an agent" }, -} - -const departmentOptions = DEPARTMENT_OPTIONS - -const statusColors: Record = { - active: { bg: "var(--success-soft)", text: "var(--success)", dot: "var(--success)" }, - paused: { bg: "var(--warn-soft)", text: "var(--warn)", dot: "var(--warn)" }, - disabled: { bg: "var(--panel-soft)", text: "var(--muted)", dot: "var(--muted)" }, - error: { bg: "var(--danger-soft)", text: "var(--danger)", dot: "var(--danger)" }, -} - -const workflowStatusColors: Record = { - draft: { bg: "var(--panel-soft)", text: "var(--muted-strong)", dot: "var(--muted)" }, - active: { bg: "var(--success-soft)", text: "var(--success)", dot: "var(--success)" }, - paused: { bg: "var(--warn-soft)", text: "var(--warn)", dot: "var(--warn)" }, - completed: { bg: "var(--success-soft)", text: "var(--success)", dot: "var(--success)" }, - failed: { bg: "var(--danger-soft)", text: "var(--danger)", dot: "var(--danger)" }, -} - -const stepStatusConfig: Record = { - pending: { bg: "var(--panel-soft)", border: "var(--line)", text: "var(--muted)" }, - running: { bg: "var(--accent-soft)", border: ACCENT, text: ACCENT }, - completed: { bg: "var(--success-soft)", border: "var(--success)", text: "var(--success)" }, - failed: { bg: "var(--danger-soft)", border: "var(--danger)", text: "var(--danger)" }, - skipped: { bg: "var(--panel-soft)", border: "var(--line)", text: "var(--muted)" }, -} - -function safeJsonParse(json: string): unknown { - try { - return JSON.parse(json) - } catch { - return {} - } -} - -function safeJsonStringify(value: string): string { - try { - const parsed = JSON.parse(value) - return JSON.stringify(parsed, null, 2) - } catch { - return value - } -} - -interface AutomationLogEntry { - ruleId: string - ruleName: string - trigger: string - action: string - executedAt: string -} - -function RuleCard({ - rule, - onToggle, - onDelete, - onEdit, -}: { - rule: AutomationRuleRecord - onToggle: () => void - onDelete: () => void - onEdit: () => void -}) { - const triggerInfo = triggerConfig[rule.trigger] - const actionInfo = actionConfig[rule.action] - const TriggerIcon = triggerInfo?.icon ?? Lightning - const colors = statusColors[rule.status] - - return ( - -
-
- -
- - - -
- - -
-
-
- ) -} - -interface RuleFormState { - name: string - description: string - trigger: AutomationTrigger - conditionsJson: string - action: AutomationAction - actionConfigJson: string - sourceDepartment: string - targetDepartment: string - priority: number -} - -const defaultRuleForm: RuleFormState = { - name: "", - description: "", - trigger: "task_status_changed", - conditionsJson: "{}", - action: "assign_task", - actionConfigJson: "{}", - sourceDepartment: "", - targetDepartment: "", - priority: 50, -} - -function ruleToFormState(rule: AutomationRuleRecord): RuleFormState { - return { - name: rule.name, - description: rule.description, - trigger: rule.trigger, - conditionsJson: safeJsonStringify(rule.conditionsJson), - action: rule.action, - actionConfigJson: safeJsonStringify(rule.actionConfigJson), - sourceDepartment: rule.sourceDepartment ?? "", - targetDepartment: rule.targetDepartment ?? "", - priority: rule.priority, - } -} - -function RuleFormModal({ - open, - editingRule, - companyId, - onClose, - onSaved, -}: { - open: boolean - editingRule: AutomationRuleRecord | null - companyId: string - onClose: () => void - onSaved: () => void -}) { - const isEditing = editingRule !== null - const [form, setForm] = useState(defaultRuleForm) - const [saving, setSaving] = useState(false) - const [error, setError] = useState(null) - - useEffect(() => { - if (open) { - setForm(editingRule ? ruleToFormState(editingRule) : defaultRuleForm) - setError(null) - } - }, [open, editingRule]) - - const updateField = useCallback((key: K, value: RuleFormState[K]) => { - setForm((prev) => ({ ...prev, [key]: value })) - }, []) - - const handleSave = useCallback(async () => { - if (!form.name.trim()) { - setError("Rule name is required") - return - } - try { JSON.parse(form.conditionsJson) } catch { - setError("Conditions must be valid JSON") - return - } - try { JSON.parse(form.actionConfigJson) } catch { - setError("Action config must be valid JSON") - return - } - - setSaving(true) - setError(null) - try { - const result = await window.agentCompany.saveAutomationRule({ - id: editingRule?.id ?? null, - companyId, - name: form.name.trim(), - description: form.description.trim(), - trigger: form.trigger, - conditionsJson: form.conditionsJson, - action: form.action, - actionConfigJson: form.actionConfigJson, - sourceDepartment: (form.sourceDepartment || null) as DepartmentId | null, - targetDepartment: (form.targetDepartment || null) as DepartmentId | null, - priority: form.priority, - status: editingRule?.status ?? "active", - }) - if (!result.ok) { - setError(result.error.message) - return - } - onSaved() - onClose() - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to save rule") - } finally { - setSaving(false) - } - }, [form, editingRule, companyId, onSaved, onClose]) - - if (!open) return null - - const triggerOptions = Object.entries(triggerConfig).map(([key, config]) => ({ - value: key, - label: config.label, - })) - - const actionOptions = Object.entries(actionConfig).map(([key, config]) => ({ - value: key, - label: config.label, - })) - - const selectedTriggerInfo = triggerConfig[form.trigger] - const selectedActionInfo = actionConfig[form.action] - - return ( - - e.stopPropagation()} - > -
-
-
- -
-

- {isEditing ? "Edit Automation Rule" : "New Automation Rule"} -

-
- -
- -
-
- {error ? ( -
- {error} -
- ) : null} - -
- - updateField("name", e.target.value)} - placeholder="e.g., Auto-assign blocked tasks" - className="focus-ring w-full rounded-[8px] border bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]" - style={{ borderColor: LINE }} - /> -
- -
- -