|
|
@@ -10,8 +10,12 @@ import {
|
|
|
Button,
|
|
|
Checkbox,
|
|
|
} from 'antd'
|
|
|
-import { PlayCircleFilled, FilterFilled } from '@ant-design/icons'
|
|
|
-import type { ColumnsType } from 'antd/es/table'
|
|
|
+import {
|
|
|
+ PlayCircleFilled,
|
|
|
+ FilterFilled,
|
|
|
+ QuestionCircleOutlined,
|
|
|
+} from '@ant-design/icons'
|
|
|
+import type { ColumnsType, SorterResult } from 'antd/es/table/interface'
|
|
|
import type { EssenceWord, VideoMatchEnrichedVO } from '../api/types'
|
|
|
import { useConfigCodes, getConfigDisplayLabel } from '../api/configCodes'
|
|
|
import { toHttps } from '../utils/url'
|
|
|
@@ -21,11 +25,15 @@ import {
|
|
|
getScoreStyle,
|
|
|
parseNum,
|
|
|
} from '../utils/format'
|
|
|
+import {
|
|
|
+ computeCompositeScore,
|
|
|
+ type RankingParams,
|
|
|
+ type ScoreBreakdown,
|
|
|
+} from '../utils/scoring'
|
|
|
|
|
|
const { Text, Paragraph } = Typography
|
|
|
|
|
|
interface Filters {
|
|
|
- score: number | null
|
|
|
configCodes: string[]
|
|
|
pv: number | null
|
|
|
hl: number | null
|
|
|
@@ -33,13 +41,15 @@ interface Filters {
|
|
|
}
|
|
|
|
|
|
const EMPTY_FILTERS: Filters = {
|
|
|
- score: null,
|
|
|
configCodes: [],
|
|
|
pv: null,
|
|
|
hl: null,
|
|
|
rov: null,
|
|
|
}
|
|
|
|
|
|
+/** 派生:item + 综合得分分解, 渲染时统一用这个壳 */
|
|
|
+type RowItem = VideoMatchEnrichedVO & { _breakdown: ScoreBreakdown | null }
|
|
|
+
|
|
|
const ACTIVE_ICON_COLOR = '#1677ff'
|
|
|
const IDLE_ICON_COLOR = '#bfbfbf'
|
|
|
|
|
|
@@ -55,32 +65,53 @@ const METRIC_CELL_STYLE: React.CSSProperties = {
|
|
|
|
|
|
interface Props {
|
|
|
items: VideoMatchEnrichedVO[]
|
|
|
+ rankingParams: RankingParams
|
|
|
+ /** 受控的"综合得分"列 sort 状态 — 由外部按钮 + 列头联动 */
|
|
|
+ compositeSort: 'descend' | 'ascend' | null
|
|
|
+ onCompositeSortChange: (next: 'descend' | 'ascend' | null) => void
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 召回结果表格 — 14 列横向并列, 缺字段统一 "--", 长文案 ellipsis + hover tooltip
|
|
|
- * 列顺序按业务约定: 标题/封面/召回维度/相似度/4个解构/3个旧AI/3个量级
|
|
|
+ * 召回结果表格
|
|
|
+ * 列顺序: 标题/封面/召回维度/综合得分/向量相似度/解构/旧AI/指标
|
|
|
+ * 综合得分 = c × (α × sim_norm + (1-α) × rov_norm), 参数走 rankingParams (浮层可改)
|
|
|
+ * sim < simThreshold 的 item 直接剔除
|
|
|
*/
|
|
|
-export default function RecallResultTable({ items }: Props) {
|
|
|
+export default function RecallResultTable({
|
|
|
+ items,
|
|
|
+ rankingParams,
|
|
|
+ compositeSort,
|
|
|
+ onCompositeSortChange,
|
|
|
+}: Props) {
|
|
|
const configCodes = useConfigCodes()
|
|
|
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS)
|
|
|
|
|
|
+ /** 给每条 item 挂上综合得分分解 */
|
|
|
+ const rowItems: RowItem[] = useMemo(
|
|
|
+ () => items.map((it) => ({ ...it, _breakdown: computeCompositeScore(it, rankingParams) })),
|
|
|
+ [items, rankingParams],
|
|
|
+ )
|
|
|
+
|
|
|
+ /** 阈值剔除 — sim 缺失或 < simThreshold 的项不进入展示 */
|
|
|
+ const thresholdFiltered = useMemo(
|
|
|
+ () => rowItems.filter((it) => it._breakdown != null && it._breakdown.passesThreshold),
|
|
|
+ [rowItems],
|
|
|
+ )
|
|
|
+ const thresholdRejected = rowItems.length - thresholdFiltered.length
|
|
|
+
|
|
|
const codeOptions = useMemo(() => {
|
|
|
const set = new Set<string>()
|
|
|
- items.forEach((it) => {
|
|
|
+ thresholdFiltered.forEach((it) => {
|
|
|
if (it.configCode) set.add(it.configCode)
|
|
|
})
|
|
|
return Array.from(set).map((code) => ({
|
|
|
label: getConfigDisplayLabel(code, configCodes),
|
|
|
value: code,
|
|
|
}))
|
|
|
- }, [items, configCodes])
|
|
|
+ }, [thresholdFiltered, configCodes])
|
|
|
|
|
|
const filteredItems = useMemo(() => {
|
|
|
- return items.filter((it) => {
|
|
|
- if (filters.score != null) {
|
|
|
- if (it.score == null || it.score <= filters.score) return false
|
|
|
- }
|
|
|
+ return thresholdFiltered.filter((it) => {
|
|
|
if (filters.configCodes.length > 0) {
|
|
|
if (!it.configCode || !filters.configCodes.includes(it.configCode)) return false
|
|
|
}
|
|
|
@@ -98,10 +129,9 @@ export default function RecallResultTable({ items }: Props) {
|
|
|
}
|
|
|
return true
|
|
|
})
|
|
|
- }, [items, filters])
|
|
|
+ }, [thresholdFiltered, filters])
|
|
|
|
|
|
const hasFilter =
|
|
|
- filters.score != null ||
|
|
|
filters.configCodes.length > 0 ||
|
|
|
filters.pv != null ||
|
|
|
filters.hl != null ||
|
|
|
@@ -112,7 +142,7 @@ export default function RecallResultTable({ items }: Props) {
|
|
|
}
|
|
|
|
|
|
const thresholdDropdown = (
|
|
|
- field: 'score' | 'pv' | 'hl' | 'rov',
|
|
|
+ field: 'pv' | 'hl' | 'rov',
|
|
|
placeholder: string,
|
|
|
step?: number,
|
|
|
) =>
|
|
|
@@ -158,7 +188,7 @@ export default function RecallResultTable({ items }: Props) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
- const columns: ColumnsType<VideoMatchEnrichedVO> = [
|
|
|
+ const columns: ColumnsType<RowItem> = [
|
|
|
{
|
|
|
title: '标题',
|
|
|
key: 'title',
|
|
|
@@ -240,18 +270,38 @@ export default function RecallResultTable({ items }: Props) {
|
|
|
)
|
|
|
},
|
|
|
},
|
|
|
+ {
|
|
|
+ title: (
|
|
|
+ <Tooltip
|
|
|
+ overlayStyle={{ maxWidth: 360 }}
|
|
|
+ title={
|
|
|
+ <div style={{ fontSize: 12 }}>
|
|
|
+ 点击列头按综合得分倒排,公式见右上角"排序参数"。
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <span>
|
|
|
+ 综合得分{' '}
|
|
|
+ <QuestionCircleOutlined style={{ color: 'rgba(0,0,0,0.45)' }} />
|
|
|
+ </span>
|
|
|
+ </Tooltip>
|
|
|
+ ),
|
|
|
+ key: 'composite',
|
|
|
+ width: 130,
|
|
|
+ align: 'center',
|
|
|
+ fixed: 'left',
|
|
|
+ sorter: (a, b) =>
|
|
|
+ (a._breakdown?.composite ?? -Infinity) - (b._breakdown?.composite ?? -Infinity),
|
|
|
+ sortDirections: ['descend', 'ascend'],
|
|
|
+ sortOrder: compositeSort ?? null,
|
|
|
+ render: (_v, item) => <CompositeScoreCell breakdown={item._breakdown} />,
|
|
|
+ },
|
|
|
{
|
|
|
title: '向量相似度',
|
|
|
key: 'score',
|
|
|
width: 130,
|
|
|
align: 'center',
|
|
|
fixed: 'left',
|
|
|
- filterIcon: () => (
|
|
|
- <FilterFilled
|
|
|
- style={{ color: filters.score != null ? ACTIVE_ICON_COLOR : IDLE_ICON_COLOR }}
|
|
|
- />
|
|
|
- ),
|
|
|
- filterDropdown: thresholdDropdown('score', '相似度大于', 0.01),
|
|
|
render: (_v, item) => <ScoreCell score={item.score} />,
|
|
|
},
|
|
|
deconstructTopicCol(280),
|
|
|
@@ -332,6 +382,22 @@ export default function RecallResultTable({ items }: Props) {
|
|
|
|
|
|
return (
|
|
|
<div>
|
|
|
+ {thresholdRejected > 0 && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ marginBottom: 8,
|
|
|
+ padding: '6px 10px',
|
|
|
+ background: '#fff7e6',
|
|
|
+ border: '1px solid #ffd591',
|
|
|
+ borderRadius: 4,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Text style={{ fontSize: 12, color: '#d46b08' }}>
|
|
|
+ 相似度阈值 ≥ {rankingParams.simThreshold} (剔除 <b>{thresholdRejected}</b> 条);
|
|
|
+ 如需调整请点右上角"排序参数"
|
|
|
+ </Text>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
{hasFilter && (
|
|
|
<div
|
|
|
style={{
|
|
|
@@ -346,7 +412,7 @@ export default function RecallResultTable({ items }: Props) {
|
|
|
}}
|
|
|
>
|
|
|
<Text style={{ fontSize: 12 }}>
|
|
|
- 已应用筛选: 显示 <b>{filteredItems.length}</b> / {items.length}
|
|
|
+ 已应用筛选: 显示 <b>{filteredItems.length}</b> / {thresholdFiltered.length}
|
|
|
</Text>
|
|
|
<ActiveFilterTags
|
|
|
filters={filters}
|
|
|
@@ -360,14 +426,23 @@ export default function RecallResultTable({ items }: Props) {
|
|
|
</Button>
|
|
|
</div>
|
|
|
)}
|
|
|
- <Table<VideoMatchEnrichedVO>
|
|
|
+ <Table<RowItem>
|
|
|
size="small"
|
|
|
bordered
|
|
|
rowKey={(r) => `${r.modality}-${r.id}-${r.configCode ?? ''}`}
|
|
|
dataSource={filteredItems}
|
|
|
columns={columns}
|
|
|
pagination={false}
|
|
|
- scroll={{ x: 2300 }}
|
|
|
+ scroll={{ x: 2430 }}
|
|
|
+ onChange={(_pagination, _filters, sorter) => {
|
|
|
+ const s = sorter as SorterResult<RowItem>
|
|
|
+ if (s && s.columnKey === 'composite') {
|
|
|
+ onCompositeSortChange((s.order as 'descend' | 'ascend' | undefined) ?? null)
|
|
|
+ } else {
|
|
|
+ // 点了别的列的 sorter (例如 ROV) → 取消综合排序
|
|
|
+ onCompositeSortChange(null)
|
|
|
+ }
|
|
|
+ }}
|
|
|
/>
|
|
|
</div>
|
|
|
)
|
|
|
@@ -383,7 +458,6 @@ function ActiveFilterTags({
|
|
|
onClear: (field: keyof Filters) => void
|
|
|
}) {
|
|
|
const tags: { key: keyof Filters; text: string }[] = []
|
|
|
- if (filters.score != null) tags.push({ key: 'score', text: `相似度>${filters.score}` })
|
|
|
if (filters.configCodes.length > 0) {
|
|
|
const labels = filters.configCodes.map((c) => getConfigDisplayLabel(c, configCodes)).join('/')
|
|
|
tags.push({ key: 'configCodes', text: `维度: ${labels}` })
|
|
|
@@ -412,7 +486,7 @@ function textCol(
|
|
|
key: string,
|
|
|
width: number,
|
|
|
wrap = false,
|
|
|
-): ColumnsType<VideoMatchEnrichedVO>[number] {
|
|
|
+): ColumnsType<RowItem>[number] {
|
|
|
return {
|
|
|
title,
|
|
|
key,
|
|
|
@@ -443,7 +517,7 @@ function textCol(
|
|
|
}
|
|
|
|
|
|
/** 解构:选题 列 - 文本 ellipsis + tooltip,数据源 videoDetail.deconstruct.topic */
|
|
|
-function deconstructTopicCol(width: number): ColumnsType<VideoMatchEnrichedVO>[number] {
|
|
|
+function deconstructTopicCol(width: number): ColumnsType<RowItem>[number] {
|
|
|
return {
|
|
|
title: '解构:选题',
|
|
|
key: 'deconstruct.topic',
|
|
|
@@ -471,7 +545,7 @@ function pointsCol(
|
|
|
title: string,
|
|
|
type: '灵感点' | '关键点' | '目的点',
|
|
|
width: number,
|
|
|
-): ColumnsType<VideoMatchEnrichedVO>[number] {
|
|
|
+): ColumnsType<RowItem>[number] {
|
|
|
const essenceKey = `${type}-实质` as const
|
|
|
return {
|
|
|
title,
|
|
|
@@ -641,3 +715,51 @@ function ScoreCell({ score }: { score: number | null }) {
|
|
|
</span>
|
|
|
)
|
|
|
}
|
|
|
+
|
|
|
+/** 综合得分单元格 — hover Tooltip 展示 sim_norm/rov_norm/c 分解 */
|
|
|
+function CompositeScoreCell({ breakdown }: { breakdown: ScoreBreakdown | null }) {
|
|
|
+ if (!breakdown) {
|
|
|
+ return <Text type="secondary">--</Text>
|
|
|
+ }
|
|
|
+ const { composite, simNorm, rovNorm, boost } = breakdown
|
|
|
+ // 用与向量相似度同款配色 — 但映射区间不同, 综合得分 [0, c] 内, 走 0.6/0.45/0.3 三档
|
|
|
+ const styleScore =
|
|
|
+ composite >= 0.6
|
|
|
+ ? { bg: '#f6ffed', border: '#b7eb8f', text: '#389e0d' }
|
|
|
+ : composite >= 0.45
|
|
|
+ ? { bg: '#fcffe6', border: '#eaff8f', text: '#7cb305' }
|
|
|
+ : composite >= 0.3
|
|
|
+ ? { bg: '#fff7e6', border: '#ffd591', text: '#d46b08' }
|
|
|
+ : { bg: '#fff1f0', border: '#ffa39e', text: '#cf1322' }
|
|
|
+ const text = composite.toFixed(4)
|
|
|
+ const tip = (
|
|
|
+ <div style={{ fontSize: 12, lineHeight: 1.6 }}>
|
|
|
+ <div>sim_norm = {simNorm.toFixed(3)}</div>
|
|
|
+ <div>rov_norm = {rovNorm.toFixed(3)}</div>
|
|
|
+ <div>c = {boost.toFixed(2)}</div>
|
|
|
+ <div style={{ borderTop: '1px solid rgba(255,255,255,0.3)', marginTop: 4, paddingTop: 4 }}>
|
|
|
+ composite = {boost.toFixed(2)} × (α·sim_norm + (1-α)·rov_norm) = <b>{text}</b>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ return (
|
|
|
+ <Tooltip title={tip} overlayStyle={{ maxWidth: 360 }}>
|
|
|
+ <span
|
|
|
+ style={{
|
|
|
+ display: 'inline-block',
|
|
|
+ padding: '2px 8px',
|
|
|
+ background: styleScore.bg,
|
|
|
+ border: `1px solid ${styleScore.border}`,
|
|
|
+ color: styleScore.text,
|
|
|
+ borderRadius: 4,
|
|
|
+ fontWeight: 600,
|
|
|
+ fontVariantNumeric: 'tabular-nums',
|
|
|
+ fontSize: 12,
|
|
|
+ cursor: 'help',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {text}
|
|
|
+ </span>
|
|
|
+ </Tooltip>
|
|
|
+ )
|
|
|
+}
|