Browse Source

feat(web-v3): 顶部压缩(标题改名+去全局平台徽章+漏斗/时间线合一+单行 tab)

- 标题 ContentFind 可视化工作台 → ContentFindAgent;删 AppShell 写死的全局「Douyin V1」徽章
- 平台改 run 级动态 chip(抖音·V1,从 run 数据取),放概览条左侧——多数据源适配
- 漏斗条 + 时间线摘要条 合并成一条概览条(平台chip+漏斗7段+总耗时/事件/配额/查看时间线);明细指标收进时间线页
- 5 面板卡 → 单行紧凑 tab(计数已在漏斗);顶部 4 带→2 带

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sam Lee 1 day ago
parent
commit
bd60dd26ac
3 changed files with 58 additions and 63 deletions
  1. 26 0
      web/app/globals.css
  2. 2 6
      web/components/layout/AppShell.tsx
  3. 30 57
      web/features/runs/RunDashboardPage.tsx

+ 26 - 0
web/app/globals.css

@@ -1718,3 +1718,29 @@ a {
 .cfg-block-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #eef1f6; }
 .cfg-block-head strong { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; color: #172033; }
 .cfg-edit-hint { display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; color: #8a5a12; background: #fdf0d9; border-radius: 6px; padding: 2px 9px; }
+
+/* ===== 顶部压缩:概览条(平台chip + 漏斗 + 运行元信息)+ 单行 tab ===== */
+.run-overview {
+  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
+  padding: 8px 12px; margin-bottom: 8px;
+  border: 1px solid #dfe5ee; border-radius: 10px; background: #ffffff;
+}
+.run-overview .funnel-strip { flex: 1; border: 0; padding: 0; margin: 0; background: transparent; }
+.run-platform-chip {
+  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
+  font-size: 12px; font-weight: 700; color: #2360ad;
+  background: #eaf3ff; border: 1px solid #cfe0f7; border-radius: 999px; padding: 3px 11px;
+}
+.run-meta-inline {
+  display: inline-flex; align-items: center; gap: 12px; flex-wrap: wrap;
+  font-size: 12px; color: #6b7689; white-space: nowrap;
+}
+.run-meta-inline .strip-warn { color: #a82626; font-weight: 600; }
+
+.panel-nav { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
+.panel-nav-tab {
+  font-size: 13px; font-weight: 700; padding: 7px 16px; cursor: pointer;
+  border: 1px solid #dfe5ee; border-radius: 8px; background: #ffffff; color: #5d6b82;
+}
+.panel-nav-tab:hover { border-color: #b9c4d6; background: #fafbfd; }
+.panel-nav-tab.active { border-color: #2360ad; background: #eaf3ff; color: #2360ad; box-shadow: inset 0 0 0 1px #d8e8ff; }

+ 2 - 6
web/components/layout/AppShell.tsx

@@ -1,5 +1,5 @@
 import Link from "next/link";
-import { Activity, ArrowLeft, RefreshCw } from "lucide-react";
+import { ArrowLeft, RefreshCw } from "lucide-react";
 
 export function AppShell({
   children,
@@ -25,17 +25,13 @@ export function AppShell({
             ) : null}
           </div>
           <Link className="brand" href="/runs">
-            <strong>ContentFind 可视化工作台</strong>
+            <strong>ContentFindAgent</strong>
           </Link>
           <div className="toolbar">
             {toolbarLeading}
             <Link className="back-link" href="/config">
               配置
             </Link>
-            <span className="badge">
-              <Activity size={13} />
-              Douyin V1
-            </span>
             {onRefresh ? (
               <button className="icon-button" onClick={onRefresh} type="button" title="刷新">
                 <RefreshCw size={16} />

+ 30 - 57
web/features/runs/RunDashboardPage.tsx

@@ -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>
         );
       })}