فهرست منبع

Load complete topic sources before query generation

SamLee 5 روز پیش
والد
کامیت
6f47548873

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


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 8
app/frontend/dist/assets/index-DZEE1jrB.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>创作知识</title>
-    <script type="module" crossorigin src="/app/assets/index-DZEE1jrB.js"></script>
+    <script type="module" crossorigin src="/app/assets/index-B7Xec4DL.js"></script>
     <link rel="stylesheet" crossorigin href="/app/assets/index-Bxmn3oKF.css">
   </head>
   <body>

+ 8 - 0
app/frontend/src/api/workbench.js

@@ -31,3 +31,11 @@ export function generateTopicTableQueries(payload, { signal } = {}) {
     signal,
   })
 }
+
+export function getTopicTableSources(payload, { signal } = {}) {
+  return request('/api/query-generation/topic-table/source-preview', {
+    method: 'POST',
+    json: payload,
+    signal,
+  })
+}

+ 37 - 76
app/frontend/src/features/query-board/TopicTableQueryPanel.jsx

@@ -1,74 +1,11 @@
-import { useState } from 'react'
-import { generateTopicTableQueries, getTopicTableQueryPrompt } from '../../api/workbench.js'
+import { useCallback, useEffect, useState } from 'react'
+import { generateTopicTableQueries, getTopicTableQueryPrompt, getTopicTableSources } from '../../api/workbench.js'
 
 const EXAMPLE = {
   topicBuildId: '1229',
   topicId: '1392',
 }
 
