Parcourir la source

增加寻找视频可视化

xueyiming il y a 22 heures
Parent
commit
13b961134f

+ 59 - 2
api/services/video_discovery_records.py

@@ -5,6 +5,7 @@ import json
 from typing import Any
 
 from sqlalchemy import func, or_, select
+from sqlalchemy.orm import load_only
 
 from supply_infra.db.models.video_discovery import (
     VideoDiscoveryCandidate,
@@ -56,7 +57,27 @@ def _serialize_run(row: VideoDiscoveryRun, counts: dict[str, int]) -> dict[str,
     }
 
 
-def _serialize_search(row: VideoDiscoverySearch) -> dict[str, Any]:
+def _serialize_search_candidate(row: VideoDiscoveryCandidate) -> dict[str, Any]:
+    return {
+        "id": int(row.id),
+        "aweme_id": row.aweme_id,
+        "title": row.title,
+        "content_link": row.content_link,
+        "author_name": row.author_name,
+        "decision_bucket": row.decision_bucket,
+        "play_count": int(row.play_count) if row.play_count is not None else None,
+        "like_count": int(row.like_count) if row.like_count is not None else None,
+        "relevance_score": _number(row.relevance_score),
+        "elder_score": _number(row.elder_score),
+        "share_score": _number(row.share_score),
+        "value_score": _number(row.value_score),
+    }
+
+
+def _serialize_search(
+    row: VideoDiscoverySearch,
+    candidates: list[VideoDiscoveryCandidate],
+) -> dict[str, Any]:
     return {
         "id": int(row.id),
         "run_id": row.run_id,
@@ -80,6 +101,7 @@ def _serialize_search(row: VideoDiscoverySearch) -> dict[str, Any]:
         "has_more": bool(row.has_more),
         "next_cursor": row.next_cursor,
         "result_ids": _json_value(row.result_ids_json),
+        "candidates": [_serialize_search_candidate(candidate) for candidate in candidates],
         "status": row.status,
         "error_message": row.error_message,
         "create_time": _timestamp(row.create_time),
@@ -270,8 +292,43 @@ def list_video_discovery_searches(
                 .offset(offset)
             ).all()
         )
+        candidates_by_search: dict[int, list[VideoDiscoveryCandidate]] = {
+            int(row.id): [] for row in rows
+        }
+        if candidates_by_search:
+            candidate_rows = session.scalars(
+                select(VideoDiscoveryCandidate)
+                .options(
+                    load_only(
+                        VideoDiscoveryCandidate.id,
+                        VideoDiscoveryCandidate.search_id,
+                        VideoDiscoveryCandidate.aweme_id,
+                        VideoDiscoveryCandidate.title,
+                        VideoDiscoveryCandidate.content_link,
+                        VideoDiscoveryCandidate.author_name,
+                        VideoDiscoveryCandidate.decision_bucket,
+                        VideoDiscoveryCandidate.play_count,
+                        VideoDiscoveryCandidate.like_count,
+                        VideoDiscoveryCandidate.relevance_score,
+                        VideoDiscoveryCandidate.elder_score,
+                        VideoDiscoveryCandidate.share_score,
+                        VideoDiscoveryCandidate.value_score,
+                    )
+                )
+                .where(VideoDiscoveryCandidate.search_id.in_(candidates_by_search))
+                .order_by(
+                    VideoDiscoveryCandidate.search_id,
+                    VideoDiscoveryCandidate.id,
+                )
+            ).all()
+            for candidate in candidate_rows:
+                if candidate.search_id is not None:
+                    candidates_by_search[int(candidate.search_id)].append(candidate)
         return {
-            "items": [_serialize_search(row) for row in rows],
+            "items": [
+                _serialize_search(row, candidates_by_search.get(int(row.id), []))
+                for row in rows
+            ],
             "total": total,
             "limit": limit,
             "offset": offset,

+ 8 - 3
prd/09-找视频记录展示设计.md

@@ -41,7 +41,7 @@
 - 基本身份:`run_id`、`biz_dt`、`demand_grade_id`、种子视频。
 - Agent 解释:`intent_summary`。
 - 运行结果:搜索页、全部候选、主推荐、淘汰、待评估。
-- 展开上下文:`relevant_points_json` 和 `stop_reason`。
+- 默认展开上下文:`relevant_points_json` 和 `stop_reason`,用户仍可手动收起
 
 计数由关联表实时聚合,避免只依赖运行表中的缓存计数而掩盖数据不一致。
 
@@ -56,7 +56,8 @@
 - 结果数、新增候选数、是否有下一页;
 - 创建时间。
 
-展开后展示搜索键、完整检索参数、供应方分页状态、结果视频 ID 和错误信息。
+展开后优先以卡片展示该搜索记录直接关联的候选视频,包括标题、作者、分池、播放量、
+点赞量和价值评分;同时展示搜索键、完整检索参数、供应方分页状态和错误信息。
 
 ### 3.4 候选视频
 
@@ -69,10 +70,14 @@
 - AIGC 计划标签和爬取计划;
 - 创建时间。
 
-展开后展示决策原因、标签、内容受众年龄证据、账号粉丝年龄证据、归一化结果、来源搜索和完整 AIGC 计划标识。存在内容链接时允许在新窗口打开原视频。
+展开后只展示决策原因、标签和原视频入口,不展示年龄证据、归一化结果、来源搜索或
+AIGC 计划明细。
 
 候选支持按分池以及视频 ID、标题、作者筛选。
 
+页面正文、表格、筛选控件和状态标签使用不小于 11px 的字号,主要操作与正文使用
+12px~14px,避免审计信息过小影响阅读。
+
 ## 4. 接口设计
 
 | 接口 | 用途 |

+ 5 - 0
tests/api/test_video_discovery_records.py

@@ -167,6 +167,11 @@ def test_lists_searches_and_candidates_with_filters(monkeypatch) -> None:
     assert searches["total"] == 1
     assert searches["items"][0]["provider_state"] == {"cursor": "next"}
     assert searches["items"][0]["result_ids"] == ["aweme-1", "aweme-2"]
+    assert [item["aweme_id"] for item in searches["items"][0]["candidates"]] == [
+        "aweme-1",
+        "aweme-2",
+    ]
+    assert searches["items"][0]["candidates"][0]["title"] == "教爸妈设置大字体"
 
     candidates = records_service.list_video_discovery_candidates(
         "find-001",

+ 16 - 0
web/src/types/videoDiscoveryRecords.ts

@@ -42,12 +42,28 @@ export interface VideoDiscoverySearchRecord {
   has_more: boolean
   next_cursor: string | null
   result_ids: JsonValue
+  candidates: VideoDiscoverySearchCandidate[]
   status: string
   error_message: string | null
   create_time: string | null
   update_time: string | null
 }
 
+export interface VideoDiscoverySearchCandidate {
+  id: number
+  aweme_id: string
+  title: string | null
+  content_link: string | null
+  author_name: string | null
+  decision_bucket: string
+  play_count: number | null
+  like_count: number | null
+  relevance_score: number | null
+  elder_score: number | null
+  share_score: number | null
+  value_score: number | null
+}
+
 export interface VideoDiscoveryCandidateRecord {
   id: number
   run_id: string

+ 161 - 97
web/src/views/FindAgentRecordsView.vue

@@ -381,7 +381,7 @@ function openCandidate(candidate: VideoDiscoveryCandidateRecord) {
               <span v-if="selected.demand_grade_id">需求分级 #{{ selected.demand_grade_id }}</span>
               <span v-if="selected.seed_video_id">种子视频 {{ selected.seed_video_id }}</span>
             </div>
-            <details v-if="selected.relevant_points || selected.stop_reason" class="run-context">
+            <details v-if="selected.relevant_points || selected.stop_reason" class="run-context" open>
               <summary>查看输入点位与停止原因</summary>
               <div v-if="selected.stop_reason">
                 <strong>停止原因</strong>
@@ -499,6 +499,37 @@ function openCandidate(candidate: VideoDiscoveryCandidateRecord) {
                   </tr>
                   <tr v-if="expandedSearchId === item.id" class="expanded-row">
                     <td colspan="7">
+                      <div class="search-videos">
+                        <div class="search-videos-heading">
+                          <strong>本次搜索视频</strong>
+                          <span>{{ item.candidates.length }} 个已入库候选</span>
+                        </div>
+                        <div v-if="item.candidates.length" class="search-video-grid">
+                          <article v-for="video in item.candidates" :key="video.id">
+                            <div>
+                              <span class="bucket-pill" :class="`bucket-${video.decision_bucket}`">
+                                {{ bucketLabel(video.decision_bucket) }}
+                              </span>
+                              <strong>{{ video.title || '未命名视频' }}</strong>
+                              <small>{{ video.author_name || '未知作者' }} · {{ video.aweme_id }}</small>
+                            </div>
+                            <div class="search-video-metrics">
+                              <span>{{ formatCount(video.play_count) }} 播放</span>
+                              <span>{{ formatCount(video.like_count) }} 点赞</span>
+                              <span>V {{ formatScore(video.value_score) }}</span>
+                            </div>
+                            <a
+                              v-if="video.content_link"
+                              :href="video.content_link"
+                              target="_blank"
+                              rel="noopener noreferrer"
+                            >
+                              打开视频 ↗
+                            </a>
+                          </article>
+                        </div>
+                        <p v-else>本次搜索没有关联的候选视频。</p>
+                      </div>
                       <div class="expanded-grid">
                         <div>
                           <strong>检索参数</strong>
@@ -515,10 +546,6 @@ function openCandidate(candidate: VideoDiscoveryCandidateRecord) {
                           <strong>供应方状态</strong>
                           <pre>{{ formatJson(item.provider_state) }}</pre>
                         </div>
-                        <div>
-                          <strong>结果视频 ID</strong>
-                          <pre>{{ formatJson(item.result_ids) }}</pre>
-                        </div>
                       </div>
                     </td>
                   </tr>
@@ -604,31 +631,6 @@ function openCandidate(candidate: VideoDiscoveryCandidateRecord) {
                             打开原视频 ↗
                           </button>
                         </div>
-                        <div class="evidence-grid">
-                          <details>
-                            <summary>内容受众年龄证据</summary>
-                            <pre>{{ formatJson(item.content_age_evidence) }}</pre>
-                          </details>
-                          <details>
-                            <summary>账号粉丝年龄证据</summary>
-                            <pre>{{ formatJson(item.account_age_evidence) }}</pre>
-                          </details>
-                          <details>
-                            <summary>年龄归一化结果</summary>
-                            <pre>{{ formatJson(item.age_normalization) }}</pre>
-                          </details>
-                          <details>
-                            <summary>来源与 AIGC 计划</summary>
-                            <dl>
-                              <dt>Search ID</dt><dd>{{ item.search_id || '历史记录未关联' }}</dd>
-                              <dt>来源关键词</dt><dd>{{ formatJson(item.source_keywords) }}</dd>
-                              <dt>来源搜索</dt><dd>{{ formatJson(item.source_search_ids) }}</dd>
-                              <dt>爬取计划</dt><dd>{{ item.aigc_crawler_plan_id || '—' }}</dd>
-                              <dt>生成计划</dt><dd>{{ item.aigc_produce_plan_id || '—' }}</dd>
-                              <dt>发布计划</dt><dd>{{ item.aigc_publish_plan_id || '—' }}</dd>
-                            </dl>
-                          </details>
-                        </div>
                       </div>
                     </td>
                   </tr>
@@ -680,7 +682,7 @@ function openCandidate(candidate: VideoDiscoveryCandidateRecord) {
   align-items: center;
   gap: 8px;
   color: #737b8c;
-  font-size: 9px;
+  font-size: 12px;
   font-weight: 800;
   letter-spacing: .17em;
 }
@@ -702,7 +704,7 @@ function openCandidate(candidate: VideoDiscoveryCandidateRecord) {
 .page-header p {
   margin: 0;
   color: #8a91a0;
-  font-size: 11px;
+  font-size: 14px;
 }
 
 button, input, select { font: inherit; }
@@ -723,7 +725,7 @@ button, input, select { font: inherit; }
   border: 1px solid #dfe2e8;
   background: #fff;
   color: #5d6575;
-  font-size: 10px;
+  font-size: 13px;
   font-weight: 700;
 }
 
@@ -748,7 +750,7 @@ button, input, select { font: inherit; }
 
 .filters label > span {
   color: #858c9b;
-  font-size: 9px;
+  font-size: 12px;
   font-weight: 700;
 }
 
@@ -762,7 +764,7 @@ button, input, select { font: inherit; }
   outline: none;
   background: #fff;
   color: #424a5b;
-  font-size: 10px;
+  font-size: 13px;
 }
 
 .filters input { width: 100%; padding: 0 11px; }
@@ -777,7 +779,7 @@ button, input, select { font: inherit; }
   border: 1px solid #5d5dcd;
   background: #6262d2;
   color: #fff;
-  font-size: 10px;
+  font-size: 13px;
   font-weight: 700;
 }
 
@@ -786,7 +788,7 @@ button, input, select { font: inherit; }
   border: 1px solid #dfe2e8;
   background: #fff;
   color: #7a8190;
-  font-size: 10px;
+  font-size: 13px;
 }
 
 .error-banner {
@@ -796,7 +798,7 @@ button, input, select { font: inherit; }
   border-radius: 10px;
   background: #fff5f6;
   color: #a43d4b;
-  font-size: 10px;
+  font-size: 13px;
 }
 
 .error-banner strong { margin-right: 8px; }
@@ -805,7 +807,7 @@ button, input, select { font: inherit; }
   display: grid;
   min-height: 650px;
   flex: 1;
-  grid-template-columns: 260px minmax(0, 1fr);
+  grid-template-columns: 290px minmax(0, 1fr);
   gap: 14px;
 }
 
@@ -835,8 +837,8 @@ button, input, select { font: inherit; }
 
 .panel-heading div > span,
 .panel-heading div > strong { display: block; }
-.panel-heading div > span { color: #a0a6b1; font-size: 8px; font-weight: 700; }
-.panel-heading div > strong { margin-top: 3px; font-size: 12px; }
+.panel-heading div > span { color: #a0a6b1; font-size: 11px; font-weight: 700; }
+.panel-heading div > strong { margin-top: 3px; font-size: 15px; }
 .panel-heading b {
   display: grid;
   min-width: 27px;
@@ -846,7 +848,7 @@ button, input, select { font: inherit; }
   border-radius: 7px;
   background: #f0f0ff;
   color: #5f5fcc;
-  font-size: 9px;
+  font-size: 12px;
 }
 
 .run-list {
@@ -884,10 +886,10 @@ button, input, select { font: inherit; }
   gap: 6px;
 }
 
-.run-card-top time { color: #9aa0ac; font-size: 8px; }
+.run-card-top time { color: #9aa0ac; font-size: 11px; }
 .run-card > strong {
   overflow: hidden;
-  font-size: 12px;
+  font-size: 15px;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -895,14 +897,14 @@ button, input, select { font: inherit; }
   overflow: hidden;
   color: #9ca2ae;
   font-family: ui-monospace, monospace;
-  font-size: 8px;
+  font-size: 11px;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 .run-card-meta {
   justify-content: flex-start;
   color: #7e8594;
-  font-size: 8px;
+  font-size: 11px;
 }
 .run-card-meta span + span::before { margin-right: 6px; color: #c3c7cf; content: '·'; }
 
@@ -914,7 +916,7 @@ button, input, select { font: inherit; }
   gap: 5px;
   padding: 3px 7px;
   border-radius: 999px;
-  font-size: 8px;
+  font-size: 11px;
   font-weight: 800;
 }
 .status-pill i { width: 5px; height: 5px; border-radius: 50%; }
@@ -924,7 +926,7 @@ button, input, select { font: inherit; }
 .status-finished i { background: #31aa80; }
 .status-failed { background: #fff0f1; color: #af4652; }
 .status-failed i { background: #d25563; }
-.status-pill.large { padding: 5px 9px; font-size: 9px; }
+.status-pill.large { padding: 5px 9px; font-size: 12px; }
 
 .detail-panel { overflow: hidden; }
 .run-summary {
@@ -941,9 +943,9 @@ button, input, select { font: inherit; }
   justify-content: space-between;
   gap: 15px;
 }
-.summary-title > div > span { color: #9298a5; font-size: 9px; font-weight: 700; }
-.summary-title h2 { margin: 4px 0 0; font-size: 20px; letter-spacing: -.03em; }
-.intent { max-width: 800px; margin: 11px 0 10px; color: #586071; font-size: 10px; line-height: 1.7; }
+.summary-title > div > span { color: #9298a5; font-size: 12px; font-weight: 700; }
+.summary-title h2 { margin: 4px 0 0; font-size: 22px; letter-spacing: -.03em; }
+.intent { max-width: 800px; margin: 11px 0 10px; color: #586071; font-size: 13px; line-height: 1.7; }
 .intent.muted { color: #9ca2ad; }
 .identity-row { display: flex; flex-wrap: wrap; gap: 8px; }
 .identity-row code, .identity-row span {
@@ -951,9 +953,9 @@ button, input, select { font: inherit; }
   border-radius: 6px;
   background: #f0f2f5;
   color: #707787;
-  font-size: 8px;
+  font-size: 11px;
 }
-.run-context { margin-top: 10px; font-size: 9px; }
+.run-context { margin-top: 10px; font-size: 12px; }
 .run-context summary { color: #6262cc; cursor: pointer; }
 .run-context strong { display: block; margin-top: 8px; color: #606878; }
 .run-context p { margin: 4px 0; color: #737b89; }
@@ -966,7 +968,7 @@ pre {
   border-radius: 8px;
   background: #f8f9fb;
   color: #596173;
-  font: 8px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
+  font: 11px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
   white-space: pre-wrap;
   word-break: break-word;
 }
@@ -978,9 +980,9 @@ pre {
   border-radius: 10px;
   background: rgba(255,255,255,.8);
 }
-.metric-grid span { display: block; color: #9298a5; font-size: 8px; font-weight: 700; }
+.metric-grid span { display: block; color: #9298a5; font-size: 11px; font-weight: 700; }
 .metric-grid strong { display: block; margin-top: 5px; font-size: 19px; letter-spacing: -.04em; }
-.metric-grid strong small { color: #999fac; font-size: 11px; }
+.metric-grid strong small { color: #999fac; font-size: 14px; }
 .metric-grid .metric-primary { border-color: #cceadf; background: #f4fbf8; }
 .metric-primary strong { color: #248062; }
 .metric-grid .metric-rejected { border-color: #f0dadd; background: #fff9f9; }
@@ -1002,12 +1004,12 @@ pre {
   border-bottom: 2px solid transparent;
   background: transparent;
   color: #858c9a;
-  font-size: 10px;
+  font-size: 13px;
   font-weight: 700;
   cursor: pointer;
 }
 .tabs button.active { border-bottom-color: #6262d2; color: #4f50bd; }
-.tabs b { margin-left: 4px; color: #a1a6b1; font-size: 8px; }
+.tabs b { margin-left: 4px; color: #a1a6b1; font-size: 11px; }
 
 .detail-filters {
   display: flex;
@@ -1046,30 +1048,30 @@ pre {
   border-radius: 8px;
   background: #fff;
   color: #646c7b;
-  font-size: 9px;
+  font-size: 12px;
   font-weight: 700;
   cursor: pointer;
 }
-.result-count { margin-left: auto; color: #939aa7; font-size: 9px; }
+.result-count { margin-left: auto; color: #939aa7; font-size: 12px; }
 
 .table-wrap { width: 100%; overflow-x: auto; }
-table { width: 100%; min-width: 920px; border-collapse: collapse; table-layout: fixed; }
+table { width: 100%; min-width: 1080px; border-collapse: collapse; table-layout: fixed; }
 th {
-  height: 36px;
+  height: 42px;
   padding: 0 10px;
   border-bottom: 1px solid #e8eaee;
   background: #fff;
   color: #9298a5;
-  font-size: 8px;
+  font-size: 11px;
   font-weight: 700;
   text-align: left;
 }
 td {
-  height: 64px;
+  height: 74px;
   padding: 9px 10px;
   border-bottom: 1px solid #eff0f3;
   color: #5b6373;
-  font-size: 9px;
+  font-size: 12px;
   vertical-align: middle;
 }
 tbody > tr:not(.expanded-row):hover { background: #fafafe; }
@@ -1077,7 +1079,7 @@ td strong, td small { display: block; }
 td strong {
   overflow: hidden;
   color: #41495a;
-  font-size: 9px;
+  font-size: 12px;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -1086,37 +1088,37 @@ td small {
   margin-top: 4px;
   overflow: hidden;
   color: #9096a3;
-  font-size: 8px;
+  font-size: 11px;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
-.search-table th:nth-child(1), .search-table td:nth-child(1) { width: 86px; }
+.search-table th:nth-child(1), .search-table td:nth-child(1) { width: 100px; }
 .search-table th:nth-child(2), .search-table td:nth-child(2) { width: 25%; }
 .search-table th:nth-child(3), .search-table td:nth-child(3) { width: 15%; }
 .search-table th:nth-child(4), .search-table td:nth-child(4) { width: 16%; }
 .search-table th:nth-child(5), .search-table td:nth-child(5) { width: 14%; }
-.search-table th:nth-child(6), .search-table td:nth-child(6) { width: 112px; }
-.search-table th:nth-child(7), .search-table td:nth-child(7) { width: 53px; }
-.candidate-table th:nth-child(1), .candidate-table td:nth-child(1) { width: 89px; }
+.search-table th:nth-child(6), .search-table td:nth-child(6) { width: 130px; }
+.search-table th:nth-child(7), .search-table td:nth-child(7) { width: 64px; }
+.candidate-table th:nth-child(1), .candidate-table td:nth-child(1) { width: 105px; }
 .candidate-table th:nth-child(2), .candidate-table td:nth-child(2) { width: 24%; }
 .candidate-table th:nth-child(3), .candidate-table td:nth-child(3) { width: 19%; }
 .candidate-table th:nth-child(4), .candidate-table td:nth-child(4) { width: 20%; }
 .candidate-table th:nth-child(5), .candidate-table td:nth-child(5) { width: 13%; }
-.candidate-table th:nth-child(6), .candidate-table td:nth-child(6) { width: 112px; }
-.candidate-table th:nth-child(7), .candidate-table td:nth-child(7) { width: 53px; }
-.wide-cell strong { font-size: 10px; }
+.candidate-table th:nth-child(6), .candidate-table td:nth-child(6) { width: 130px; }
+.candidate-table th:nth-child(7), .candidate-table td:nth-child(7) { width: 64px; }
+.wide-cell strong { font-size: 13px; }
 .record-id {
   display: block;
   margin-bottom: 5px;
   color: #858c9a;
   font-family: ui-monospace, monospace;
-  font-size: 8px;
+  font-size: 11px;
 }
 .mini-status, .source-pill {
   display: inline-block;
   padding: 2px 6px;
   border-radius: 5px;
-  font-size: 8px;
+  font-size: 11px;
   font-weight: 700;
 }
 .mini-status.ok { background: #eaf8f2; color: #278365; }
@@ -1132,7 +1134,7 @@ td small {
   border-radius: 5px;
   background: #f1f2f5;
   color: #687080;
-  font-size: 7px;
+  font-size: 10px;
 }
 .score-line .value-score { background: #ececff; color: #5758c2; }
 .expand-button {
@@ -1140,13 +1142,79 @@ td small {
   border: 0;
   background: transparent;
   color: #5e60c8;
-  font-size: 8px;
+  font-size: 11px;
   font-weight: 700;
   cursor: pointer;
 }
 .expanded-row > td { height: auto; padding: 13px; background: #f8f9fc; }
-.expanded-grid { display: grid; grid-template-columns: .8fr 1fr 1fr; gap: 12px; }
-.expanded-grid > div, .decision-block, .evidence-grid details {
+.search-videos {
+  margin-bottom: 12px;
+  padding: 13px;
+  border: 1px solid #dfe2ea;
+  border-radius: 10px;
+  background: #fff;
+}
+.search-videos-heading {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10px;
+}
+.search-videos-heading strong { color: #424a5b; font-size: 14px; }
+.search-videos-heading span,
+.search-videos > p { margin: 0; color: #8b92a0; font-size: 12px; }
+.search-video-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 8px;
+}
+.search-video-grid article {
+  display: grid;
+  min-width: 0;
+  padding: 11px;
+  grid-template-columns: minmax(0, 1fr) auto;
+  gap: 8px 12px;
+  border: 1px solid #e7e9ee;
+  border-radius: 9px;
+  background: #fafbfc;
+}
+.search-video-grid article > div:first-child { min-width: 0; }
+.search-video-grid article strong,
+.search-video-grid article small { display: block; }
+.search-video-grid article strong {
+  margin-top: 7px;
+  overflow: hidden;
+  color: #3f4758;
+  font-size: 13px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.search-video-grid article small {
+  margin-top: 4px;
+  overflow: hidden;
+  color: #858c9a;
+  font-size: 11px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.search-video-metrics {
+  display: grid;
+  align-content: center;
+  gap: 3px;
+  color: #737b8a;
+  font-size: 11px;
+  text-align: right;
+  white-space: nowrap;
+}
+.search-video-grid article a {
+  width: fit-content;
+  color: #5b5dc7;
+  font-size: 12px;
+  font-weight: 700;
+  text-decoration: none;
+}
+.expanded-grid { display: grid; grid-template-columns: .8fr 1fr; gap: 12px; }
+.expanded-grid > div, .decision-block {
   min-width: 0;
   padding: 11px;
   border: 1px solid #e3e6ec;
@@ -1155,22 +1223,22 @@ td small {
 }
 .expanded-grid > div > strong, .decision-block > strong {
   color: #4b5363;
-  font-size: 9px;
+  font-size: 12px;
 }
 dl {
   display: grid;
   grid-template-columns: 78px minmax(0, 1fr);
   gap: 6px;
   margin: 8px 0 0;
-  font-size: 8px;
+  font-size: 11px;
 }
 dt { color: #9ba1ad; }
 dd { margin: 0; overflow-wrap: anywhere; color: #606878; }
 .error-text { color: #ad4350; }
-.candidate-detail { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 12px; }
-.decision-block p { color: #697181; font-size: 9px; line-height: 1.7; }
+.candidate-detail { display: block; }
+.decision-block p { color: #697181; font-size: 12px; line-height: 1.7; }
 .tag-row { display: flex; flex-wrap: wrap; gap: 5px; }
-.tag-row span { padding: 3px 6px; border-radius: 5px; background: #f0f1f5; color: #717887; font-size: 8px; }
+.tag-row span { padding: 3px 6px; border-radius: 5px; background: #f0f1f5; color: #717887; font-size: 11px; }
 .link-button {
   margin-top: 10px;
   padding: 5px 8px;
@@ -1178,13 +1246,9 @@ dd { margin: 0; overflow-wrap: anywhere; color: #606878; }
   border-radius: 6px;
   background: #f5f5ff;
   color: #5a5cc5;
-  font-size: 8px;
+  font-size: 11px;
   cursor: pointer;
 }
-.evidence-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
-.evidence-grid details { padding: 9px; }
-.evidence-grid summary { color: #5e6676; font-size: 8px; font-weight: 700; cursor: pointer; }
-
 .state {
   display: flex;
   min-height: 190px;
@@ -1192,11 +1256,11 @@ dd { margin: 0; overflow-wrap: anywhere; color: #606878; }
   justify-content: center;
   gap: 8px;
   color: #8d94a2;
-  font-size: 10px;
+  font-size: 13px;
 }
 .state.empty { flex-direction: column; }
-.state.empty strong { color: #666e7d; font-size: 11px; }
-.state.empty span { color: #9ba1ad; font-size: 9px; }
+.state.empty strong { color: #666e7d; font-size: 14px; }
+.state.empty span { color: #9ba1ad; font-size: 12px; }
 .loader {
   width: 13px;
   height: 13px;
@@ -1213,7 +1277,7 @@ dd { margin: 0; overflow-wrap: anywhere; color: #606878; }
   padding: 8px 13px;
   border-top: 1px solid #eaecf0;
   color: #8b92a0;
-  font-size: 9px;
+  font-size: 12px;
 }
 .pagination div { display: flex; gap: 6px; }
 .pagination button {
@@ -1223,7 +1287,7 @@ dd { margin: 0; overflow-wrap: anywhere; color: #606878; }
   border-radius: 7px;
   background: #fff;
   color: #656d7d;
-  font-size: 8px;
+  font-size: 11px;
   cursor: pointer;
 }
 .pagination button:disabled { opacity: .4; cursor: default; }
@@ -1238,8 +1302,8 @@ dd { margin: 0; overflow-wrap: anywhere; color: #606878; }
   color: #9299a6;
 }
 .no-selection > span { font-size: 32px; color: #7778d4; }
-.no-selection strong { margin-top: 9px; color: #596171; font-size: 13px; }
-.no-selection p { margin: 5px 0; font-size: 9px; }
+.no-selection strong { margin-top: 9px; color: #596171; font-size: 16px; }
+.no-selection p { margin: 5px 0; font-size: 12px; }
 
 @media (max-width: 1180px) {
   .run-summary { grid-template-columns: 1fr; }