feat(awooop): add run detail action panel
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 1m7s
CD Pipeline / build-and-deploy (push) Successful in 3m27s
CD Pipeline / post-deploy-checks (push) Successful in 1m18s

This commit is contained in:
Your Name
2026-05-07 09:25:49 +08:00
parent c52ebfc042
commit beba668a4c
3 changed files with 183 additions and 0 deletions

View File

@@ -12,12 +12,17 @@ import {
Activity,
AlertCircle,
ArrowLeft,
ArrowRight,
CheckCircle2,
Clock,
MessageSquareText,
RefreshCw,
Route,
SearchCheck,
Send,
ShieldAlert,
ShieldCheck,
TriangleAlert,
Wrench,
} from "lucide-react";
@@ -102,6 +107,8 @@ const STATUS_TRANSLATION_KEYS: Record<string, string> = {
waiting_approval: "statuses.waitingApproval",
};
const MANUAL_STATES = new Set(["blocked", "cancelled", "error", "failed", "timeout"]);
function formatTime(value: string | null | undefined, locale: string, emptyLabel: string) {
if (!value) return emptyLabel;
return new Date(value).toLocaleString(locale === "zh-TW" ? "zh-TW" : "en-US", {
@@ -125,6 +132,112 @@ function itemIcon(kind: string) {
return Route;
}
function RunActionPanel({
run,
counts,
emptyLabel,
}: {
run?: RunDetail;
counts?: RunDetailResponse["counts"];
emptyLabel: string;
}) {
const t = useTranslations("awooop.runDetail.action");
if (!run) {
return (
<section className="border border-[#e0ddd4] bg-white p-4">
<div className="h-5 w-40 animate-pulse bg-[#f2efe6]" />
<div className="mt-3 h-4 w-full max-w-xl animate-pulse bg-[#f2efe6]" />
</section>
);
}
const isApproval = run.state === "waiting_approval";
const isManual = MANUAL_STATES.has(run.state);
const isCompleted = run.state === "completed" || run.state === "success";
const actionKey = isApproval
? "approval"
: isManual
? "manual"
: isCompleted
? "completed"
: run.state === "running"
? "running"
: "observe";
const Icon = isApproval
? ShieldCheck
: isManual
? TriangleAlert
: isCompleted
? CheckCircle2
: run.state === "running"
? Activity
: SearchCheck;
const toneClass = isApproval
? "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"
: isManual
? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
: isCompleted
? "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"
: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]";
const projectParam = encodeURIComponent(run.project_id);
const primaryHref = isApproval
? (`/awooop/approvals/${run.run_id}?project_id=${projectParam}` as never)
: (`/awooop/runs?project_id=${projectParam}` as never);
const evidence = [
{ label: t("evidence.inbound"), value: counts?.inbound_events ?? 0 },
{ label: t("evidence.outbound"), value: counts?.outbound_messages ?? 0 },
{ label: t("evidence.mcp"), value: counts?.mcp_calls ?? 0 },
{ label: t("evidence.steps"), value: counts?.steps ?? 0 },
];
return (
<section className="grid gap-px border border-[#e0ddd4] bg-[#e0ddd4] lg:grid-cols-[1.1fr_0.9fr]">
<div className="bg-white p-4">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className={cn("flex h-8 w-8 items-center justify-center border", toneClass)}>
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
<div>
<p className="text-xs font-semibold text-[#77736a]">{t("eyebrow")}</p>
<h3 className="text-base font-semibold text-[#141413]">
{t(`${actionKey}.title` as never)}
</h3>
</div>
</div>
<p className="mt-3 max-w-3xl text-sm leading-6 text-[#5f5b52]">
{t(`${actionKey}.detail` as never)}
</p>
</div>
<Link
href={primaryHref}
className={cn(
"inline-flex shrink-0 items-center gap-2 border px-3 py-2 text-sm font-semibold",
toneClass
)}
>
{t(`${actionKey}.primary` as never)}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
<div className="grid grid-cols-2 gap-px bg-[#e0ddd4] md:grid-cols-4 lg:grid-cols-2 xl:grid-cols-4">
{evidence.map((item) => (
<div key={item.label} className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
<p className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
{item.value ?? emptyLabel}
</p>
</div>
))}
</div>
</section>
);
}
function DetailField({
label,
value,
@@ -322,6 +435,8 @@ export default function RunDetailPage({
</div>
</section>
<RunActionPanel run={run} counts={detail?.counts} emptyLabel={t("empty")} />
<section className="grid gap-4 xl:grid-cols-[360px_1fr]">
<aside className="border border-[#e0ddd4] bg-white">
<div className="border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">