فهرست منبع

feat(frontend): restore f1 f2 query preview dashboard

SamLee 3 هفته پیش
والد
کامیت
792c9f033c

+ 2 - 1
app/api.py

@@ -8,7 +8,7 @@ from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.staticfiles import StaticFiles
 
-from app.routes import acquisition, decode, payloads, runs
+from app.routes import acquisition, decode, payloads, query_generation, runs
 
 ROOT = Path(__file__).resolve().parent.parent
 FRONTEND_DIST = ROOT / "app" / "frontend" / "dist"
@@ -38,6 +38,7 @@ def create_app() -> FastAPI:
     app.include_router(acquisition.router)
     app.include_router(decode.router)
     app.include_router(payloads.router)
+    app.include_router(query_generation.router)
     app.include_router(runs.router)
 
     @app.get("/health")

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 8
app/frontend/dist/assets/index-CFx5IMTf.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 8 - 0
app/frontend/dist/assets/index-D22dQVWx.js


+ 1 - 1
app/frontend/dist/index.html

@@ -4,7 +4,7 @@
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>创作知识 · Query 正交 Demo</title>
-    <script type="module" crossorigin src="/app/assets/index-CFx5IMTf.js"></script>
+    <script type="module" crossorigin src="/app/assets/index-D22dQVWx.js"></script>
     <link rel="stylesheet" crossorigin href="/app/assets/index-B-5DKb0w.css">
   </head>
   <body>

+ 116 - 6
app/frontend/src/pages/CreationDemo.jsx

@@ -1,5 +1,10 @@
 import React, { useEffect, useMemo, useState } from 'react'
 
+const AXIS_LABEL = {
+  实质: '实质(3-4层)',
+  形式: '形式(3-4层)',
+}
+
 function params() {
   return new URLSearchParams(window.location.search)
 }
@@ -28,6 +33,116 @@ function platformCounts(row) {
   return ['xiaohongshu', 'weixin', 'douyin'].map((key) => platforms[key]?.status || '未跑').join(' / ')
 }
 
