feat(web): surface ai loop writeback receipts
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 55s
CD Pipeline / build-and-deploy (push) Successful in 4m44s
CD Pipeline / post-deploy-checks (push) Successful in 1m51s
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 55s
CD Pipeline / build-and-deploy (push) Successful in 4m44s
CD Pipeline / post-deploy-checks (push) Successful in 1m51s
This commit is contained in:
@@ -11611,9 +11611,34 @@
|
||||
"sourcesDetail": "Project, product, site, service, package, and tool",
|
||||
"events": "Classified events",
|
||||
"eventsDetail": "24h {recent}",
|
||||
"consumer": "Consumer writeback",
|
||||
"consumerDetail": "{targets} targets ready",
|
||||
"ok": "ok",
|
||||
"degraded": "degraded"
|
||||
},
|
||||
"writeback": {
|
||||
"dispatch": "Dispatch ledger",
|
||||
"dispatchDetail": "metadata-only LOG controlled receipts",
|
||||
"apply": "Consumer apply",
|
||||
"applyDetail": "runtime target context receipt readback",
|
||||
"bindings": "Consumer binding",
|
||||
"bindingsDetail": "ready / total",
|
||||
"targets": "Ready targets",
|
||||
"targetsDetail": "KM / RAG / PlayBook / MCP / Verifier / AI Agent",
|
||||
"contextWrites": "Context receipts",
|
||||
"contextWritesDetail": "target writeback receipts",
|
||||
"blockers": "Active blockers",
|
||||
"noBlockers": "No active blocker",
|
||||
"targetDetail": "context writes / bindings",
|
||||
"targetsMap": {
|
||||
"km": "KM",
|
||||
"rag": "RAG",
|
||||
"playbook": "PlayBook",
|
||||
"mcp": "MCP",
|
||||
"verifier": "Verifier",
|
||||
"aiAgent": "AI Agent"
|
||||
}
|
||||
},
|
||||
"recent": "24h {count}",
|
||||
"missing": "{count} missing",
|
||||
"closedDetail": "required stages ok",
|
||||
@@ -11663,6 +11688,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"aiLoop": {
|
||||
"title": "Alert AI Loop",
|
||||
"subtitle": "Alert signals are aligned to LOG / KM / RAG / MCP / PlayBook / Verifier runtime receipts.",
|
||||
"badge": "controlled automation",
|
||||
"runs": "Runs",
|
||||
"workItems": "Work Items",
|
||||
"approvals": "Approvals"
|
||||
}
|
||||
},
|
||||
"automationAssetLedger": {
|
||||
"column": "資產沉澱",
|
||||
"title": "資產沉澱",
|
||||
|
||||
@@ -11611,9 +11611,34 @@
|
||||
"sourcesDetail": "專案 / 產品 / 網站 / 服務 / 套件 / 工具",
|
||||
"events": "分類事件",
|
||||
"eventsDetail": "近 24h {recent}",
|
||||
"consumer": "Consumer 回寫",
|
||||
"consumerDetail": "{targets} 個 target ready",
|
||||
"ok": "ok",
|
||||
"degraded": "degraded"
|
||||
},
|
||||
"writeback": {
|
||||
"dispatch": "Dispatch ledger",
|
||||
"dispatchDetail": "metadata-only LOG controlled receipts",
|
||||
"apply": "Consumer apply",
|
||||
"applyDetail": "runtime target context receipt readback",
|
||||
"bindings": "Consumer binding",
|
||||
"bindingsDetail": "ready / total",
|
||||
"targets": "Ready targets",
|
||||
"targetsDetail": "KM / RAG / PlayBook / MCP / Verifier / AI Agent",
|
||||
"contextWrites": "Context receipts",
|
||||
"contextWritesDetail": "target writeback receipts",
|
||||
"blockers": "Active blockers",
|
||||
"noBlockers": "無 active blocker",
|
||||
"targetDetail": "context writes / bindings",
|
||||
"targetsMap": {
|
||||
"km": "KM",
|
||||
"rag": "RAG",
|
||||
"playbook": "PlayBook",
|
||||
"mcp": "MCP",
|
||||
"verifier": "Verifier",
|
||||
"aiAgent": "AI Agent"
|
||||
}
|
||||
},
|
||||
"recent": "近 24h {count}",
|
||||
"missing": "缺 {count} 節點",
|
||||
"closedDetail": "required stages ok",
|
||||
@@ -11663,6 +11688,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"aiLoop": {
|
||||
"title": "告警 AI Loop",
|
||||
"subtitle": "告警訊號直接對齊 LOG / KM / RAG / MCP / PlayBook / Verifier 的 runtime receipt。",
|
||||
"badge": "controlled automation",
|
||||
"runs": "Runs",
|
||||
"workItems": "Work Items",
|
||||
"approvals": "Approvals"
|
||||
}
|
||||
},
|
||||
"automationAssetLedger": {
|
||||
"column": "資產沉澱",
|
||||
"title": "資產沉澱",
|
||||
|
||||
@@ -1,5 +1,87 @@
|
||||
import { redirect } from "next/navigation";
|
||||
"use client";
|
||||
|
||||
export default function AwoooPAlertsPage({ params }: { params: { locale: string } }) {
|
||||
redirect(`/${params.locale}/awooop/runs#ai-alert-card-delivery-readback`);
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
BellRing,
|
||||
CheckCircle2,
|
||||
ListChecks,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
|
||||
import { AutonomousRuntimeReceiptPanel } from "@/components/awooop/autonomous-runtime-receipt-panel";
|
||||
import { Link } from "@/i18n/routing";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{
|
||||
href: "/awooop/runs#ai-alert-card-delivery-readback",
|
||||
labelKey: "runs",
|
||||
icon: Activity,
|
||||
},
|
||||
{
|
||||
href: "/awooop/work-items",
|
||||
labelKey: "workItems",
|
||||
icon: ListChecks,
|
||||
},
|
||||
{
|
||||
href: "/awooop/approvals",
|
||||
labelKey: "approvals",
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export default function AwoooPAlertsPage() {
|
||||
const t = useTranslations("awooop.alerts.aiLoop");
|
||||
|
||||
return (
|
||||
<main
|
||||
className="min-h-screen bg-[#f5f2ea] px-4 py-5 text-[#141413] md:px-6 lg:px-8"
|
||||
data-testid="awooop-alerts-ai-loop-page"
|
||||
>
|
||||
<div className="mx-auto flex w-full max-w-[1600px] flex-col gap-5">
|
||||
<section className="border border-[#e0ddd4] bg-white">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4 px-4 py-4">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center border border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]">
|
||||
<BellRing className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-lg font-semibold tracking-normal text-[#141413]">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<span className="inline-flex items-center gap-1 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-0.5 text-xs font-semibold text-[#17602a]">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{t("badge")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 max-w-3xl text-xs leading-5 text-[#5f5b52]">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex flex-wrap items-center gap-2" aria-label={t("title")}>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="inline-flex items-center gap-2 border border-[#d8d3c7] bg-[#faf9f3] px-3 py-2 text-xs font-semibold text-[#141413] hover:border-[#d97757]"
|
||||
>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
<span>{t(item.labelKey)}</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<AutonomousRuntimeReceiptPanel mode="full" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,6 +200,53 @@ type PriorityWorkOrderPayload = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
type LogConsumerReadbackPayload = {
|
||||
status?: string | null;
|
||||
active_blockers?: string[] | null;
|
||||
controlled_consume?: {
|
||||
controlled_consume_allowed?: boolean | null;
|
||||
runtime_target_write_performed?: boolean | null;
|
||||
} | null;
|
||||
target_rollups?: Array<{
|
||||
target?: string | null;
|
||||
binding_count?: number | null;
|
||||
ready_binding_count?: number | null;
|
||||
target_write_performed?: boolean | null;
|
||||
context_receipt_write_count?: number | null;
|
||||
metadata_only?: boolean | null;
|
||||
}> | null;
|
||||
rollups?: {
|
||||
dispatch_ledger_row_count?: number | null;
|
||||
consumer_apply_receipt_row_count?: number | null;
|
||||
consumer_binding_count?: number | null;
|
||||
ready_consumer_binding_count?: number | null;
|
||||
ready_target_count?: number | null;
|
||||
metadata_only_receipt_count?: number | null;
|
||||
post_apply_verifier_ref_count?: number | null;
|
||||
target_context_receipt_write_count?: number | null;
|
||||
controlled_consumer_readback_ready?: boolean | null;
|
||||
runtime_target_write_performed?: boolean | null;
|
||||
km_consumer_binding_count?: number | null;
|
||||
rag_consumer_binding_count?: number | null;
|
||||
playbook_consumer_binding_count?: number | null;
|
||||
mcp_consumer_binding_count?: number | null;
|
||||
verifier_consumer_binding_count?: number | null;
|
||||
ai_agent_consumer_binding_count?: number | null;
|
||||
km_context_receipt_write_count?: number | null;
|
||||
rag_context_receipt_write_count?: number | null;
|
||||
playbook_context_receipt_write_count?: number | null;
|
||||
mcp_context_receipt_write_count?: number | null;
|
||||
verifier_context_receipt_write_count?: number | null;
|
||||
ai_agent_context_receipt_write_count?: number | null;
|
||||
} | null;
|
||||
operation_boundaries?: {
|
||||
runtime_target_write_performed?: boolean | null;
|
||||
raw_log_payload_persisted?: boolean | null;
|
||||
secret_value_collection_allowed?: boolean | null;
|
||||
github_api_used?: boolean | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type PanelMode = "full" | "compact";
|
||||
type Tone = "ok" | "warn" | "neutral";
|
||||
type WorkFilter = "all" | "completed" | "active" | "pending" | "blocked";
|
||||
@@ -297,6 +344,23 @@ async function fetchPriorityWorkOrder(): Promise<PriorityWorkOrderPayload | null
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLogConsumerReadback(): Promise<LogConsumerReadbackPayload | null> {
|
||||
const controller = new AbortController();
|
||||
const timeout = window.setTimeout(() => controller.abort(), 12_000);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/agents/agent-log-controlled-writeback-consumer-readback`, {
|
||||
cache: "no-store",
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
return (await response.json()) as LogConsumerReadbackPayload;
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export function AutonomousRuntimeReceiptPanel({
|
||||
mode = "full",
|
||||
}: {
|
||||
@@ -306,6 +370,7 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
const locale = useLocale();
|
||||
const [payload, setPayload] = useState<RuntimeControlPayload | null>(null);
|
||||
const [priorityPayload, setPriorityPayload] = useState<PriorityWorkOrderPayload | null>(null);
|
||||
const [consumerPayload, setConsumerPayload] = useState<LogConsumerReadbackPayload | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
const [updatedAt, setUpdatedAt] = useState<Date | null>(null);
|
||||
@@ -313,12 +378,14 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const [next, priority] = await Promise.all([
|
||||
const [next, priority, consumer] = await Promise.all([
|
||||
fetchRuntimeControl(),
|
||||
fetchPriorityWorkOrder(),
|
||||
fetchLogConsumerReadback(),
|
||||
]);
|
||||
setPayload(next);
|
||||
setPriorityPayload(priority);
|
||||
setConsumerPayload(consumer);
|
||||
setError(next === null);
|
||||
setUpdatedAt(next ? new Date() : null);
|
||||
setLoading(false);
|
||||
@@ -347,6 +414,14 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
const sourceFamilyItems = readback?.work_item_progress?.source_family_items ?? [];
|
||||
const latestFlow = readback?.latest_flow_closure;
|
||||
const rollups = payload?.rollups ?? {};
|
||||
const consumerRollups = consumerPayload?.rollups ?? {};
|
||||
const consumerBlockers = consumerPayload?.active_blockers ?? [];
|
||||
const consumerReady = consumerRollups.controlled_consumer_readback_ready === true
|
||||
|| consumerPayload?.status === "controlled_writeback_consumer_readback_ready";
|
||||
const runtimeTargetWritePerformed = consumerRollups.runtime_target_write_performed === true
|
||||
|| consumerPayload?.controlled_consume?.runtime_target_write_performed === true
|
||||
|| consumerPayload?.operation_boundaries?.runtime_target_write_performed === true
|
||||
|| rollups.live_log_controlled_writeback_runtime_target_write_count === 1;
|
||||
const closed = ledger?.closed === true || latestFlow?.closed === true;
|
||||
const dbOk = readback?.db_read_status === "ok";
|
||||
const missingStages = traceLedger?.missing_required_stage_ids
|
||||
@@ -607,6 +682,125 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
icon: Activity,
|
||||
tone: toNumber(rollups.live_log_classified_event_total ?? logRollups.classified_event_total) > 0 ? "ok" as Tone : "neutral" as Tone,
|
||||
},
|
||||
{
|
||||
key: "consumer",
|
||||
label: t("proof.consumer"),
|
||||
value: `${numberValue(
|
||||
rollups.live_log_controlled_writeback_consumer_apply_receipt_count
|
||||
?? consumerRollups.consumer_apply_receipt_row_count
|
||||
)}/${numberValue(
|
||||
rollups.live_log_controlled_writeback_consumer_dispatch_ledger_count
|
||||
?? consumerRollups.dispatch_ledger_row_count
|
||||
)}`,
|
||||
detail: t("proof.consumerDetail", {
|
||||
targets: numberValue(consumerRollups.ready_target_count),
|
||||
}),
|
||||
icon: Database,
|
||||
tone: consumerReady ? "ok" as Tone : "warn" as Tone,
|
||||
},
|
||||
];
|
||||
const writebackCards = [
|
||||
{
|
||||
key: "dispatch",
|
||||
label: t("writeback.dispatch"),
|
||||
value: numberValue(
|
||||
rollups.live_log_controlled_writeback_consumer_dispatch_ledger_count
|
||||
?? consumerRollups.dispatch_ledger_row_count
|
||||
),
|
||||
detail: t("writeback.dispatchDetail"),
|
||||
icon: Send,
|
||||
tone: consumerReady ? "ok" as Tone : "warn" as Tone,
|
||||
},
|
||||
{
|
||||
key: "apply",
|
||||
label: t("writeback.apply"),
|
||||
value: numberValue(
|
||||
rollups.live_log_controlled_writeback_consumer_apply_receipt_count
|
||||
?? consumerRollups.consumer_apply_receipt_row_count
|
||||
),
|
||||
detail: t("writeback.applyDetail"),
|
||||
icon: CheckCircle2,
|
||||
tone: runtimeTargetWritePerformed ? "ok" as Tone : "neutral" as Tone,
|
||||
},
|
||||
{
|
||||
key: "bindings",
|
||||
label: t("writeback.bindings"),
|
||||
value: `${numberValue(consumerRollups.ready_consumer_binding_count)}/${numberValue(consumerRollups.consumer_binding_count)}`,
|
||||
detail: t("writeback.bindingsDetail"),
|
||||
icon: Bot,
|
||||
tone: toNumber(consumerRollups.ready_consumer_binding_count) === toNumber(consumerRollups.consumer_binding_count)
|
||||
&& toNumber(consumerRollups.consumer_binding_count) > 0
|
||||
? "ok" as Tone
|
||||
: "warn" as Tone,
|
||||
},
|
||||
{
|
||||
key: "targets",
|
||||
label: t("writeback.targets"),
|
||||
value: numberValue(consumerRollups.ready_target_count),
|
||||
detail: t("writeback.targetsDetail"),
|
||||
icon: ListChecks,
|
||||
tone: toNumber(consumerRollups.ready_target_count) >= 6 ? "ok" as Tone : "warn" as Tone,
|
||||
},
|
||||
{
|
||||
key: "context",
|
||||
label: t("writeback.contextWrites"),
|
||||
value: numberValue(consumerRollups.target_context_receipt_write_count),
|
||||
detail: t("writeback.contextWritesDetail"),
|
||||
icon: BookOpenCheck,
|
||||
tone: runtimeTargetWritePerformed ? "ok" as Tone : "neutral" as Tone,
|
||||
},
|
||||
{
|
||||
key: "blockers",
|
||||
label: t("writeback.blockers"),
|
||||
value: numberValue(consumerBlockers.length),
|
||||
detail: consumerBlockers.length > 0 ? consumerBlockers[0] : t("writeback.noBlockers"),
|
||||
icon: TriangleAlert,
|
||||
tone: consumerBlockers.length > 0 ? "warn" as Tone : "ok" as Tone,
|
||||
},
|
||||
];
|
||||
const targetWritebackCards = [
|
||||
{
|
||||
key: "km",
|
||||
label: t("writeback.targetsMap.km"),
|
||||
binding: consumerRollups.km_consumer_binding_count,
|
||||
writes: consumerRollups.km_context_receipt_write_count,
|
||||
icon: BookOpenCheck,
|
||||
},
|
||||
{
|
||||
key: "rag",
|
||||
label: t("writeback.targetsMap.rag"),
|
||||
binding: consumerRollups.rag_consumer_binding_count,
|
||||
writes: consumerRollups.rag_context_receipt_write_count,
|
||||
icon: Database,
|
||||
},
|
||||
{
|
||||
key: "playbook",
|
||||
label: t("writeback.targetsMap.playbook"),
|
||||
binding: consumerRollups.playbook_consumer_binding_count,
|
||||
writes: consumerRollups.playbook_context_receipt_write_count,
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
key: "mcp",
|
||||
label: t("writeback.targetsMap.mcp"),
|
||||
binding: consumerRollups.mcp_consumer_binding_count,
|
||||
writes: consumerRollups.mcp_context_receipt_write_count,
|
||||
icon: Bot,
|
||||
},
|
||||
{
|
||||
key: "verifier",
|
||||
label: t("writeback.targetsMap.verifier"),
|
||||
binding: consumerRollups.verifier_consumer_binding_count,
|
||||
writes: consumerRollups.verifier_context_receipt_write_count,
|
||||
icon: Gauge,
|
||||
},
|
||||
{
|
||||
key: "aiAgent",
|
||||
label: t("writeback.targetsMap.aiAgent"),
|
||||
binding: consumerRollups.ai_agent_consumer_binding_count,
|
||||
writes: consumerRollups.ai_agent_context_receipt_write_count,
|
||||
icon: Rocket,
|
||||
},
|
||||
];
|
||||
const visibleWorkItems = orderedWorkItems.filter((item) => matchesWorkFilter(item, workFilter));
|
||||
const orderedCompleted = orderedWorkItems.filter((item) => item.status === "completed").length;
|
||||
@@ -674,7 +868,7 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
|
||||
<div
|
||||
data-testid="ai-automation-production-proof"
|
||||
className="grid gap-px border-b border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-5"
|
||||
className="grid gap-px border-b border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-3 xl:grid-cols-6"
|
||||
>
|
||||
{proofCards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
@@ -699,6 +893,60 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="ai-loop-consumer-writeback-readback"
|
||||
className="border-b border-[#e0ddd4] bg-white"
|
||||
>
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-3 xl:grid-cols-6">
|
||||
{writebackCards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
return (
|
||||
<div key={card.key} className="bg-white px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{card.label}</p>
|
||||
<p className="mt-2 truncate font-mono text-lg font-semibold text-[#141413]">
|
||||
{card.value}
|
||||
</p>
|
||||
</div>
|
||||
<span className={cn("flex h-8 w-8 shrink-0 items-center justify-center border", toneClass(card.tone))}>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-xs leading-5 text-[#5f5b52]">
|
||||
{card.detail}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="grid gap-px border-t border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-3 xl:grid-cols-6">
|
||||
{targetWritebackCards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
const bindingCount = toNumber(card.binding);
|
||||
const writeCount = toNumber(card.writes);
|
||||
return (
|
||||
<div key={card.key} className="bg-white px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#77736a]">{card.label}</p>
|
||||
<p className="mt-2 font-mono text-lg font-semibold text-[#141413]">
|
||||
{numberValue(writeCount)}
|
||||
{" / "}
|
||||
{numberValue(bindingCount)}
|
||||
</p>
|
||||
</div>
|
||||
<Icon className="h-4 w-4 text-[#87867f]" aria-hidden="true" />
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
|
||||
{t("writeback.targetDetail")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-px bg-[#e0ddd4] md:grid-cols-5 xl:grid-cols-12">
|
||||
<div className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("metrics.loop")}</p>
|
||||
|
||||
Reference in New Issue
Block a user