-const EXAMPLE_GROUPS = [
-  {
-    key: 'unique_hook', label: '灵感点', source_description: '来自选题的灵感点字段',
-    field_mappings: [{
-      field_key: 'point:8371:point_result', field_label: '灵感点字段',
-      field_path: 'topics[id=1392].points[id=8371].point_result',
-      field_value: '在樱花树下低角度仰拍小羊,用CCD记录折射出的彩虹光晕',
-      judgement: '这条灵感里的逆光、动物和低角度画面怎样实际拍出来',
-      queries: ['樱花树下低角度拍小羊怎么安排机位'],
-    }],
-  },
-  {
-    key: 'goal_execution', label: '创作目标', source_description: '来自目的点字段',
-    field_mappings: [{
-      field_key: 'point:8372:point_result', field_label: '目的点字段',
-      field_path: 'topics[id=1392].points[id=8372].point_result', field_value: '传递治愈感',
-      judgement: '治愈感要通过哪些画面和人物状态表现',
-      queries: ['人像摄影怎么通过光线和动作表现治愈感'],
-    }],
-  },
-  {
-    key: 'key_execution', label: '关键镜头和动作', source_description: '来自关键点字段',
-    field_mappings: [{
-      field_key: 'point:8374:point_result', field_label: '关键点字段',
-      field_path: 'topics[id=1392].points[id=8374].point_result', field_value: '逆光氛围感特写',
-      judgement: '逆光特写怎样保住人物细节又有氛围',
-      queries: ['逆光人像特写怎么避免脸部过暗'],
-    }],
-  },
-  {
-    key: 'cross_dimension', label: '实质、形式、意图及元素关系',
-    source_description: '来自组成元素、最终选题描述和元素关系字段',
-    field_mappings: [{
-      field_key: 'item:25686:element_name', field_label: '实质字段',
-      field_path: 'topics[id=1392].composition_items[id=25686].element_name', field_value: '彩虹光晕',
-      judgement: '彩虹光晕怎样在真实拍摄中稳定出现',
-      queries: ['CCD逆光拍摄怎么拍出自然彩虹光晕'],
-    }],
-  },
-  {
-    key: 'account_fit', label: '账号内容偏好',
-    source_description: '来自账号的实质偏好、形式偏好和意图偏好字段',
-    field_mappings: [{
-      field_key: 'topic_direction.persona_dimensions.形式偏好', field_label: '账号形式偏好',
-      field_path: 'topics[id=1392].topic_direction.persona_dimensions.形式偏好',
-      field_value: '低角度仰拍、广角、特写细节、动态摆拍和人景互动',
-      judgement: '这次内容怎样沿用账号常用的镜头和动作',
-      queries: ['穿搭摄影怎么组合低角度全景和细节特写'],
-    }],
-  },
-  {
-    key: 'evidence_gap', label: '参考案例和选取理由',
-    source_description: '来自每个元素引用的参考来源说明字段',
-    field_mappings: [{
-      field_key: 'item:25686:source:0:reason', field_label: '参考来源说明',
-      field_path: 'topics[id=1392].composition_items[id=25686].sources[0].reason',
-      field_value: '案例显示“彩虹印在脸上”能增强画面的惊艳感和新鲜度',
-      judgement: '案例里的彩虹光影换到樱花和小羊场景后怎样调整',
-      queries: ['彩虹投影人像换到户外逆光场景怎么调整'],
-    }],
-  },
-]
-
 function PromptModal({ payload, loading, error, onClose }) {
   return (
     <div className="manual-modal-mask" onClick={onClose}>
@@ -91,6 +28,7 @@ function PromptModal({ payload, loading, error, onClose }) {
 
 function GenerationRows({ result }) {
   const groups = result.source_groups || []
+  const generationCompleted = Boolean(result.summary?.query_generation_completed)
   const [openGroups, setOpenGroups] = useState(() => ({ unique_hook: true }))
 
   const toggleGroup = (key) => {
@@ -144,7 +82,7 @@ function GenerationRows({ result }) {
                       <div className="topic-route-judgement">
                         {usedForQuery
                           ? <p>{row.judgement}</p>
-                          : <p className="topic-unused-copy">本次未使用这个字段生成搜索词</p>}
+                          : <p className="topic-unused-copy">{generationCompleted ? '本次未使用这个字段生成搜索词' : '尚未让大模型判断这个字段'}</p>}
                       </div>
                       <div className="topic-route-queries">
                         {usedForQuery
@@ -154,7 +92,7 @@ function GenerationRows({ result }) {
                               <p>{query}</p>
                             </div>
                           ))
-                          : <p className="topic-unused-copy">暂无对应搜索词</p>}
+                          : <p className="topic-unused-copy">{generationCompleted ? '本次没有生成对应搜索词' : '点击“生成搜索词”后显示结果'}</p>}
                       </div>
                     </article>
                   )
@@ -173,12 +111,37 @@ export default function TopicTableQueryPanel() {
   const [topicId, setTopicId] = useState(EXAMPLE.topicId)
   const [result, setResult] = useState(null)
   const [loading, setLoading] = useState(false)
+  const [sourceLoading, setSourceLoading] = useState(false)
   const [error, setError] = useState('')
   const [promptOpen, setPromptOpen] = useState(false)
   const [prompt, setPrompt] = useState(null)
   const [promptLoading, setPromptLoading] = useState(false)
   const [promptError, setPromptError] = useState('')
 
+  const loadSourceData = useCallback(async (buildValue, topicValue, { signal } = {}) => {
+    setSourceLoading(true)
+    setError('')
+    try {
+      const payload = {
+        topic_build_id: Number(buildValue),
+        topic_id: topicValue ? Number(topicValue) : null,
+        max_queries: 18,
+      }
+      const sourceResult = await getTopicTableSources(payload, { signal })
+      setResult(sourceResult)
+    } catch (e) {
+      if (e.name !== 'AbortError') setError(e.message || '选题表完整数据读取失败')
+    } finally {
+      setSourceLoading(false)
+    }
+  }, [])
+
+  useEffect(() => {
+    const controller = new AbortController()
+    loadSourceData(EXAMPLE.topicBuildId, EXAMPLE.topicId, { signal: controller.signal })
+    return () => controller.abort()
+  }, [loadSourceData])
+
   const showPrompt = async () => {
     setPromptOpen(true)
     if (prompt || promptLoading) return
@@ -225,28 +188,26 @@ export default function TopicTableQueryPanel() {
       <form className="topic-table-form" onSubmit={generate}>
         <label>
           <span>选题表编号</span>
-          <input value={topicBuildId} onChange={(e) => setTopicBuildId(e.target.value)} type="number" min="1" required />
+          <input value={topicBuildId} onChange={(e) => { setTopicBuildId(e.target.value); setResult(null) }} type="number" min="1" required />
         </label>
         <label>
           <span>选题编号</span>
-          <input value={topicId} onChange={(e) => setTopicId(e.target.value)} type="number" min="1" placeholder="不填则取 mature 选题" />
+          <input value={topicId} onChange={(e) => { setTopicId(e.target.value); setResult(null) }} type="number" min="1" placeholder="不填则取 mature 选题" />
         </label>
-        <button className="topic-example-button" type="button" onClick={() => {
+        <button className="topic-example-button" type="button" disabled={sourceLoading || loading} onClick={() => {
           setTopicBuildId(EXAMPLE.topicBuildId)
           setTopicId(EXAMPLE.topicId)
-          setResult(null)
-          setError('')
+          loadSourceData(EXAMPLE.topicBuildId, EXAMPLE.topicId)
         }}>载入真实示例 1229</button>
         <button className="topic-prompt-button" type="button" onClick={showPrompt}>查看生成 Prompt</button>
-        <button className="topic-generate-button" type="submit" disabled={loading}>
+        <button className="topic-generate-button" type="submit" disabled={loading || sourceLoading}>
           {loading ? '大模型正在生成…' : '生成搜索词'}
         </button>
       </form>
 
       {error && <div className="manual-error topic-generation-error">{error}</div>}
-      {!result && !loading && (
-        <GenerationRows result={{ source_groups: EXAMPLE_GROUPS }} />
-      )}
+      {sourceLoading && !result && <div className="topic-loading-card"><span />正在读取1229的完整选题表字段…</div>}
+      {!result && !sourceLoading && !loading && <div className="topic-loading-card">填写选题表编号后,点击“载入真实示例”读取完整字段。</div>}
       {loading && <div className="topic-loading-card"><span />正在读取真实字段,并逐字段生成搜索词…</div>}
       {result && <GenerationRows result={result} />}
       {promptOpen && (

+ 17 - 0
app/routes/query_generation.py

@@ -24,6 +24,7 @@ from query_planning.topic_table import (
     TopicTableGenerationError,
     generate_topic_table_preview,
     preview_to_dict,
+    topic_table_source_preview_to_dict,
     topic_table_prompt,
 )
 
@@ -213,6 +214,22 @@ def topic_table_query_preview(request: TopicTableQueryPreviewRequest) -> dict[st
         raise HTTPException(status_code=422, detail=str(exc)) from exc
 
 
+@router.post("/topic-table/source-preview")
+def topic_table_source_preview(request: TopicTableQueryPreviewRequest) -> dict[str, Any]:
+    """Read all topic-table source fields without calling an LLM."""
+
+    try:
+        source_payload = _topic_source(_env_file()).fetch(request.topic_build_id)
+        return topic_table_source_preview_to_dict(
+            source_payload,
+            topic_id=request.topic_id,
+        )
+    except TopicBuildSourceError as exc:
+        raise HTTPException(status_code=502, detail=str(exc)) from exc
+    except TopicTableGenerationError as exc:
+        raise HTTPException(status_code=422, detail=str(exc)) from exc
+
+
 @router.get("/latest-singleton")
 def latest_singleton_overview(repo: Any = Depends(get_acquisition_repository)) -> dict[str, Any]:
     """Return links from the query preview board to the latest real singleton run."""

+ 69 - 0
query_planning/topic_table.py

@@ -532,6 +532,74 @@ def generate_topic_table_preview(
     )
 
 
+def topic_table_source_preview_to_dict(
+    source_payload: dict[str, Any],
+    *,
+    topic_id: int | None,
+) -> dict[str, Any]:
+    """Return all six source groups without calling an LLM or generating Query."""
+
+    topic = _select_topic(source_payload, topic_id)
+    snapshot = _compact_snapshot(source_payload, topic)
+    field_catalog = _field_catalog(snapshot)
+    source_groups = []
+    for route, config in ROUTES.items():
+        fields = [
+            field
+            for field in field_catalog
+            if field["source_group_key"] == route
+        ]
+        source_groups.append(
+            {
+                "key": route,
+                "label": config["label"],
+                "source_description": config["source_description"],
+                "field_count": len(fields),
+                "selected_field_count": 0,
+                "historical_field_count": sum(
+                    1 for field in fields if not field.get("is_current", True)
+                ),
+                "fields": [
+                    {
+                        "field_key": field["field_key"],
+                        "field_path": field["field_path"],
+                        "field_label": field["field_label"],
+                        "field_value": field["field_value"],
+                        "is_current": field.get("is_current", True),
+                        "used_for_query": False,
+                        "judgement": None,
+                        "queries": [],
+                    }
+                    for field in fields
+                ],
+                "field_mappings": [],
+            }
+        )
+    return {
+        "generator_kind": GeneratorKind.TOPIC_TABLE.value,
+        "source": {
+            "topic_build_id": snapshot["topic_build_id"],
+            "topic_id": snapshot["topic_id"],
+            "source_url": source_payload.get("source_url"),
+        },
+        "topic": {
+            "status": snapshot["topic"].get("status"),
+            "result": snapshot["topic"].get("result"),
+        },
+        "source_groups": source_groups,
+        "field_mappings": [],
+        "queries": [],
+        "summary": {
+            "field_count": len(field_catalog),
+            "selected_count": 0,
+            "query_generation_completed": False,
+            "persisted": False,
+            "search_started": False,
+        },
+        "prompt": {"name": PROMPT_NAME, "version": PROMPT_VERSION},
+    }
+
+
 def preview_to_dict(preview: TopicTablePreview) -> dict[str, Any]:
     result = preview.result
     route_counts: dict[str, int] = {route: 0 for route in ROUTES}
@@ -689,6 +757,7 @@ def preview_to_dict(preview: TopicTablePreview) -> dict[str, Any]:
             "selected_count": result.stats.selected_count,
             "dropped_count": result.stats.dropped_count,
             "route_counts": route_counts,
+            "query_generation_completed": True,
             "persisted": False,
             "search_started": False,
         },

+ 38 - 0
tests/test_topic_table_query_generation.py

@@ -330,3 +330,41 @@ def test_topic_table_prompt_and_preview_api_do_not_persist_or_search(monkeypatch
     assert body["summary"]["selected_count"] == 10
     assert body["summary"]["persisted"] is False
     assert body["summary"]["search_started"] is False
+
+
+def test_topic_table_source_preview_returns_every_field_without_calling_llm(monkeypatch):
+    class Source:
+        def fetch(self, topic_build_id):
+            assert topic_build_id == 1229
+            return _topic_payload()
+
+    monkeypatch.setattr(query_generation, "_topic_source", lambda _env: Source())
+    monkeypatch.setattr(
+        query_generation,
+        "_topic_query_chat",
+        lambda *_args, **_kwargs: (_ for _ in ()).throw(
+            AssertionError("source preview must not call the LLM")
+        ),
+    )
+    client = TestClient(app)
+
+    response = client.post(
+        "/api/query-generation/topic-table/source-preview",
+        json={"topic_build_id": 1229, "topic_id": 1392, "max_queries": 18},
+    )
+
+    assert response.status_code == 200
+    body = response.json()
+    assert body["summary"] == {
+        "field_count": 12,
+        "selected_count": 0,
+        "query_generation_completed": False,
+        "persisted": False,
+        "search_started": False,
+    }
+    assert [group["field_count"] for group in body["source_groups"]] == [1, 1, 1, 5, 3, 1]
+    assert all(
+        not field["used_for_query"]
+        for group in body["source_groups"]
+        for field in group["fields"]
+    )

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