+function uniqueAxisValues(items, axis) {
+  const seen = new Set()
+  const out = []
+  for (const item of items || []) {
+    const value = item.parts?.[axis]
+    if (value && !seen.has(value)) {
+      seen.add(value)
+      out.push(value)
+    }
+  }
+  return out
+}
+
+function axisValues(data, family, axis) {
+  if (axis === '实质' || axis === '形式') {
+    return uniqueAxisValues(family.items, axis)
+  }
+  const key = axis === '作用/感受/意图' ? '目的池' : axis
+  return data.axis_values?.[key] || uniqueAxisValues(family.items, key)
+}
+
+function AxisColumn({ data, family, axis }) {
+  const values = axisValues(data, family, axis)
+  return (
+    <div className="axcol">
+      <div className="axhd">
+        <span>{AXIS_LABEL[axis] || axis}</span>
+        <span className="gn">{values.length}</span>
+      </div>
+      <div className="axlist">
+        {values.map((value) => <div className="axv" key={value}>{value}</div>)}
+      </div>
+    </div>
+  )
+}
+
+function QueryGenerationPreview() {
+  const [data, setData] = useState(null)
+  const [activeKey, setActiveKey] = useState('f1')
+  const [err, setErr] = useState('')
+
+  useEffect(() => {
+    setErr('')
+    fetch('/api/query-generation/preview?per=30&dry=true')
+      .then((r) => (r.ok ? r.json() : Promise.reject(new Error('query 生成预览读取失败'))))
+      .then((payload) => {
+        setData(payload)
+        const firstKey = payload.families?.[0]?.key
+        if (firstKey) setActiveKey(firstKey)
+      })
+      .catch((e) => setErr(e.message || '读取失败'))
+  }, [])
+
+  if (err) return <div className="empty">{err}</div>
+  if (!data) return <div className="empty">加载中...</div>
+
+  const families = data.families || []
+  const family = families.find((row) => row.key === activeKey) || families[0]
+  const kept = (family?.items || []).filter((item) => item.keep !== false).length
+
+  return (
+    <div>
+      <h1>创作知识 · Query 正交 Demo</h1>
+      <div className="sub">
+        只显示当前激活的 2 种正交方式:f1 / f2。生成逻辑来自正式 query builder;此页只预览,不写 DB、不发起搜索。
+      </div>
+
+      <div className="famcols two">
+        {families.map((row) => (
+          <button
+            className={`fbtn ${row.key === family.key ? 'on' : ''}`}
+            key={row.key}
+            type="button"
+            onClick={() => setActiveKey(row.key)}
+          >
+            <span className="fbtn-pre">{row.axes?.[0] || row.key}</span>
+            <span className="fbtn-suf">{(row.axes || []).slice(1).join(' × ')}</span>
+          </button>
+        ))}
+      </div>
+
+      <div className="cdemo">
+        {(family.axes || []).map((axis) => <AxisColumn key={axis} axis={axis} data={data} family={family} />)}
+        <div className="axcol qcol">
+          <div className="axhd qhd">
+            <span>Query 词</span>
+            <span className="qhd-metrics">
+              <span className="avgcreation pending">预览</span>
+              <span className="gn">留 {kept}/{family.items?.length || 0}</span>
+            </span>
+          </div>
+          <div className="axlist">
+            {(family.items || []).map((item, index) => (
+              <div className={`qrow ${item.keep === false ? 'drop' : 'keep'}`} key={`${item.query}-${index}`} title={item.reason || ''}>
+                <span className="qmark">{item.keep === false ? '✕' : '✓'}</span>
+                {typeof item.valid === 'number' && (
+                  <span className={`qvalid ${item.valid >= 6 ? 'ok' : 'low'}`}>语义{item.valid}</span>
+                )}
+                <span className="qtext">{item.query}</span>
+                <span className="qdetail">未搜索</span>
+                {item.reason && <span className="qreason">{item.reason}</span>}
+              </div>
+            ))}
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
 export default function CreationDemo() {
   const [batch, setBatch] = useState(null)
   const [summary, setSummary] = useState(null)
@@ -57,12 +172,7 @@ export default function CreationDemo() {
   }, [batchId, runId])
 
   if (!batchId) {
-    return (
-      <div>
-        <h1>创作知识采集工作台</h1>
-        <div className="empty">请在 URL 加上 <code>?batch_id=...</code>,可选 <code>&run_id=...</code> 查看正式云端批次。</div>
-      </div>
-    )
+    return <QueryGenerationPreview />
   }
   if (err) return <div className="empty">{err}</div>
   if (!batch) return <div className="empty">加载中...</div>

+ 48 - 0
app/routes/query_generation.py

@@ -0,0 +1,48 @@
+"""Formal query-generation preview API routes."""
+from __future__ import annotations
+
+from typing import Any
+
+from fastapi import APIRouter, Query
+
+from acquisition.queries.builder import QueryBuildOptions, TREES, build_creation_query_batch
+from app.dependencies import _env_file
+from core.config import Settings
+
+router = APIRouter(prefix="/api/query-generation", tags=["query-generation"])
+
+
+def _summary(generated: dict[str, Any]) -> dict[str, Any]:
+    families = generated.get("families") or []
+    return {
+        "family_count": len(families),
+        "family_keys": [family.get("key") for family in families],
+        "query_count": sum(len(family.get("items") or []) for family in families),
+        "kept_count": sum(
+            1
+            for family in families
+            for item in family.get("items") or []
+            if item.get("keep", True)
+        ),
+    }
+
+
+@router.get("/preview")
+def query_generation_preview(
+    per: int = Query(default=30, ge=1, le=100),
+    dry: bool = Query(default=True),
+) -> dict[str, Any]:
+    """Preview the currently active formal query families without writing DB rows."""
+
+    settings = Settings.from_env(_env_file())
+    generated = build_creation_query_batch(
+        settings,
+        tree_path=TREES,
+        options=QueryBuildOptions(
+            per=per,
+            dry=dry,
+            active_family_keys=("f1", "f2"),
+        ),
+    )
+    generated["summary"] = _summary(generated)
+    return generated

+ 10 - 0
tests/test_app_api.py

@@ -245,6 +245,16 @@ def test_app_health_and_formal_acquisition_routes():
         app.dependency_overrides.clear()
 
 
+def test_query_generation_preview_defaults_to_first_two_families():
+    client = TestClient(app)
+    data = client.get("/api/query-generation/preview?per=2").json()
+
+    assert data["summary"]["family_keys"] == ["f1", "f2"]
+    assert [family["key"] for family in data["families"]] == ["f1", "f2"]
+    assert data["summary"]["query_count"] == 4
+    assert data["metadata"]["active_family_keys"] == ["f1", "f2"]
+
+
 def test_formal_app_does_not_expose_legacy_static_mounts():
     paths = {route.path for route in app.routes if hasattr(route, "path")}
     assert "/data" not in paths

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است