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

feat: 视频内容理解-旧 + 视频标题维度召回按钮

- 视频内容理解 → 视频内容理解-旧 (标题改名)
- AIUnderstandingPanel 每行加"以此召回"按钮 (RESULT_LOG_TOPIC/THEME/KEYWORDS/NARRATION)
  字典无该 code 或字段为空时 disabled + tooltip
- 视频详情卡"标题"行加"以此召回"按钮 (VIDEO_TITLE 维度) + "召回维度" Tag
- DeconstructTree 样式常量 export 出来共享, 三处按钮宽度/配色一致
- 复检全部维度召回: 已覆盖后端字典 9 个 configCode, textsForConfigCode 映射完整
刘立冬 2 дней назад
Родитель
Сommit
bfbb47b3a2

+ 91 - 35
src/components/AIUnderstandingPanel.tsx

@@ -1,56 +1,112 @@
-import { Alert, Descriptions, Empty, Typography } from 'antd'
+import { Button, Empty, Space, Tooltip, Typography } from 'antd'
+import { ThunderboltOutlined } from '@ant-design/icons'
 import type { AIUnderstandingVO } from '../api/types'
+import { ACTIVE_PARENT_BTN_STYLE, DISABLED_PARENT_BTN_STYLE } from './DeconstructTree'
 
-const { Paragraph } = Typography
+const { Paragraph, Text } = Typography
 
 interface Props {
   data: AIUnderstandingVO | null
   loading?: boolean
+  /** 后端字典: 仅当对应 configCode 在字典里时, "以此召回"按钮才生效 */
+  configCodes: Record<string, string>
+  onRecallByText: (text: string, configCodeOverride?: string) => void
 }
 
 /**
- * AI理解结果展示
- *
- * 数据来源 ODPS loghubods.result_log → DataWorks 同步Job → 本地表 video_ai_understanding。
- * MVP 期间表为空,真实查询返回 null,这里渲染"未就绪"占位。严禁伪造内容。
+ * 字段 → configCode 映射
+ * 4 个 RESULT_LOG_* 维度对应内容理解的 4 行
  */
