RankingSettingsButton.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import { useMemo } from 'react'
  2. import {
  3. Button,
  4. Popover,
  5. Slider,
  6. InputNumber,
  7. Space,
  8. Typography,
  9. Divider,
  10. Collapse,
  11. Tooltip,
  12. } from 'antd'
  13. import { SettingOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
  14. import {
  15. DEFAULT_RANKING_PARAMS,
  16. type RankingParams,
  17. } from '../utils/scoring'
  18. import { getConfigDisplayLabel } from '../api/configCodes'
  19. const { Text } = Typography
  20. interface Props {
  21. params: RankingParams
  22. onChange: (next: RankingParams) => void
  23. /** 当前结果集中出现过的 configCode 列表, 用于 "按维度阈值覆盖" 折叠面板 */
  24. configCodesInResult: string[]
  25. /** 后端字典 (configCode → 中文标签) */
  26. configCodes: Record<string, string>
  27. }
  28. const FORMULA_TEXT = `综合得分 = c × (α × sim_norm + (1-α) × rov_norm)
  29. sim_norm = clip((sim - simThreshold) / (1 - simThreshold), 0, 1)
  30. rov_norm = clip((rov - rovP5) / (rovP95 - rovP5), 0, 1)
  31. c = deconstructBoost (选题/灵感点/关键点/目的点) / 1.0 (其他维度)
  32. 先按 simThreshold 硬筛 (sim < 阈值剔除), 再按综合分排序.`
  33. export default function RankingSettingsButton({
  34. params,
  35. onChange,
  36. configCodesInResult,
  37. configCodes,
  38. }: Props) {
  39. const update = (patch: Partial<RankingParams>) => onChange({ ...params, ...patch })
  40. const updateCodeThreshold = (code: string, value: number | null) => {
  41. const next = { ...params.simThresholdsByCode }
  42. if (value == null) delete next[code]
  43. else next[code] = value
  44. update({ simThresholdsByCode: next })
  45. }
  46. const codeRows = useMemo(
  47. () =>
  48. configCodesInResult.map((c) => ({
  49. code: c,
  50. label: getConfigDisplayLabel(c, configCodes),
  51. override: params.simThresholdsByCode[c],
  52. })),
  53. [configCodesInResult, configCodes, params.simThresholdsByCode],
  54. )
  55. const content = (
  56. <div style={{ width: 380 }}>
  57. <Space direction="vertical" size={12} style={{ width: '100%' }}>
  58. <Row label="相似度阈值 simThreshold" tip="低于此值的结果被剔除, 同时作为 sim_norm 的下界">
  59. <InputNumber
  60. min={0}
  61. max={1}
  62. step={0.01}
  63. value={params.simThreshold}
  64. onChange={(v) => update({ simThreshold: typeof v === 'number' ? v : 0 })}
  65. style={{ width: 120 }}
  66. />
  67. </Row>
  68. <Row label="α 相关性权重" tip="α 越大越看重相关性, 越小越看重 ROV 质量">
  69. <div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
  70. <Slider
  71. min={0}
  72. max={1}
  73. step={0.05}
  74. value={params.alpha}
  75. onChange={(v) => update({ alpha: v })}
  76. style={{ flex: 1 }}
  77. />
  78. <InputNumber
  79. min={0}
  80. max={1}
  81. step={0.05}
  82. value={params.alpha}
  83. onChange={(v) => update({ alpha: typeof v === 'number' ? v : 0.6 })}
  84. style={{ width: 80 }}
  85. />
  86. </div>
  87. </Row>
  88. <Row label="ROV 归一化下界 rovP5">
  89. <InputNumber
  90. min={0}
  91. max={1}
  92. step={0.001}
  93. value={params.rovP5}
  94. onChange={(v) => update({ rovP5: typeof v === 'number' ? v : 0 })}
  95. style={{ width: 120 }}
  96. />
  97. </Row>
  98. <Row label="ROV 归一化上界 rovP95">
  99. <InputNumber
  100. min={0}
  101. max={1}
  102. step={0.001}
  103. value={params.rovP95}
  104. onChange={(v) => update({ rovP95: typeof v === 'number' ? v : 0.07 })}
  105. style={{ width: 120 }}
  106. />
  107. </Row>
  108. {/* 素材质量维度权重 */}
  109. <Divider orientation="left" style={{ fontSize: 12, margin: '8px 0' }}>素材质量维度权重</Divider>
  110. <Row label="相似度 wSim" tip="素材模态下相似度的权重">
  111. <Slider
  112. min={0} max={1} step={0.05}
  113. value={params.wSim}
  114. onChange={(v) => update({ wSim: typeof v === 'number' ? v : 0.4 })}
  115. />
  116. <span style={{ width: 48, textAlign: 'right' }}>{params.wSim.toFixed(2)}</span>
  117. </Row>
  118. <Row label="打开率 wCtr" tip="CTR 百分位的权重">
  119. <Slider
  120. min={0} max={1} step={0.05}
  121. value={params.wCtr}
  122. onChange={(v) => update({ wCtr: typeof v === 'number' ? v : 0.3 })}
  123. />
  124. <span style={{ width: 48, textAlign: 'right' }}>{params.wCtr.toFixed(2)}</span>
  125. </Row>
  126. <Row label="裂变率 wViral" tip="裂变率百分位的权重">
  127. <Slider
  128. min={0} max={1} step={0.05}
  129. value={params.wViral}
  130. onChange={(v) => update({ wViral: typeof v === 'number' ? v : 0.2 })}
  131. />
  132. <span style={{ width: 48, textAlign: 'right' }}>{params.wViral.toFixed(2)}</span>
  133. </Row>
  134. <Row label="ROI wRoi" tip="ROI 百分位的权重">
  135. <Slider
  136. min={0} max={1} step={0.05}
  137. value={params.wRoi}
  138. onChange={(v) => update({ wRoi: typeof v === 'number' ? v : 0.1 })}
  139. />
  140. <span style={{ width: 48, textAlign: 'right' }}>{params.wRoi.toFixed(2)}</span>
  141. </Row>
  142. <Divider orientation="left" style={{ fontSize: 12, margin: '8px 0' }}>视频维度</Divider>
  143. <Row
  144. label="解构加权 c"
  145. tip="选题 / 灵感点 / 关键点 / 目的点 维度的额外加权系数, 其他维度恒为 1"
  146. >
  147. <InputNumber
  148. min={0.5}
  149. max={3}
  150. step={0.05}
  151. value={params.deconstructBoost}
  152. onChange={(v) => update({ deconstructBoost: typeof v === 'number' ? v : 1.2 })}
  153. style={{ width: 120 }}
  154. />
  155. </Row>
  156. {codeRows.length > 0 && (
  157. <Collapse
  158. size="small"
  159. ghost
  160. items={[
  161. {
  162. key: 'override',
  163. label: (
  164. <Text style={{ fontSize: 12 }}>
  165. 按维度覆盖阈值 (
  166. {Object.keys(params.simThresholdsByCode).length} / {codeRows.length} 已设)
  167. </Text>
  168. ),
  169. children: (
  170. <Space direction="vertical" size={6} style={{ width: '100%' }}>
  171. {codeRows.map((r) => (
  172. <div
  173. key={r.code}
  174. style={{ display: 'flex', alignItems: 'center', gap: 8 }}
  175. >
  176. <Text style={{ flex: 1, fontSize: 12 }}>{r.label}</Text>
  177. <InputNumber
  178. size="small"
  179. min={0}
  180. max={1}
  181. step={0.01}
  182. placeholder={`默认 ${params.simThreshold}`}
  183. value={r.override}
  184. onChange={(v) =>
  185. updateCodeThreshold(r.code, typeof v === 'number' ? v : null)
  186. }
  187. style={{ width: 110 }}
  188. />
  189. </div>
  190. ))}
  191. </Space>
  192. ),
  193. },
  194. ]}
  195. />
  196. )}
  197. <Divider style={{ margin: '4px 0' }} />
  198. <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
  199. <Button
  200. size="small"
  201. icon={<ReloadOutlined />}
  202. onClick={() => onChange(DEFAULT_RANKING_PARAMS)}
  203. >
  204. 重置默认
  205. </Button>
  206. </div>
  207. </Space>
  208. </div>
  209. )
  210. const title = (
  211. <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
  212. <span>排序参数</span>
  213. <Tooltip
  214. overlayStyle={{ maxWidth: 480 }}
  215. title={<pre style={{ margin: 0, whiteSpace: 'pre-wrap', fontSize: 12 }}>{FORMULA_TEXT}</pre>}
  216. >
  217. <QuestionCircleOutlined style={{ color: 'rgba(0,0,0,0.45)' }} />
  218. </Tooltip>
  219. </div>
  220. )
  221. return (
  222. <Popover trigger="click" placement="bottomRight" title={title} content={content}>
  223. <Button icon={<SettingOutlined />}>排序参数</Button>
  224. </Popover>
  225. )
  226. }
  227. function Row({ label, tip, children }: { label: string; tip?: string; children: React.ReactNode }) {
  228. return (
  229. <div>
  230. <div style={{ marginBottom: 4, fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 }}>
  231. <Text style={{ fontSize: 12 }}>{label}</Text>
  232. {tip && (
  233. <Tooltip title={tip}>
  234. <QuestionCircleOutlined style={{ fontSize: 11, color: 'rgba(0,0,0,0.45)' }} />
  235. </Tooltip>
  236. )}
  237. </div>
  238. {children}
  239. </div>
  240. )
  241. }