RankingSettingsButton.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import {
  2. Button,
  3. Popover,
  4. Slider,
  5. InputNumber,
  6. Space,
  7. Typography,
  8. Divider,
  9. Tooltip,
  10. } from 'antd'
  11. import { SettingOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
  12. import type { Modality } from '../api/types'
  13. import {
  14. DEFAULT_RANKING_PARAMS,
  15. type RankingParams,
  16. } from '../utils/scoring'
  17. const { Text } = Typography
  18. type ActiveModality = 'ALL' | Modality
  19. interface Props {
  20. params: RankingParams
  21. onChange: (next: RankingParams) => void
  22. /** 当前结果 Tab,控制排序参数展示范围 */
  23. activeModality: ActiveModality
  24. }
  25. const FORMULA_VIDEO = `综合得分 = α·c·sim_norm + (1-α)·rov_norm (c 仅作用于相关性分)
  26. sim_norm = clip((sim - simThreshold) / (1 - simThreshold), 0, 1)
  27. rov_norm = clip((rov - rovClipLow) / (rovClipHigh - rovClipLow), 0, 1)
  28. c = boostsByCode[configCode] (选题默认 1, 其他维度默认 0.4) / deconstructBoost (未知维度兜底)
  29. 先按 simThreshold 硬筛 (sim < 阈值剔除), 再按综合分排序.`
  30. const FORMULA_MATERIAL = `综合得分 = α·c·sim_norm + (1-α)·qualityScore (c 仅作用于相关性分)
  31. sim_norm = clip((sim - simThreshold) / (1 - simThreshold), 0, 1)
  32. qualityScore = wCtr×ctr + wCvr×cvr + wRoi×roi + wOpenRate×openRate + wFissionRate×fissionRate
  33. 各维度直接用原始效率比率
  34. c = boostsByCode[configCode] (默认 1) / deconstructBoost (未知维度兜底)
  35. 先按 simThreshold 硬筛, 再按综合分排序.`
  36. const FORMULA_ARTICLE = `综合得分 = α·c·sim_norm + (1-α)·qualityScore (c 仅作用于相关性分)
  37. sim_norm = clip((sim - simThreshold) / (1 - simThreshold), 0, 1)
  38. qualityScore = (wRead×readScore + wOpen×openScore + wFission×fissionScore) / (wRead+wOpen+wFission)
  39. c = boostsByCode[configCode] (选题默认 1, 其他维度默认 0.4) / deconstructBoost (未知维度兜底)
  40. 先按 simThreshold 硬筛, 再按综合分排序.`
  41. const FORMULA_LEGACY = `${FORMULA_VIDEO}
  42. 素材模态:
  43. ${FORMULA_MATERIAL}
  44. 文章模态:
  45. ${FORMULA_ARTICLE}`
  46. function formulaForModality(active: ActiveModality): string {
  47. if (active === 'VIDEO') return FORMULA_VIDEO
  48. if (active === 'MATERIAL') return FORMULA_MATERIAL
  49. if (active === 'ARTICLE') return FORMULA_ARTICLE
  50. return FORMULA_LEGACY
  51. }
  52. export default function RankingSettingsButton({
  53. params,
  54. onChange,
  55. activeModality,
  56. }: Props) {
  57. const update = (patch: Partial<RankingParams>) => onChange({ ...params, ...patch })
  58. const content = (
  59. <div style={{ width: 380 }}>
  60. <Space direction="vertical" size={12} style={{ width: '100%' }}>
  61. {activeModality === 'ARTICLE' && (
  62. <>
  63. <SimThresholdRow params={params} update={update} />
  64. <AlphaRow params={params} update={update} />
  65. <Divider orientation="left" style={{ fontSize: 12, margin: '8px 0' }}>
  66. 文章质量维度权重
  67. </Divider>
  68. <ArticleWeightRows params={params} update={update} />
  69. </>
  70. )}
  71. {activeModality === 'VIDEO' && (
  72. <>
  73. <SimThresholdRow params={params} update={update} />
  74. <AlphaRow params={params} update={update} />
  75. <RovClipRows params={params} update={update} />
  76. </>
  77. )}
  78. {activeModality === 'MATERIAL' && (
  79. <>
  80. <SimThresholdRow params={params} update={update} />
  81. <Divider orientation="left" style={{ fontSize: 12, margin: '8px 0' }}>
  82. 素材质量维度权重
  83. </Divider>
  84. <MaterialWeightRows params={params} update={update} />
  85. </>
  86. )}
  87. {activeModality === 'ALL' && (
  88. <>
  89. <SimThresholdRow params={params} update={update} />
  90. <AlphaRow params={params} update={update} />
  91. <RovClipRows params={params} update={update} />
  92. <Divider orientation="left" style={{ fontSize: 12, margin: '8px 0' }}>
  93. 素材质量维度权重
  94. </Divider>
  95. <MaterialWeightRows params={params} update={update} />
  96. <Divider orientation="left" style={{ fontSize: 12, margin: '8px 0' }}>
  97. 文章质量维度权重
  98. </Divider>
  99. <ArticleWeightRows params={params} update={update} />
  100. </>
  101. )}
  102. <Divider style={{ margin: '4px 0' }} />
  103. <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
  104. <Button
  105. size="small"
  106. icon={<ReloadOutlined />}
  107. onClick={() => onChange(DEFAULT_RANKING_PARAMS)}
  108. >
  109. 重置默认
  110. </Button>
  111. </div>
  112. </Space>
  113. </div>
  114. )
  115. const title = (
  116. <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
  117. <span>排序参数</span>
  118. <Tooltip
  119. overlayStyle={{ maxWidth: 480 }}
  120. title={
  121. <pre style={{ margin: 0, whiteSpace: 'pre-wrap', fontSize: 12 }}>
  122. {formulaForModality(activeModality)}
  123. </pre>
  124. }
  125. >
  126. <QuestionCircleOutlined style={{ color: 'rgba(0,0,0,0.45)' }} />
  127. </Tooltip>
  128. </div>
  129. )
  130. return (
  131. <Popover trigger="click" placement="bottomRight" title={title} content={content}>
  132. <Button icon={<SettingOutlined />}>排序参数</Button>
  133. </Popover>
  134. )
  135. }
  136. type PatchFn = (patch: Partial<RankingParams>) => void
  137. function SimThresholdRow({ params, update }: { params: RankingParams; update: PatchFn }) {
  138. return (
  139. <Row label="相似度阈值 simThreshold" tip="低于此值的结果被剔除, 同时作为 sim_norm 的下界">
  140. <InputNumber
  141. min={0}
  142. max={1}
  143. step={0.01}
  144. value={params.simThreshold}
  145. onChange={(v) => update({ simThreshold: typeof v === 'number' ? v : 0 })}
  146. style={{ width: 120 }}
  147. />
  148. </Row>
  149. )
  150. }
  151. function AlphaRow({ params, update }: { params: RankingParams; update: PatchFn }) {
  152. return (
  153. <Row label="α 相关性权重" tip="α 越大越看重相关性, 越小越看重 ROV 质量">
  154. <div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
  155. <Slider
  156. min={0}
  157. max={1}
  158. step={0.05}
  159. value={params.alpha}
  160. onChange={(v) => update({ alpha: v })}
  161. style={{ flex: 1 }}
  162. />
  163. <InputNumber
  164. min={0}
  165. max={1}
  166. step={0.05}
  167. value={params.alpha}
  168. onChange={(v) => update({ alpha: typeof v === 'number' ? v : 0.6 })}
  169. style={{ width: 80 }}
  170. />
  171. </div>
  172. </Row>
  173. )
  174. }
  175. function RovClipRows({ params, update }: { params: RankingParams; update: PatchFn }) {
  176. return (
  177. <>
  178. <Row label="ROV 归一化下界(clip 低值)">
  179. <InputNumber
  180. min={0}
  181. max={1}
  182. step={0.001}
  183. value={params.rovClipLow}
  184. onChange={(v) => update({ rovClipLow: typeof v === 'number' ? v : 0 })}
  185. style={{ width: 120 }}
  186. />
  187. </Row>
  188. <Row label="ROV 归一化上界(clip 高值)">
  189. <InputNumber
  190. min={0}
  191. max={1}
  192. step={0.001}
  193. value={params.rovClipHigh}
  194. onChange={(v) => update({ rovClipHigh: typeof v === 'number' ? v : 0.07 })}
  195. style={{ width: 120 }}
  196. />
  197. </Row>
  198. </>
  199. )
  200. }
  201. function MaterialWeightRows({ params, update }: { params: RankingParams; update: PatchFn }) {
  202. return (
  203. <>
  204. <Row label="相关性 α" tip="素材模态下相关性 VS 质量的权衡权重">
  205. <Slider
  206. min={0}
  207. max={1}
  208. step={0.05}
  209. value={params.alpha}
  210. onChange={(v) => update({ alpha: typeof v === 'number' ? v : 0.6 })}
  211. />
  212. <span style={{ width: 48, textAlign: 'right' }}>{params.alpha.toFixed(2)}</span>
  213. </Row>
  214. <Row label="CTR wCtr" tip="CTR 百分位的权重">
  215. <Slider
  216. min={0}
  217. max={1}
  218. step={0.05}
  219. value={params.wCtr}
  220. onChange={(v) => update({ wCtr: typeof v === 'number' ? v : 0.2 })}
  221. />
  222. <span style={{ width: 48, textAlign: 'right' }}>{params.wCtr.toFixed(2)}</span>
  223. </Row>
  224. <Row label="CVR wCvr" tip="CVR 百分位的权重">
  225. <Slider
  226. min={0}
  227. max={1}
  228. step={0.05}
  229. value={params.wCvr}
  230. onChange={(v) => update({ wCvr: typeof v === 'number' ? v : 0.2 })}
  231. />
  232. <span style={{ width: 48, textAlign: 'right' }}>{params.wCvr.toFixed(2)}</span>
  233. </Row>
  234. <Row label="ROI wRoi" tip="ROI 百分位的权重">
  235. <Slider
  236. min={0}
  237. max={1}
  238. step={0.05}
  239. value={params.wRoi}
  240. onChange={(v) => update({ wRoi: typeof v === 'number' ? v : 0.2 })}
  241. />
  242. <span style={{ width: 48, textAlign: 'right' }}>{params.wRoi.toFixed(2)}</span>
  243. </Row>
  244. <Row label="打开率 wOpenRate" tip="小程序打开率百分位的权重">
  245. <Slider
  246. min={0}
  247. max={1}
  248. step={0.05}
  249. value={params.wOpenRate}
  250. onChange={(v) => update({ wOpenRate: typeof v === 'number' ? v : 0.2 })}
  251. />
  252. <span style={{ width: 48, textAlign: 'right' }}>{params.wOpenRate.toFixed(2)}</span>
  253. </Row>
  254. <Row label="裂变率 wFissionRate" tip="T0裂变率百分位的权重">
  255. <Slider
  256. min={0}
  257. max={1}
  258. step={0.05}
  259. value={params.wFissionRate}
  260. onChange={(v) => update({ wFissionRate: typeof v === 'number' ? v : 0.2 })}
  261. />
  262. <span style={{ width: 48, textAlign: 'right' }}>{params.wFissionRate.toFixed(2)}</span>
  263. </Row>
  264. </>
  265. )
  266. }
  267. /** 文章质量维度权重 */
  268. function ArticleWeightRows({ params, update }: { params: RankingParams; update: PatchFn }) {
  269. return (
  270. <>
  271. <Row label="阅读 wRead" tip="阅读百分位的权重">
  272. <Slider
  273. min={0}
  274. max={1}
  275. step={0.05}
  276. value={params.wRead}
  277. onChange={(v) => update({ wRead: typeof v === 'number' ? v : 0.4 })}
  278. />
  279. <span style={{ width: 48, textAlign: 'right' }}>{params.wRead.toFixed(2)}</span>
  280. </Row>
  281. <Row label="打开率 wOpen" tip="打开率百分位的权重">
  282. <Slider
  283. min={0}
  284. max={1}
  285. step={0.05}
  286. value={params.wOpen}
  287. onChange={(v) => update({ wOpen: typeof v === 'number' ? v : 0.3 })}
  288. />
  289. <span style={{ width: 48, textAlign: 'right' }}>{params.wOpen.toFixed(2)}</span>
  290. </Row>
  291. <Row label="裂变率 wFission" tip="裂变率百分位的权重">
  292. <Slider
  293. min={0}
  294. max={1}
  295. step={0.05}
  296. value={params.wFission}
  297. onChange={(v) => update({ wFission: typeof v === 'number' ? v : 0.3 })}
  298. />
  299. <span style={{ width: 48, textAlign: 'right' }}>{params.wFission.toFixed(2)}</span>
  300. </Row>
  301. </>
  302. )
  303. }
  304. function Row({ label, tip, children }: { label: string; tip?: string; children: React.ReactNode }) {
  305. return (
  306. <div>
  307. <div style={{ marginBottom: 4, fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 }}>
  308. <Text style={{ fontSize: 12 }}>{label}</Text>
  309. {tip && (
  310. <Tooltip title={tip}>
  311. <QuestionCircleOutlined style={{ fontSize: 11, color: 'rgba(0,0,0,0.45)' }} />
  312. </Tooltip>
  313. )}
  314. </div>
  315. {children}
  316. </div>
  317. )
  318. }