|
|
@@ -5,6 +5,7 @@ import { WalkJourney } from "@/features/runs/WalkJourney";
|
|
|
import { StrategyConfigPanel } from "@/features/runs/StrategyConfigPanel";
|
|
|
|
|
|
import {
|
|
|
+ Activity,
|
|
|
ChevronRight,
|
|
|
FileJson,
|
|
|
GitBranch,
|
|
|
@@ -33,7 +34,8 @@ import type {
|
|
|
StageConclusion,
|
|
|
TimelineResponse,
|
|
|
} from "@/lib/api/types";
|
|
|
-import {compactValue, statusLabel } from "@/lib/status/status";
|
|
|
+import { compactValue } from "@/lib/status/status";
|
|
|
+import { platformLabel } from "@/lib/platform/content";
|
|
|
|
|
|
type DashboardData = {
|
|
|
dashboard: DashboardResponse;
|
|
|
@@ -137,9 +139,12 @@ export function RunDashboardPage({ runId }: { runId: string }) {
|
|
|
|
|
|
{data ? (
|
|
|
<section className="detail-panel business-page">
|
|
|
- <FunnelStrip dashboard={data.dashboard} stages={data.dashboard.stage_conclusions} onSelect={setActiveStage} />
|
|
|
- <PanelNav dashboard={data.dashboard} stages={data.dashboard.stage_conclusions} activeStage={activeStage} onSelect={setActiveStage} />
|
|
|
- <TimelineSummaryStrip runId={runId} timeline={data.timeline} contentItems={data.contentItems} />
|
|
|
+ <div className="run-overview">
|
|
|
+ <PlatformChip data={data} />
|
|
|
+ <FunnelStrip dashboard={data.dashboard} stages={data.dashboard.stage_conclusions} onSelect={setActiveStage} />
|
|
|
+ <RunMetaInline runId={runId} timeline={data.timeline} />
|
|
|
+ </div>
|
|
|
+ <PanelNav stages={data.dashboard.stage_conclusions} activeStage={activeStage} onSelect={setActiveStage} />
|
|
|
<StagePanel
|
|
|
activeStage={activeStage}
|
|
|
data={data}
|
|
|
@@ -155,47 +160,29 @@ export function RunDashboardPage({ runId }: { runId: string }) {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function TimelineSummaryStrip({
|
|
|
- runId,
|
|
|
- timeline,
|
|
|
- contentItems
|
|
|
-}: {
|
|
|
- runId: string;
|
|
|
- timeline: TimelineResponse;
|
|
|
- contentItems: ContentItemsResponse;
|
|
|
-}) {
|
|
|
+// run 级平台 chip(动态,取代写死的全局「Douyin V1」徽章;多数据源天然适配)。
|
|
|
+function PlatformChip({ data }: { data: DashboardData }) {
|
|
|
+ const summary = (data.dashboard.summary || {}) as Record<string, unknown>;
|
|
|
+ const plat = String(data.contentItems.items[0]?.platform || summary.platform || "");
|
|
|
+ const ver = String(summary.strategy_version || "");
|
|
|
+ if (!plat) return null;
|
|
|
+ return (
|
|
|
+ <span className="run-platform-chip">
|
|
|
+ <Activity size={13} />
|
|
|
+ {platformLabel(plat)}{ver ? ` · ${ver}` : ""}
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// 概览条右侧:总耗时 · 事件 · 配额截断,以及完整时间线入口(明细指标收进时间线页)。
|
|
|
+function RunMetaInline({ runId, timeline }: { runId: string; timeline: TimelineResponse }) {
|
|
|
const summary = timeline.summary;
|
|
|
- // V3:Gemini 判定状态计数(来自 content items 的 pattern_match_result.judge_status)。
|
|
|
- const judgeCounts: Record<string, number> = {};
|
|
|
- let hasJudge = false;
|
|
|
- contentItems.items.forEach((item) => {
|
|
|
- const pmr = item.pattern_match_result as Record<string, unknown> | undefined;
|
|
|
- const js = pmr?.judge_status;
|
|
|
- if (typeof js === "string" && js) {
|
|
|
- hasJudge = true;
|
|
|
- judgeCounts[js] = (judgeCounts[js] || 0) + 1;
|
|
|
- }
|
|
|
- });
|
|
|
- const judgeLine = Object.entries(judgeCounts)
|
|
|
- .map(([key, value]) => `${statusLabel(key)} ${value}`)
|
|
|
- .join(" / ");
|
|
|
- // 配额截断事件(run_events 通道)。
|
|
|
const quotaEvent = timeline.items.find((it) => it.event_type === "gemini_quota_exhausted");
|
|
|
- // V2 历史字段:仅老 run 回看时非空。
|
|
|
- const decodeLine = Object.entries(summary?.decode_status_counts || {})
|
|
|
- .map(([key, value]) => `${key} ${value}`)
|
|
|
- .join(" / ");
|
|
|
return (
|
|
|
- <div className="timeline-strip">
|
|
|
- <span>
|
|
|
- 总耗时 {summary?.total_duration_ms == null ? "未知" : `${summary.total_duration_ms} ms`}
|
|
|
- </span>
|
|
|
- <span>query 失败 {summary?.query_failure_count ?? 0}</span>
|
|
|
- <span>平台限流 {summary?.platform_rate_limited_count ?? 0}</span>
|
|
|
- {hasJudge ? <span>Gemini 判定 {judgeLine}</span> : null}
|
|
|
+ <div className="run-meta-inline">
|
|
|
+ <span>总耗时 {summary?.total_duration_ms == null ? "未知" : `${summary.total_duration_ms} ms`}</span>
|
|
|
+ <span>事件 {timeline.total}</span>
|
|
|
{quotaEvent ? <span className="strip-warn">⚠ Gemini 配额截断</span> : null}
|
|
|
- {decodeLine ? <span className="strip-muted">decode(历史) {decodeLine}</span> : null}
|
|
|
- <span>事件 {timeline.total} 条</span>
|
|
|
<Link className="text-button" href={`/runs/${encodeURIComponent(runId)}/timeline`}>
|
|
|
查看完整时间线 / 日志
|
|
|
</Link>
|
|
|
@@ -250,18 +237,15 @@ const PANELS: Array<{ id: string; label: string }> = [
|
|
|
];
|
|
|
|
|
|
function PanelNav({
|
|
|
- dashboard,
|
|
|
stages,
|
|
|
activeStage,
|
|
|
onSelect
|
|
|
}: {
|
|
|
- dashboard: DashboardResponse;
|
|
|
stages: StageConclusion[];
|
|
|
activeStage: string;
|
|
|
onSelect: (stageId: string) => void;
|
|
|
}) {
|
|
|
const statusOf = (id: string) => stages.find((s) => s.stage_id === id)?.status || "";
|
|
|
- const headlineOf = (id: string) => stages.find((s) => s.stage_id === id)?.headline || "";
|
|
|
return (
|
|
|
<nav className="panel-nav" aria-label="工作台面板">
|
|
|
{PANELS.map((p) => {
|
|
|
@@ -270,21 +254,10 @@ function PanelNav({
|
|
|
<button
|
|
|
type="button"
|
|
|
key={p.id}
|
|
|
- className={`panel-nav-card ${statusOf(p.id)} ${active ? "active" : ""}`}
|
|
|
+ className={`panel-nav-tab ${statusOf(p.id)} ${active ? "active" : ""}`}
|
|
|
onClick={() => onSelect(p.id)}
|
|
|
>
|
|
|
- <div className="panel-nav-top">
|
|
|
- <strong>{p.label}</strong>
|
|
|
- </div>
|
|
|
- {p.id === "walk" ? (
|
|
|
- <div className="panel-nav-sub">
|
|
|
- Query {stageCount(dashboard, "query")} · 内容 {stageCount(dashboard, "platform")} · 判定 {stageCount(dashboard, "judge")}
|
|
|
- </div>
|
|
|
- ) : p.id === "config" ? (
|
|
|
- <div className="panel-nav-sub">当前生效的全局策略配置</div>
|
|
|
- ) : (
|
|
|
- <div className="panel-nav-sub">{headlineOf(p.id)}</div>
|
|
|
- )}
|
|
|
+ {p.label}
|
|
|
</button>
|
|
|
);
|
|
|
})}
|