-export default function AIUnderstandingPanel({ data, loading }: Props) {
-  if (loading) {
-    return <div>加载中...</div>
-  }
+const ROW_CODE: Array<{ label: string; field: keyof AIUnderstandingVO; code: string }> = [
+  { label: '内容选题', field: 'contentTopic', code: 'RESULT_LOG_TOPIC' },
+  { label: '视频主题', field: 'videoTheme', code: 'RESULT_LOG_THEME' },
+  { label: '视频关键词', field: 'videoKeywords', code: 'RESULT_LOG_KEYWORDS' },
+  { label: '视频口播', field: 'videoNarration', code: 'RESULT_LOG_NARRATION' },
+]
 
-  if (!data) {
-    return <Empty description="准备中" image={Empty.PRESENTED_IMAGE_SIMPLE} />
-  }
+/**
+ * 视频内容理解-旧 (RESULT_LOG_* 维度)
+ *
+ * 数据源: video:detail Redis (loghubods.video_dimension_detail_add_column 同步而来)
+ * 视频口播 字段不在 ODPS 维度表里, 始终为 null → 按钮 disabled
+ */
+export default function AIUnderstandingPanel({ data, loading, configCodes, onRecallByText }: Props) {
+  if (loading) return <div>加载中...</div>
+  if (!data) return <Empty description="准备中" image={Empty.PRESENTED_IMAGE_SIMPLE} />
 
-  const items = [
-    { label: '内容选题', value: data.contentTopic },
-    { label: '视频主题', value: data.videoTheme },
-    { label: '视频关键词', value: data.videoKeywords },
-    { label: '视频口播', value: data.videoNarration },
-  ]
+  const rows = ROW_CODE.map(({ label, field, code }) => ({
+    label,
+    code,
+    value: ((data[field] as string | undefined | null) ?? '').toString().trim(),
+  }))
 
-  const allEmpty = items.every((i) => !i.value)
-  if (allEmpty) {
+  if (rows.every((r) => !r.value)) {
     return <Empty description="该视频AI理解结果为空" />
   }
 
   return (
-    <Descriptions column={1} bordered size="small">
-      {items.map(({ label, value }) => (
-        <Descriptions.Item key={label} label={label}>
-          {value ? (
-            <Paragraph style={{ marginBottom: 0 }}>{value}</Paragraph>
-          ) : (
-            <Typography.Text type="secondary">-</Typography.Text>
-          )}
-        </Descriptions.Item>
-      ))}
+    <Space direction="vertical" size={8} style={{ width: '100%' }}>
+      {rows.map(({ label, value, code }) => {
+        const dictHas = code in configCodes
+        const supported = dictHas && !!value
+        const tip = !dictHas
+          ? `当前未启用"${label}"维度向量召回 (${code} 不在后端字典)`
+          : !value
+            ? '该字段无内容,无法召回'
+            : ''
+        const btn = supported ? (
+          <Button
+            type="primary"
+            icon={<ThunderboltOutlined />}
+            onClick={() => onRecallByText(value, code)}
+            style={ACTIVE_PARENT_BTN_STYLE}
+          >
+            以此召回
+          </Button>
+        ) : (
+          <Tooltip title={tip}>
+            <Button disabled icon={<ThunderboltOutlined />} style={DISABLED_PARENT_BTN_STYLE}>
+              以此召回
+            </Button>
+          </Tooltip>
+        )
+        return (
+          <div
+            key={label}
+            style={{
+              display: 'flex',
+              alignItems: 'center',
+              gap: 12,
+              padding: '8px 12px',
+              border: value ? '1px solid #ffd591' : '1px solid #e8e8e8',
+              background: value ? '#fff7e6' : '#fafafa',
+              borderRadius: 6,
+            }}
+          >
+            <Text strong style={{ minWidth: 76, fontSize: 12, color: '#d46b08', flexShrink: 0 }}>
+              {label}
+            </Text>
+            <div style={{ flex: 1, minWidth: 0, fontSize: 13 }}>
+              {value ? (
+                <Paragraph
+                  style={{ marginBottom: 0 }}
+                  ellipsis={{ rows: 3, tooltip: value }}
+                >
+                  {value}
+                </Paragraph>
+              ) : (
+                <Text type="secondary">-</Text>
+              )}
+            </div>
+            {btn}
+          </div>
+        )
+      })}
       {data.dt && (
-        <Descriptions.Item label="数据分区">
-          <Typography.Text type="secondary">{data.dt}</Typography.Text>
-        </Descriptions.Item>
+        <Text type="secondary" style={{ fontSize: 11 }}>
+          数据分区: {data.dt}
+        </Text>
       )}
-    </Descriptions>
+    </Space>
   )
 }

+ 7 - 7
src/components/DeconstructTree.tsx

@@ -31,11 +31,11 @@ const UNSUPPORTED_WORD_TIP = '暂不支持词级召回(尚未配置词级向量
  * 子级(实质词召回)用 size="small" → 字体更小,视觉退一级
  * 两者宽度都=110, 都带闪电图标, 右对齐
  */
-const BTN_WIDTH = 110
+export const RECALL_BTN_WIDTH = 110
 
-/** 父级生效: 绿色实色 */
-const ACTIVE_PARENT_BTN_STYLE: React.CSSProperties = {
-  width: BTN_WIDTH,
+/** 父级生效: 绿色实色 — 对外暴露给 AIUnderstandingPanel/视频详情共享, 保证三处样式一致 */
+export const ACTIVE_PARENT_BTN_STYLE: React.CSSProperties = {
+  width: RECALL_BTN_WIDTH,
   background: '#52c41a',
   borderColor: '#52c41a',
   color: '#fff',
@@ -43,14 +43,14 @@ const ACTIVE_PARENT_BTN_STYLE: React.CSSProperties = {
 }
 
 /** 父级 disabled: 灰色 */
-const DISABLED_PARENT_BTN_STYLE: React.CSSProperties = {
-  width: BTN_WIDTH,
+export const DISABLED_PARENT_BTN_STYLE: React.CSSProperties = {
+  width: RECALL_BTN_WIDTH,
   flexShrink: 0,
 }
 
 /** 子级 disabled: 字体小,灰色 */
 const DISABLED_CHILD_BTN_STYLE: React.CSSProperties = {
-  width: BTN_WIDTH,
+  width: RECALL_BTN_WIDTH,
   flexShrink: 0,
   fontSize: 12,
 }

+ 57 - 7
src/pages/RecallTestPage.tsx

@@ -24,10 +24,14 @@ import {
   ApartmentOutlined,
   BulbOutlined,
   VideoCameraOutlined,
+  ThunderboltOutlined,
 } from '@ant-design/icons'
 import RecallResultList from '../components/RecallResultList'
 import VideoPlayer from '../components/VideoPlayer'
-import DeconstructTree from '../components/DeconstructTree'
+import DeconstructTree, {
+  ACTIVE_PARENT_BTN_STYLE,
+  DISABLED_PARENT_BTN_STYLE,
+} from '../components/DeconstructTree'
 import AIUnderstandingPanel from '../components/AIUnderstandingPanel'
 import { toHttps } from '../utils/url'
 import {
@@ -428,25 +432,30 @@ function VideoIdTab() {
               </Card>
             </Col>
 
-            {/* 视频内容理解 - 放第二位 */}
+            {/* 视频内容理解-旧 - 放第二位 */}
             <Col xs={24} md={8}>
               <Card
                 size="small"
                 title={
                   <Space size={8}>
                     <BulbOutlined style={{ color: '#fa8c16', fontSize: 18 }} />
-                    <span style={{ fontSize: 17, fontWeight: 700 }}>视频内容理解</span>
+                    <span style={{ fontSize: 17, fontWeight: 700 }}>视频内容理解-旧</span>
                     <Tag color="orange-inverse" style={{ marginLeft: 4 }}>召回维度</Tag>
                   </Space>
                 }
                 loading={loadingMain}
                 style={{ height: '100%' }}
               >
-                <AIUnderstandingPanel data={ai} loading={loadingMain} />
+                <AIUnderstandingPanel
+                  data={ai}
+                  loading={loadingMain}
+                  configCodes={configCodes}
+                  onRecallByText={onRecallByText}
+                />
               </Card>
             </Col>
 
-            {/* 视频详情 + 播放器 - 放第三位 */}
+            {/* 视频详情 + 播放器 - 放第三位 (标题作为 VIDEO_TITLE 召回维度) */}
             <Col xs={24} md={8}>
               <Card
                 size="small"
@@ -454,6 +463,7 @@ function VideoIdTab() {
                   <Space size={8}>
                     <VideoCameraOutlined style={{ color: '#1677ff', fontSize: 18 }} />
                     <span style={{ fontSize: 17, fontWeight: 700 }}>视频详情</span>
+                    <Tag color="blue-inverse" style={{ marginLeft: 4 }}>召回维度</Tag>
                   </Space>
                 }
                 loading={loadingMain}
@@ -462,7 +472,11 @@ function VideoIdTab() {
                 {detail ? (
                   <Space direction="vertical" size={10} style={{ width: '100%' }}>
                     <DetailRow label="ID" value={String(detail.videoId)} />
-                    <DetailRow label="标题" value={detail.title ?? '-'} />
+                    <DetailRow
+                      label="标题"
+                      value={detail.title ?? '-'}
+                      actionButton={renderTitleRecallButton(detail.title, configCodes, onRecallByText)}
+                    />
                     <DetailRow
                       label="播放量"
                       value={
@@ -504,13 +518,15 @@ function DetailRow({
   label,
   value,
   hint,
+  actionButton,
 }: {
   label: string
   value: React.ReactNode
   hint?: string
+  actionButton?: React.ReactNode
 }) {
   return (
-    <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
+    <div style={{ display: 'flex', alignItems: actionButton ? 'center' : 'flex-start', gap: 12 }}>
       <Text type="secondary" style={{ minWidth: 56, fontSize: 12 }}>
         {label}
       </Text>
@@ -522,10 +538,44 @@ function DetailRow({
           </Text>
         )}
       </div>
+      {actionButton}
     </div>
   )
 }
 
+/** "标题" 行的"以此召回"按钮 — 走 VIDEO_TITLE 维度, 字典里有该 code 才生效 */
+function renderTitleRecallButton(
+  title: string | null | undefined,
+  configCodes: Record<string, string>,
+  onRecallByText: (text: string, configCodeOverride?: string) => void,
+) {
+  const trimmed = (title ?? '').trim()
+  const code = 'VIDEO_TITLE'
+  const dictHas = code in configCodes
+  const supported = dictHas && !!trimmed
+  const tip = !dictHas
+    ? `当前未启用"视频标题"维度向量召回 (${code} 不在后端字典)`
+    : !trimmed
+      ? '该字段无内容,无法召回'
+      : ''
+  return supported ? (
+    <Button
+      type="primary"
+      icon={<ThunderboltOutlined />}
+      onClick={() => onRecallByText(trimmed, code)}
+      style={ACTIVE_PARENT_BTN_STYLE}
+    >
+      以此召回
+    </Button>
+  ) : (
+    <Tooltip title={tip}>
+      <Button disabled icon={<ThunderboltOutlined />} style={DISABLED_PARENT_BTN_STYLE}>
+        以此召回
+      </Button>
+    </Tooltip>
+  )
+}
+
 /** 召回结果卡片标题: "召回维度:[中文标签]" + 描述 */
 function RecallTitle({
   meta,