This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 14:03:56 -04:00
1 parent 806200b3d1
commit 5547c29491
216 files changed
+5043 -3352

No files matched your search

@@ -2,8 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"
import { ArrowRight, ArrowsClockwise, Lightning, Plus, Robot, X } from "../ui/icons"
import type { AutomationAction, AutomationRuleRecord, AutomationStatus, AutomationTrigger, WorkflowPipelineRecord } from "@shared/types"
import { ActionButton, ConfirmDialog } from "../ui"
import { useT } from "../../i18n"
import { ActionButton, ConfirmDialog, Card, Eyebrow } from "../ui"
import { useT, type TranslationFn } from "../../i18n"
import { triggerConfig, actionConfig, type AutomationLogEntry } from "./automation-config"
import { timeAgo } from "../../lib/formatters"
import { RuleCard } from "./RuleCard"
@@ -11,9 +11,9 @@ import { RuleFormModal } from "./RuleEditor"
import { WorkflowCard } from "./WorkflowList"
import { WorkflowFormModal } from "./WorkflowEditor"
function AutomationActivityLog({ entries }: { entries: AutomationLogEntry[] }) {
function AutomationActivityLog({ entries, t }: { entries: AutomationLogEntry[]; t: TranslationFn }) {
if (entries.length === 0) {
return <div className="py-6 text-center text-[13px] text-muted-foreground">No activity yet</div>
return <div className="py-6 text-center text-[13px] text-muted-foreground">{t("auto.activity.noActivity")}</div>
}
return (
<div className="space-y-1">
@@ -48,7 +48,7 @@ function AutomationActivityLog({ entries }: { entries: AutomationLogEntry[] }) {
)
}
function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleRecord[]; companyId: string; onRefresh: () => void }) {
function RulesPanel({ rules, companyId, onRefresh, t }: { rules: AutomationRuleRecord[]; companyId: string; onRefresh: () => void; t: TranslationFn }) {
const [showCreate, setShowCreate] = useState(false); const [editRule, setEditRule] = useState<AutomationRuleRecord | null>(null); const [delTarget, setDelTarget] = useState<AutomationRuleRecord | null>(null)
const [actLog, setActLog] = useState<AutomationLogEntry[]>([]); const [activeTab, setActiveTab] = useState<"rules"|"activity">("rules"); const [filterStatus, setFilterStatus] = useState<AutomationStatus|"all">("all")
const [actionError, setActionError] = useState<string | null>(null)
@@ -56,8 +56,8 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
useEffect(() => { void window.agentCompany.getAutomationLog(companyId).then((r) => { if (r.ok) setActLog(r.data) }) }, [companyId, rules])
const filtered = useMemo(() => filterStatus === "all" ? rules : rules.filter((r) => r.status === filterStatus), [rules, filterStatus])
const counts = useMemo(() => { let active = 0, paused = 0, error = 0; for (const r of rules) { if (r.status === "active") active++; else if (r.status === "paused") paused++; else if (r.status === "error") error++; } return { total: rules.length, active, paused, error } }, [rules])
const handleToggle = useCallback(async (rule: AutomationRuleRecord) => { setActionError(null); try { const r = await window.agentCompany.toggleAutomationRule({ id: rule.id, companyId, status: (rule.status === "active" ? "paused" : "active") as "active"|"paused" }); if (!r.ok) { setActionError(r.error.message); return }; onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : "Failed") } }, [companyId, onRefresh])
const handleDelete = useCallback(async () => { if (!delTarget) return; setActionError(null); try { const r = await window.agentCompany.deleteAutomationRule({ id: delTarget.id, companyId }); if (!r.ok) { setActionError(r.error.message); setDelTarget(null); return }; setDelTarget(null); onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : "Failed"); setDelTarget(null) } }, [delTarget, companyId, onRefresh])
const handleToggle = useCallback(async (rule: AutomationRuleRecord) => { setActionError(null); try { const r = await window.agentCompany.toggleAutomationRule({ id: rule.id, companyId, status: (rule.status === "active" ? "paused" : "active") as "active"|"paused" }); if (!r.ok) { setActionError(r.error.message); return }; onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : t("auto.failedAction")) } }, [companyId, onRefresh, t])
const handleDelete = useCallback(async () => { if (!delTarget) return; setActionError(null); try { const r = await window.agentCompany.deleteAutomationRule({ id: delTarget.id, companyId }); if (!r.ok) { setActionError(r.error.message); setDelTarget(null); return }; setDelTarget(null); onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : t("auto.failedAction")); setDelTarget(null) } }, [delTarget, companyId, onRefresh, t])
return (
<div className="space-y-5">
@@ -75,25 +75,25 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
<Lightning size={20} weight="fill" />
</div>
<div>
<h2 className="text-[16px] font-bold text-foreground">Automation Rules</h2>
<h2 className="text-[16px] font-bold text-foreground">{t("auto.rules.title")}</h2>
<p className="text-[12px] text-muted-foreground">
{counts.active} active, {counts.paused} paused{counts.error > 0 ? `, ${counts.error} errored` : ""}
{t("auto.rules.summaryActive").replace("{n}", String(counts.active))},{t("auto.rules.summaryPaused").replace("{n}", String(counts.paused))}{counts.error > 0 ? `,${t("auto.rules.summaryErrored").replace("{n}", String(counts.error))}` : ""}
</p>
</div>
</div>
<ActionButton label="New Rule" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" />
<ActionButton label={t("auto.rules.newRule")} onClick={() => setShowCreate(true)} icon={Plus} tone="accent" />
</div>
{rules.length > 0 ? (
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
{[
{ l: "Total Rules", v: counts.total, accent: false },
{ l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), accent: true },
{ l: "Recent Activity", v: actLog.length, accent: true },
{ l: t("auto.rules.totalRules"), v: counts.total, accent: false },
{ l: t("auto.rules.totalExecutions"), v: rules.reduce((s, r) => s + r.executionCount, 0), accent: true },
{ l: t("auto.rules.recentActivity"), v: actLog.length, accent: true },
].map((s) => (
<div key={s.l} className="rounded-lg border border-border bg-card px-4 py-3.5">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{s.l}</div>
<Card key={s.l} padding="sm" className="!py-3.5">
<Eyebrow>{s.l}</Eyebrow>
<div className={`text-[26px] font-bold tabular-nums ${s.accent ? "text-primary" : ""}`}>{s.v}</div>
</div>
</Card>
))}
</div>
) : null}
@@ -103,45 +103,51 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
onClick={() => setActiveTab("rules")}
className={`flex-1 rounded-lg px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "rules" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}
>
Rules ({rules.length})
{t("auto.rules.tabRules").replace("{n}", String(rules.length))}
</button>
<button
type="button"
onClick={() => setActiveTab("activity")}
className={`flex-1 rounded-lg px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "activity" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}
>
Activity Log ({actLog.length})
{t("auto.rules.tabActivity").replace("{n}", String(actLog.length))}
</button>
</div>
{activeTab === "rules" ? (
<>
{rules.length > 0 ? (
<div className="flex flex-wrap items-center gap-1.5">
{(["all", "active", "paused", "error"] as const).map((s) => (
<button
key={s}
type="button"
onClick={() => setFilterStatus(s)}
className={`rounded-full px-3 py-1.5 text-[11px] font-medium transition ${filterStatus === s ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-[color:var(--border)]"}`}
>
{s === "all" ? "All" : s.charAt(0).toUpperCase() + s.slice(1)}
</button>
))}
{(["all", "active", "paused", "error"] as const).map((s) => {
const filterLabel = s === "all" ? t("auto.rules.filterAll")
: s === "active" ? t("auto.rules.filterActive")
: s === "paused" ? t("auto.rules.filterPaused")
: t("auto.rules.filterError");
return (
<button
key={s}
type="button"
onClick={() => setFilterStatus(s)}
className={`rounded-full px-3 py-1.5 text-[11px] font-medium transition ${filterStatus === s ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-[color:var(--border)]"}`}
>
{filterLabel}
</button>
);
})}
</div>
) : null}
{filtered.length === 0 && rules.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center">
<Lightning size={36} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-muted-foreground">No automation rules</div>
<div className="text-[14px] font-medium text-muted-foreground">{t("auto.rules.noRules")}</div>
<div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-muted-foreground">
Create your first rule to make the company autonomous.
{t("auto.rules.noRulesDetail")}
</div>
<div className="mt-4">
<ActionButton label="Create First Rule" onClick={() => setShowCreate(true)} icon={Lightning} tone="accent" />
<ActionButton label={t("auto.rules.createFirstRule")} onClick={() => setShowCreate(true)} icon={Lightning} tone="accent" />
</div>
</div>
) : filtered.length === 0 ? (
<div className="py-8 text-center text-[13px] text-muted-foreground">No rules match the current filter.</div>
<div className="py-8 text-center text-[13px] text-muted-foreground">{t("auto.rules.noRulesFilter")}</div>
) : (
<div className="space-y-2">
<AnimatePresence>
@@ -159,7 +165,7 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
)}
</>
) : (
<AutomationActivityLog entries={actLog} />
<AutomationActivityLog entries={actLog} t={t} />
)}
<AnimatePresence>
{showCreate ? (
@@ -171,8 +177,8 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
</AnimatePresence>
<ConfirmDialog
open={!!delTarget}
title="Delete rule"
message={delTarget ? `Are you sure you want to delete ${delTarget.name}? This action cannot be undone.` : ""}
title={t("auto.rules.deleteRule")}
message={delTarget ? t("auto.rules.deleteRuleConfirm").replace("{name}", delTarget.name) : ""}
onConfirm={() => void handleDelete()}
onCancel={() => setDelTarget(null)}
/>
@@ -180,11 +186,11 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
)
}
function WorkflowsPanel({ workflows, companyId, onRefresh }: { workflows: WorkflowPipelineRecord[]; companyId: string; onRefresh: () => void }) {
function WorkflowsPanel({ workflows, companyId, onRefresh, t }: { workflows: WorkflowPipelineRecord[]; companyId: string; onRefresh: () => void; t: TranslationFn }) {
const [showCreate, setShowCreate] = useState(false); const [editWf, setEditWf] = useState<WorkflowPipelineRecord | null>(null); const [delTarget, setDelTarget] = useState<WorkflowPipelineRecord | null>(null)
const [actionError, setActionError] = useState<string | null>(null); const [runningId, setRunningId] = useState<string | null>(null)
const handleDelete = useCallback(async () => { if (!delTarget) return; setActionError(null); try { const r = await window.agentCompany.deleteWorkflow({ id: delTarget.id, companyId }); if (!r.ok) { setActionError(r.error.message); setDelTarget(null); return }; setDelTarget(null); onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : "Failed"); setDelTarget(null) } }, [delTarget, companyId, onRefresh])
const handleRun = useCallback(async (wf: WorkflowPipelineRecord) => { setActionError(null); setRunningId(wf.id); try { const r = await window.agentCompany.runWorkflow({ id: wf.id, companyId }); if (!r.ok) { setActionError(r.error.message); return }; onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : "Failed") } finally { setRunningId(null) } }, [companyId, onRefresh])
const handleDelete = useCallback(async () => { if (!delTarget) return; setActionError(null); try { const r = await window.agentCompany.deleteWorkflow({ id: delTarget.id, companyId }); if (!r.ok) { setActionError(r.error.message); setDelTarget(null); return }; setDelTarget(null); onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : t("auto.failedAction")); setDelTarget(null) } }, [delTarget, companyId, onRefresh, t])
const handleRun = useCallback(async (wf: WorkflowPipelineRecord) => { setActionError(null); setRunningId(wf.id); try { const r = await window.agentCompany.runWorkflow({ id: wf.id, companyId }); if (!r.ok) { setActionError(r.error.message); return }; onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : t("auto.failedAction")) } finally { setRunningId(null) } }, [companyId, onRefresh, t])
const counts = useMemo(() => ({ total: workflows.length, active: workflows.filter((w) => w.status === "active").length, draft: workflows.filter((w) => w.status === "draft").length }), [workflows])
return (
<div className="space-y-5">
@@ -202,24 +208,24 @@ function WorkflowsPanel({ workflows, companyId, onRefresh }: { workflows: Workfl
<ArrowsClockwise size={20} weight="fill" />
</div>
<div>
<h2 className="text-[16px] font-bold text-foreground">Workflow Pipelines</h2>
<h2 className="text-[16px] font-bold text-foreground">{t("auto.workflows.title")}</h2>
<p className="text-[12px] text-muted-foreground">
{counts.active} active, {counts.draft} draft
{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}
{t("auto.workflows.summaryActive").replace("{n}", String(counts.active))},{t("auto.workflows.summaryDraft").replace("{n}", String(counts.draft))}
{counts.total > counts.active + counts.draft ? `,${t("auto.workflows.summaryOther").replace("{n}", String(counts.total - counts.active - counts.draft))}` : ""}
</p>
</div>
</div>
<ActionButton label="New Workflow" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" />
<ActionButton label={t("auto.workflows.newWorkflow")} onClick={() => setShowCreate(true)} icon={Plus} tone="accent" />
</div>
{workflows.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center">
<ArrowsClockwise size={36} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-muted-foreground">No workflow pipelines</div>
<div className="text-[14px] font-medium text-muted-foreground">{t("auto.workflows.noWorkflows")}</div>
<div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-muted-foreground">
Workflows chain actions into multi-step pipelines.
{t("auto.workflows.noWorkflowsDetail")}
</div>
<div className="mt-4">
<ActionButton label="Create First Workflow" onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" />
<ActionButton label={t("auto.workflows.createFirstWorkflow")} onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" />
</div>
</div>
) : (
@@ -248,8 +254,8 @@ function WorkflowsPanel({ workflows, companyId, onRefresh }: { workflows: Workfl
</AnimatePresence>
<ConfirmDialog
open={!!delTarget}
title="Delete workflow"
message={delTarget ? `Are you sure you want to delete ${delTarget.name}? This action cannot be undone.` : ""}
title={t("auto.workflows.deleteWorkflow")}
message={delTarget ? t("auto.workflows.deleteWorkflowConfirm").replace("{name}", delTarget.name) : ""}
onConfirm={() => void handleDelete()}
onCancel={() => setDelTarget(null)}
/>
@@ -269,7 +275,7 @@ export function AutomationPanel({ rules, workflows, companyId, onRefresh }: { ru
<div>
<div className="text-[13px] font-semibold text-foreground">{t("auto.automationEngine")}</div>
<div className="mt-0.5 text-[11px] leading-relaxed text-muted-foreground">
Rules trigger actions based on company events. Workflows chain multiple steps into pipelines. Together, they let your AI company run fully autonomously.
{t("auto.engineDescription")}
</div>
</div>
</div>
@@ -281,7 +287,7 @@ export function AutomationPanel({ rules, workflows, companyId, onRefresh }: { ru
className={`flex flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2 text-[13px] font-medium transition ${activeSection === "rules" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}
>
<Lightning size={15} weight={activeSection === "rules" ? "fill" : "regular"} className={activeSection === "rules" ? "text-primary" : ""} />
Automation Rules ({rules.length})
{t("auto.tabRules").replace("{n}", String(rules.length))}
</button>
<button
type="button"
@@ -289,7 +295,7 @@ export function AutomationPanel({ rules, workflows, companyId, onRefresh }: { ru
className={`flex flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2 text-[13px] font-medium transition ${activeSection === "workflows" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}
>
<ArrowsClockwise size={15} weight={activeSection === "workflows" ? "fill" : "regular"} className={activeSection === "workflows" ? "text-primary" : ""} />
Workflows ({workflows.length})
{t("auto.tabWorkflows").replace("{n}", String(workflows.length))}
</button>
</div>
<AnimatePresence mode="wait">
@@ -301,9 +307,9 @@ export function AutomationPanel({ rules, workflows, companyId, onRefresh }: { ru
transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }}
>
{activeSection === "rules" ? (
<RulesPanel rules={rules} companyId={companyId} onRefresh={onRefresh} />
<RulesPanel rules={rules} companyId={companyId} onRefresh={onRefresh} t={t} />
) : (
<WorkflowsPanel workflows={workflows} companyId={companyId} onRefresh={onRefresh} />
<WorkflowsPanel workflows={workflows} companyId={companyId} onRefresh={onRefresh} t={t} />
)}
</motion.div>
</AnimatePresence>