Просмотр исходного кода

Show complete topic query source data

SamLee 6 дней назад
Родитель
Сommit
8826ce7554

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/frontend/dist/assets/index-Bxmn3oKF.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/frontend/dist/assets/index-DZEE1jrB.js


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

@@ -4,8 +4,8 @@
     <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-Dj16zM9i.js"></script>
-    <link rel="stylesheet" crossorigin href="/app/assets/index-Bv56Fb5D.css">
+    <script type="module" crossorigin src="/app/assets/index-DZEE1jrB.js"></script>
+    <link rel="stylesheet" crossorigin href="/app/assets/index-Bxmn3oKF.css">
   </head>
   <body>
     <div id="root"></div>

+ 63 - 29
app/frontend/src/features/query-board/TopicTableQueryPanel.jsx

@@ -91,6 +91,11 @@ function PromptModal({ payload, loading, error, onClose }) {
 
 function GenerationRows({ result }) {
   const groups = result.source_groups || []
+  const [openGroups, setOpenGroups] = useState(() => ({ unique_hook: true }))
+
+  const toggleGroup = (key) => {
+    setOpenGroups((current) => ({ ...current, [key]: !current[key] }))
+  }
 
   return (
     <section className="topic-route-map">
@@ -100,35 +105,64 @@ function GenerationRows({ result }) {
         <strong>生成哪些具体搜索词</strong>
       </div>
       <div className="topic-route-map-body">
-        {groups.map((group, groupIndex) => (
-          <section className="topic-source-group" key={group.key}>
-            <header className="topic-source-group-head">
-              <span>第 {groupIndex + 1} 种数据来源</span>
-              <strong>{group.label}</strong>
-              <small>{group.source_description}</small>
-            </header>
-            {(group.field_mappings || []).map((row) => (
-              <article className="topic-route-row" key={row.field_key}>
-                <div className="topic-route-input">
-                  <span>{row.field_label}</span>
-                  <code>{row.field_path}</code>
-                  <p>{row.field_value}</p>
-                </div>
-                <div className="topic-route-judgement">
-                  <p>{row.judgement}</p>
-                </div>
-                <div className="topic-route-queries">
-                  {row.queries.map((query, index) => (
-                    <div key={`${query}-${index}`}>
-                      <b>{index + 1}</b>
-                      <p>{query}</p>
-                    </div>
-                  ))}
-                </div>
-              </article>
-            ))}
-          </section>
-        ))}
+        {groups.map((group, groupIndex) => {
+          const rows = group.fields || group.field_mappings || []
+          const selectedCount = group.selected_field_count ?? rows.filter((row) => row.used_for_query || row.queries?.length).length
+          const historicalCount = group.historical_field_count || 0
+          const isOpen = Boolean(openGroups[group.key])
+          return (
+            <section className={`topic-source-group${isOpen ? ' is-open' : ''}`} key={group.key}>
+              <button
+                className="topic-source-group-toggle"
+                type="button"
+                onClick={() => toggleGroup(group.key)}
+                aria-expanded={isOpen}
+              >
+                <span className="topic-source-index">来源 {groupIndex + 1}</span>
+                <span className="topic-source-title">
+                  <strong>{group.label}</strong>
+                  <small>{group.source_description}</small>
+                </span>
+                <span className="topic-source-counts">
+                  <b>{group.field_count ?? rows.length} 个真实字段</b>
+                  <em>{selectedCount} 个用于生成</em>
+                  {historicalCount > 0 && <i>{historicalCount} 个历史关系</i>}
+                </span>
+                <span className="topic-source-chevron">{isOpen ? '收起' : '展开'} <b>⌄</b></span>
+              </button>
+              <div className="topic-source-group-fields" hidden={!isOpen}>
+                {rows.map((row) => {
+                  const usedForQuery = row.used_for_query ?? Boolean(row.queries?.length)
+                  return (
+                    <article className={`topic-route-row${usedForQuery ? ' is-selected' : ''}`} key={row.field_key}>
+                      <div className="topic-route-input">
+                        <span>{row.field_label}</span>
+                        {row.is_current === false && <span className="topic-history-tag">历史构建关系</span>}
+                        <code>{row.field_path}</code>
+                        <p>{row.field_value}</p>
+                      </div>
+                      <div className="topic-route-judgement">
+                        {usedForQuery
+                          ? <p>{row.judgement}</p>
+                          : <p className="topic-unused-copy">本次未使用这个字段生成搜索词</p>}
+                      </div>
+                      <div className="topic-route-queries">
+                        {usedForQuery
+                          ? row.queries.map((query, index) => (
+                            <div key={`${query}-${index}`}>
+                              <b>{index + 1}</b>
+                              <p>{query}</p>
+                            </div>
+                          ))
+                          : <p className="topic-unused-copy">暂无对应搜索词</p>}
+                      </div>
+                    </article>
+                  )
+                })}
+              </div>
+            </section>
+          )
+        })}
       </div>
     </section>
   )

+ 115 - 10
app/frontend/src/styles/query-board.css

@@ -811,34 +811,117 @@
   border-top: 2px solid #d8d1eb;
 }
 
-.topic-source-group-head {
-  display: flex;
-  align-items: baseline;
-  gap: 10px;
-  padding: 10px 18px;
-  border-bottom: 1px solid #e5e1ed;
+.topic-source-group-toggle {
+  display: grid;
+  grid-template-columns: auto minmax(220px, 1fr) auto auto;
+  align-items: center;
+  gap: 12px;
+  width: 100%;
+  padding: 12px 18px;
+  border: 0;
+  color: inherit;
+  text-align: left;
   background: #f7f4ff;
+  cursor: pointer;
+}
+
+.topic-source-group-toggle:hover {
+  background: #f1edfc;
+}
+
+.topic-source-group-toggle:focus-visible {
+  position: relative;
+  z-index: 2;
+  outline: 2px solid #7256ce;
+  outline-offset: -2px;
 }
 
-.topic-source-group-head span {
+.topic-source-index {
+  padding: 3px 7px;
+  border-radius: 5px;
   color: #7159c7;
+  background: #eae4fb;
   font-size: 10px;
   font-weight: 850;
 }
 
-.topic-source-group-head strong {
+.topic-source-title {
+  display: grid;
+  gap: 2px;
+}
+
+.topic-source-title strong {
   color: #312a45;
   font-size: 12px;
 }
 
-.topic-source-group-head small {
+.topic-source-title small {
   color: #847d91;
   font-size: 10px;
 }
 
+.topic-source-counts {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 6px;
+}
+
+.topic-source-counts b,
+.topic-source-counts em,
+.topic-source-counts i {
+  padding: 3px 7px;
+  border-radius: 999px;
+  font-size: 9px;
+  font-style: normal;
+  font-weight: 750;
+  white-space: nowrap;
+}
+
+.topic-source-counts b {
+  color: #544c64;
+  background: #e9e5ef;
+}
+
+.topic-source-counts em {
+  color: #197152;
+  background: #dff5e9;
+}
+
+.topic-source-counts i {
+  color: #986417;
+  background: #fff0cf;
+}
+
+.topic-source-chevron {
+  color: #625b70;
+  font-size: 10px;
+  font-weight: 750;
+}
+
+.topic-source-chevron b {
+  display: inline-block;
+  font-size: 13px;
+  transition: transform 160ms ease;
+}
+
+.topic-source-group.is-open .topic-source-chevron b {
+  transform: rotate(180deg);
+}
+
+.topic-source-group-fields {
+  max-height: 620px;
+  overflow: auto;
+  border-top: 1px solid #e5e1ed;
+}
+
 .topic-route-row {
   position: relative;
-  border-left: 4px solid #7c5ce5;
+  border-left: 4px solid #d7d2e2;
+}
+
+.topic-route-row.is-selected {
+  border-left-color: #7c5ce5;
 }
 
 .topic-route-row + .topic-route-row {
@@ -868,6 +951,12 @@
   font-weight: 850;
 }
 
+.topic-route-input > .topic-history-tag {
+  margin-left: 5px;
+  color: #986417;
+  background: #fff0cf;
+}
+
 .topic-route-input > p {
   margin: 9px 0 0;
   color: #332e3f;
@@ -947,6 +1036,12 @@
   line-height: 1.5;
 }
 
+.topic-route-judgement .topic-unused-copy,
+.topic-route-queries > .topic-unused-copy {
+  color: #96909f;
+  font-weight: 500;
+}
+
 @media (max-width: 1180px) {
   .topic-table-form {
     grid-template-columns: repeat(3, minmax(120px, 1fr));
@@ -960,6 +1055,16 @@
     display: none;
   }
 
+  .topic-source-group-toggle {
+    grid-template-columns: auto 1fr auto;
+  }
+
+  .topic-source-counts {
+    grid-column: 2 / -1;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+  }
+
   .topic-example-map,
   .topic-route-row {
     grid-template-columns: 1fr;

+ 65 - 8
query_planning/topic_table.py

@@ -58,6 +58,7 @@ class TopicTablePreview:
     result: GenerationResult
     source: dict[str, Any]
     topic: dict[str, Any]
+    field_catalog: list[dict[str, Any]]
     llm_output: dict[str, Any]
 
 
@@ -181,6 +182,7 @@ def _field_catalog(snapshot: dict[str, Any]) -> list[dict[str, Any]]:
         route: str,
         point_id: Any = None,
         item_ids: list[Any] | None = None,
+        is_current: bool = True,
     ) -> None:
         value = str(field_value or "").strip()
         if not value:
@@ -195,6 +197,7 @@ def _field_catalog(snapshot: dict[str, Any]) -> list[dict[str, Any]]:
                 "source_group_label": ROUTES[route]["label"],
                 "point_id": point_id,
                 "item_ids": list(item_ids or []),
+                "is_current": is_current,
             }
         )
 
@@ -262,21 +265,26 @@ def _field_catalog(snapshot: dict[str, Any]) -> list[dict[str, Any]]:
                 route="evidence_gap",
                 item_ids=[item_id],
             )
+    active_item_ids = {
+        item.get("id") for item in topic.get("composition_items") or []
+    }
     for relation_index, relation in enumerate(topic.get("item_relations") or []):
+        relation_item_ids = [
+            value
+            for value in (
+                relation.get("source_item_id"),
+                relation.get("target_item_id"),
+            )
+            if value is not None
+        ]
         add(
             field_key=f"relation:{relation_index}:reason",
             field_path=f"topics[id={topic_id}].item_relations[{relation_index}].reason",
             field_label="元素关系说明",
             field_value=relation.get("reason"),
             route="cross_dimension",
-            item_ids=[
-                value
-                for value in (
-                    relation.get("source_item_id"),
-                    relation.get("target_item_id"),
-                )
-                if value is not None
-            ],
+            item_ids=relation_item_ids,
+            is_current=all(item_id in active_item_ids for item_id in relation_item_ids),
         )
     return fields
 
@@ -519,6 +527,7 @@ def generate_topic_table_preview(
             "source_url": source_payload.get("source_url"),
         },
         topic=snapshot["topic"],
+        field_catalog=field_catalog,
         llm_output=llm_output,
     )
 
@@ -553,6 +562,14 @@ def preview_to_dict(preview: TopicTablePreview) -> dict[str, Any]:
         for need_key in need_keys:
             if need_key:
                 queries_by_need.setdefault(need_key, []).append(query.query_text)
+    selected_by_field_key = {
+        str(need.source_ref.get("field_key")): {
+            "judgement": need.decision_context,
+            "queries": queries_by_need.get(need.need_key, []),
+        }
+        for need in result.knowledge_needs
+        if need.source_ref.get("field_key")
+    }
     return {
         "generator_kind": GeneratorKind.TOPIC_TABLE.value,
         "source": preview.source,
@@ -598,6 +615,46 @@ def preview_to_dict(preview: TopicTablePreview) -> dict[str, Any]:
                 "key": route,
                 "label": config["label"],
                 "source_description": config["source_description"],
+                "field_count": sum(
+                    1
+                    for field in preview.field_catalog
+                    if field["source_group_key"] == route
+                ),
+                "selected_field_count": sum(
+                    1
+                    for field in preview.field_catalog
+                    if field["source_group_key"] == route
+                    and field["field_key"] in selected_by_field_key
+                    and selected_by_field_key[field["field_key"]]["queries"]
+                ),
+                "historical_field_count": sum(
+                    1
+                    for field in preview.field_catalog
+                    if field["source_group_key"] == route
+                    and 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": bool(
+                            selected_by_field_key.get(field["field_key"], {}).get(
+                                "queries"
+                            )
+                        ),
+                        "judgement": selected_by_field_key.get(
+                            field["field_key"], {}
+                        ).get("judgement"),
+                        "queries": selected_by_field_key.get(
+                            field["field_key"], {}
+                        ).get("queries", []),
+                    }
+                    for field in preview.field_catalog
+                    if field["source_group_key"] == route
+                ],
                 "field_mappings": [
                     {
                         "field_key": need.source_ref.get("field_key"),

+ 10 - 0
tests/test_topic_table_query_generation.py

@@ -196,6 +196,16 @@ def test_topic_table_generation_maps_real_fields_and_uses_unified_budget():
     assert data["summary"]["search_started"] is False
     assert len(data["source_groups"]) == 6
     assert all(group["field_mappings"] for group in data["source_groups"])
+    assert [group["field_count"] for group in data["source_groups"]] == [1, 1, 1, 5, 3, 1]
+    assert sum(len(group["fields"]) for group in data["source_groups"]) == 12
+    topic_result_field = next(
+        field
+        for field in data["source_groups"][3]["fields"]
+        if field["field_key"] == "topic.result"
+    )
+    assert topic_result_field["used_for_query"] is False
+    assert topic_result_field["judgement"] is None
+    assert topic_result_field["queries"] == []
     assert data["topic"]["dimensions"] == {
         "实质": ["羊"],
         "形式": ["低角度仰拍"],

Некоторые файлы не были показаны из-за большого количества измененных файлов