Browse Source

feat(web-v3): 需求详情证据区写细——种子词标注「来自 Pattern」+ 命中分类展开为分类树节点

- 种子词:加「来自 Pattern」标签 + Pattern #itemset_id(频繁项集),点明种子词=该 Pattern 命中分类名、直接作搜索词
- 命中分类:从只显第一条路径,改为「内容分类树节点」列表——每条显示 分类名 + 完整树路径 + 分类树 N 级 · 实质/形式类型(支持多节点)
- 证据规模/验证/挖掘来源 收成一行 meta
- 实测 6e65:Pattern #1608352 = 爱国情感(6级实质)+ 人物故事(4级实质),与种子词一一对应

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sam Lee 1 month ago
parent
commit
7a4fd8140e
2 changed files with 64 additions and 33 deletions
  1. 18 4
      web/app/globals.css
  2. 46 29
      web/features/runs/DemandDetail.tsx

+ 18 - 4
web/app/globals.css

@@ -2036,10 +2036,24 @@ a {
 .demand-card { border: 1px solid #e4e9f2; border-radius: 13px; background: #ffffff; padding: 14px 16px; }
 .demand-card-title { display: inline-flex; align-items: center; gap: 7px; margin: 0 0 12px; font-size: 14px; font-weight: 900; color: #172033; }
 
-.demand-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
-.demand-fact { display: flex; flex-direction: column; gap: 3px; padding: 9px 12px; border: 1px solid #eef1f6; border-radius: 10px; background: #fbfcfe; }
-.demand-fact span { font-size: 11px; font-weight: 800; color: #8491a6; }
-.demand-fact strong { font-size: 13px; font-weight: 900; color: #2c3550; word-break: break-word; }
+/* 凭什么有这个需求:种子词(Pattern)+ 命中分类(分类树)+ 证据规模 */
+.demand-evidence { display: flex; flex-direction: column; gap: 12px; }
+.demand-ev-block { display: flex; flex-direction: column; gap: 7px; }
+.demand-ev-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
+.demand-tag { font-size: 11.5px; font-weight: 900; padding: 3px 10px; border-radius: 7px; }
+.demand-tag.pattern { color: #5a4ba8; background: #f2effb; border: 1px solid #ddd6f3; }
+.demand-tag.tree { color: #146a52; background: #e8f6f0; border: 1px solid #c4e6d8; }
+.demand-pid { font-size: 11px; font-weight: 800; color: #8491a6; }
+.demand-seed-terms { display: flex; flex-wrap: wrap; gap: 7px; }
+.demand-seed-term { font-size: 13px; font-weight: 900; color: #2c3550; background: #eef3fb; border: 1px solid #d8e3f3; border-radius: 8px; padding: 4px 12px; }
+.demand-tip { margin: 0; font-size: 11.5px; line-height: 1.55; color: #8491a6; }
+.demand-cat-list { display: flex; flex-direction: column; gap: 6px; }
+.demand-cat-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 8px 11px; border: 1px solid #eef1f6; border-radius: 10px; background: #fbfcfe; }
+.demand-cat-name { font-size: 13px; font-weight: 900; color: #172033; white-space: nowrap; }
+.demand-cat-path { flex: 1; min-width: 200px; font-family: "SFMono-Regular", ui-monospace, Menlo, monospace; font-size: 11.5px; color: #51607a; word-break: break-all; }
+.demand-cat-meta { font-size: 11px; font-weight: 800; color: #146a52; background: #e8f6f0; border-radius: 6px; padding: 2px 8px; white-space: nowrap; }
+.demand-meta-row { display: flex; flex-wrap: wrap; gap: 14px; font-size: 12px; color: #6d7c93; padding-top: 2px; }
+.demand-meta-row b { color: #2c3550; font-weight: 900; }
 
 .demand-yield { display: flex; gap: 10px; }
 .demand-yield-stat { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 12px 8px; border-radius: 11px; border: 1px solid #e0e6f0; }

+ 46 - 29
web/features/runs/DemandDetail.tsx

@@ -26,16 +26,6 @@ function fmtTime(iso?: string | null): string {
   return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
 }
 
-function firstCategoryPath(seed: AnyRec): string {
-  const bindings = Array.isArray(seed.category_bindings) ? seed.category_bindings : [];
-  const first = rec(bindings[0]);
-  const path = first.category_path || first.category_full_path;
-  if (path) return String(path);
-  const itemsets = Array.isArray(seed.itemsets) ? seed.itemsets : [];
-  const fi = rec(itemsets[0]);
-  return fi.category_path ? String(fi.category_path) : "—";
-}
-
 export function DemandDetail({ item }: { item: RunListItem }) {
   const runId = item.run_id;
   const [dash, setDash] = useState<DashboardResponse | null>(null);
@@ -61,8 +51,13 @@ export function DemandDetail({ item }: { item: RunListItem }) {
   }, [runId]);
 
   const biz = rec(dash?.business_summary);
-  const seedTerms = seed && Array.isArray(seed.seed_terms) ? seed.seed_terms : [];
+  const seedTerms: string[] = seed && Array.isArray(seed.seed_terms) ? seed.seed_terms.map(String) : [];
   const matched = seed && Array.isArray(seed.matched_post_ids) ? seed.matched_post_ids.length : null;
+  // 命中分类 = 这个 Pattern(itemset)锚定的分类树节点(可能多个)
+  const bindings = seed && Array.isArray(seed.category_bindings) ? seed.category_bindings.map(rec) : [];
+  const source = compactValue(seed?.pattern_source_system) || "—";
+  // Pattern 标识 = itemset_id(频繁项集);从分类绑定里取唯一值
+  const patternIds = [...new Set(bindings.map((b) => b.itemset_id).filter((x) => x != null).map(String))];
 
   return (
     <div className="demand-detail">
@@ -89,26 +84,48 @@ export function DemandDetail({ item }: { item: RunListItem }) {
         {loading ? (
           <div className="loading-state">加载中…</div>
         ) : seed ? (
-          <div className="demand-facts">
-            <div className="demand-fact">
-              <span>种子词</span>
-              <strong>{seedTerms.length ? seedTerms.map(String).join("、") : "—"}</strong>
-            </div>
-            <div className="demand-fact">
-              <span>命中分类</span>
-              <strong>{firstCategoryPath(seed)}</strong>
+          <div className="demand-evidence">
+            {/* 种子词 = 这个 Pattern 命中的分类名,直接用作搜索词 */}
+            <div className="demand-ev-block">
+              <div className="demand-ev-head">
+                <span className="demand-tag pattern">种子词 · 来自 Pattern</span>
+                {patternIds.length ? <span className="demand-pid">Pattern #{patternIds.join(" + ")}(频繁项集)</span> : null}
+              </div>
+              <div className="demand-seed-terms">
+                {seedTerms.length ? seedTerms.map((t) => <span className="demand-seed-term" key={t}>{t}</span>) : <span className="muted">—</span>}
+              </div>
+              <p className="demand-tip">
+                上游 Pattern 挖掘({source})得出的频繁项集,种子词就是这个 Pattern 命中的分类名,直接作为搜索词去找内容。
+              </p>
             </div>
-            <div className="demand-fact">
-              <span>证据样本</span>
-              <strong>{matched != null ? `${matched} 条匹配` : "—"}</strong>
-            </div>
-            <div className="demand-fact">
-              <span>验证状态</span>
-              <strong>{compactValue(seed.validation_status) || "—"}</strong>
+
+            {/* 命中分类 = 分类树节点 */}
+            <div className="demand-ev-block">
+              <div className="demand-ev-head">
+                <span className="demand-tag tree">命中分类 · 内容分类树节点</span>
+              </div>
+              {bindings.length ? (
+                <div className="demand-cat-list">
+                  {bindings.map((b, i) => (
+                    <div className="demand-cat-row" key={i}>
+                      <strong className="demand-cat-name">{compactValue(b.category_name)}</strong>
+                      <code className="demand-cat-path">{compactValue(b.category_path)}</code>
+                      <span className="demand-cat-meta">
+                        分类树 {compactValue(b.category_level)} 级 · {compactValue(b.category_source_type)}
+                      </span>
+                    </div>
+                  ))}
+                </div>
+              ) : (
+                <span className="muted">—</span>
+              )}
             </div>
-            <div className="demand-fact">
-              <span>挖掘来源</span>
-              <strong>{compactValue(seed.pattern_source_system) || "—"}</strong>
+
+            {/* 证据规模 / 验证 */}
+            <div className="demand-meta-row">
+              <span>证据样本 <b>{matched != null ? matched : "—"}</b> 条匹配</span>
+              <span>验证状态 <b>{compactValue(seed.validation_status) || "—"}</b></span>
+              <span>挖掘来源 <b>{source}</b></span>
             </div>
           </div>
         ) : (