|
|
@@ -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>
|
|
|
)
|
|
|
}
|