| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- import {
- Button,
- Popover,
- Slider,
- InputNumber,
- Space,
- Typography,
- Divider,
- Tooltip,
- } from 'antd'
- import { SettingOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
- import type { Modality } from '../api/types'
- import {
- DEFAULT_RANKING_PARAMS,
- type RankingParams,
- } from '../utils/scoring'
- const { Text } = Typography
- type ActiveModality = 'ALL' | Modality
- interface Props {
- params: RankingParams
- onChange: (next: RankingParams) => void
- /** 当前结果 Tab,控制排序参数展示范围 */
- activeModality: ActiveModality
- }
- const FORMULA_VIDEO = `综合得分 = α·c·sim_norm + (1-α)·rov_norm (c 仅作用于相关性分)
- sim_norm = clip((sim - simThreshold) / (1 - simThreshold), 0, 1)
- rov_norm = clip((rov - rovClipLow) / (rovClipHigh - rovClipLow), 0, 1)
- c = boostsByCode[configCode] (选题默认 1, 其他维度默认 0.4) / deconstructBoost (未知维度兜底)
- 先按 simThreshold 硬筛 (sim < 阈值剔除), 再按综合分排序.`
- const FORMULA_MATERIAL = `综合得分 = α·c·sim_norm + (1-α)·qualityScore (c 仅作用于相关性分)
- sim_norm = clip((sim - simThreshold) / (1 - simThreshold), 0, 1)
- qualityScore = wCtr×ctr + wCvr×cvr + wRoi×roi + wOpenRate×openRate + wFissionRate×fissionRate
- 各维度直接用原始效率比率
- c = boostsByCode[configCode] (默认 1) / deconstructBoost (未知维度兜底)
- 先按 simThreshold 硬筛, 再按综合分排序.`
- const FORMULA_ARTICLE = `综合得分 = α·c·sim_norm + (1-α)·qualityScore (c 仅作用于相关性分)
- sim_norm = clip((sim - simThreshold) / (1 - simThreshold), 0, 1)
- qualityScore = (wRead×readScore + wOpen×openScore + wFission×fissionScore) / (wRead+wOpen+wFission)
- c = boostsByCode[configCode] (选题默认 1, 其他维度默认 0.4) / deconstructBoost (未知维度兜底)
- 先按 simThreshold 硬筛, 再按综合分排序.`
- const FORMULA_LEGACY = `${FORMULA_VIDEO}
- 素材模态:
- ${FORMULA_MATERIAL}
- 文章模态:
- ${FORMULA_ARTICLE}`
- function formulaForModality(active: ActiveModality): string {
- if (active === 'VIDEO') return FORMULA_VIDEO
- if (active === 'MATERIAL') return FORMULA_MATERIAL
- if (active === 'ARTICLE') return FORMULA_ARTICLE
- return FORMULA_LEGACY
- }
- export default function RankingSettingsButton({
- params,
- onChange,
- activeModality,
- }: Props) {
- const update = (patch: Partial<RankingParams>) => onChange({ ...params, ...patch })
- const content = (
- <div style={{ width: 380 }}>
- <Space direction="vertical" size={12} style={{ width: '100%' }}>
- {activeModality === 'ARTICLE' && (
- <>
- <SimThresholdRow params={params} update={update} />
- <AlphaRow params={params} update={update} />
- <Divider orientation="left" style={{ fontSize: 12, margin: '8px 0' }}>
- 文章质量维度权重
- </Divider>
- <ArticleWeightRows params={params} update={update} />
- </>
- )}
- {activeModality === 'VIDEO' && (
- <>
- <SimThresholdRow params={params} update={update} />
- <AlphaRow params={params} update={update} />
- <RovClipRows params={params} update={update} />
- </>
- )}
- {activeModality === 'MATERIAL' && (
- <>
- <SimThresholdRow params={params} update={update} />
- <Divider orientation="left" style={{ fontSize: 12, margin: '8px 0' }}>
- 素材质量维度权重
- </Divider>
- <MaterialWeightRows params={params} update={update} />
- </>
- )}
- {activeModality === 'ALL' && (
- <>
- <SimThresholdRow params={params} update={update} />
- <AlphaRow params={params} update={update} />
- <RovClipRows params={params} update={update} />
- <Divider orientation="left" style={{ fontSize: 12, margin: '8px 0' }}>
- 素材质量维度权重
- </Divider>
- <MaterialWeightRows params={params} update={update} />
- <Divider orientation="left" style={{ fontSize: 12, margin: '8px 0' }}>
- 文章质量维度权重
- </Divider>
- <ArticleWeightRows params={params} update={update} />
- </>
- )}
- <Divider style={{ margin: '4px 0' }} />
- <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
- <Button
- size="small"
- icon={<ReloadOutlined />}
- onClick={() => onChange(DEFAULT_RANKING_PARAMS)}
- >
- 重置默认
- </Button>
- </div>
- </Space>
- </div>
- )
- const title = (
- <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
- <span>排序参数</span>
- <Tooltip
- overlayStyle={{ maxWidth: 480 }}
- title={
- <pre style={{ margin: 0, whiteSpace: 'pre-wrap', fontSize: 12 }}>
- {formulaForModality(activeModality)}
- </pre>
- }
- >
- <QuestionCircleOutlined style={{ color: 'rgba(0,0,0,0.45)' }} />
- </Tooltip>
- </div>
- )
- return (
- <Popover trigger="click" placement="bottomRight" title={title} content={content}>
- <Button icon={<SettingOutlined />}>排序参数</Button>
- </Popover>
- )
- }
- type PatchFn = (patch: Partial<RankingParams>) => void
- function SimThresholdRow({ params, update }: { params: RankingParams; update: PatchFn }) {
- return (
- <Row label="相似度阈值 simThreshold" tip="低于此值的结果被剔除, 同时作为 sim_norm 的下界">
- <InputNumber
- min={0}
- max={1}
- step={0.01}
- value={params.simThreshold}
- onChange={(v) => update({ simThreshold: typeof v === 'number' ? v : 0 })}
- style={{ width: 120 }}
- />
- </Row>
- )
- }
- function AlphaRow({ params, update }: { params: RankingParams; update: PatchFn }) {
- return (
- <Row label="α 相关性权重" tip="α 越大越看重相关性, 越小越看重 ROV 质量">
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
- <Slider
- min={0}
- max={1}
- step={0.05}
- value={params.alpha}
- onChange={(v) => update({ alpha: v })}
- style={{ flex: 1 }}
- />
- <InputNumber
- min={0}
- max={1}
- step={0.05}
- value={params.alpha}
- onChange={(v) => update({ alpha: typeof v === 'number' ? v : 0.6 })}
- style={{ width: 80 }}
- />
- </div>
- </Row>
- )
- }
- function RovClipRows({ params, update }: { params: RankingParams; update: PatchFn }) {
- return (
- <>
- <Row label="ROV 归一化下界(clip 低值)">
- <InputNumber
- min={0}
- max={1}
- step={0.001}
- value={params.rovClipLow}
- onChange={(v) => update({ rovClipLow: typeof v === 'number' ? v : 0 })}
- style={{ width: 120 }}
- />
- </Row>
- <Row label="ROV 归一化上界(clip 高值)">
- <InputNumber
- min={0}
- max={1}
- step={0.001}
- value={params.rovClipHigh}
- onChange={(v) => update({ rovClipHigh: typeof v === 'number' ? v : 0.07 })}
- style={{ width: 120 }}
- />
- </Row>
- </>
- )
- }
- function MaterialWeightRows({ params, update }: { params: RankingParams; update: PatchFn }) {
- return (
- <>
- <Row label="相关性 α" tip="素材模态下相关性 VS 质量的权衡权重">
- <Slider
- min={0}
- max={1}
- step={0.05}
- value={params.alpha}
- onChange={(v) => update({ alpha: typeof v === 'number' ? v : 0.6 })}
- />
- <span style={{ width: 48, textAlign: 'right' }}>{params.alpha.toFixed(2)}</span>
- </Row>
- <Row label="CTR wCtr" tip="CTR 百分位的权重">
- <Slider
- min={0}
- max={1}
- step={0.05}
- value={params.wCtr}
- onChange={(v) => update({ wCtr: typeof v === 'number' ? v : 0.2 })}
- />
- <span style={{ width: 48, textAlign: 'right' }}>{params.wCtr.toFixed(2)}</span>
- </Row>
- <Row label="CVR wCvr" tip="CVR 百分位的权重">
- <Slider
- min={0}
- max={1}
- step={0.05}
- value={params.wCvr}
- onChange={(v) => update({ wCvr: typeof v === 'number' ? v : 0.2 })}
- />
- <span style={{ width: 48, textAlign: 'right' }}>{params.wCvr.toFixed(2)}</span>
- </Row>
- <Row label="ROI wRoi" tip="ROI 百分位的权重">
- <Slider
- min={0}
- max={1}
- step={0.05}
- value={params.wRoi}
- onChange={(v) => update({ wRoi: typeof v === 'number' ? v : 0.2 })}
- />
- <span style={{ width: 48, textAlign: 'right' }}>{params.wRoi.toFixed(2)}</span>
- </Row>
- <Row label="打开率 wOpenRate" tip="小程序打开率百分位的权重">
- <Slider
- min={0}
- max={1}
- step={0.05}
- value={params.wOpenRate}
- onChange={(v) => update({ wOpenRate: typeof v === 'number' ? v : 0.2 })}
- />
- <span style={{ width: 48, textAlign: 'right' }}>{params.wOpenRate.toFixed(2)}</span>
- </Row>
- <Row label="裂变率 wFissionRate" tip="T0裂变率百分位的权重">
- <Slider
- min={0}
- max={1}
- step={0.05}
- value={params.wFissionRate}
- onChange={(v) => update({ wFissionRate: typeof v === 'number' ? v : 0.2 })}
- />
- <span style={{ width: 48, textAlign: 'right' }}>{params.wFissionRate.toFixed(2)}</span>
- </Row>
- </>
- )
- }
- /** 文章质量维度权重 */
- function ArticleWeightRows({ params, update }: { params: RankingParams; update: PatchFn }) {
- return (
- <>
- <Row label="阅读 wRead" tip="阅读百分位的权重">
- <Slider
- min={0}
- max={1}
- step={0.05}
- value={params.wRead}
- onChange={(v) => update({ wRead: typeof v === 'number' ? v : 0.4 })}
- />
- <span style={{ width: 48, textAlign: 'right' }}>{params.wRead.toFixed(2)}</span>
- </Row>
- <Row label="打开率 wOpen" tip="打开率百分位的权重">
- <Slider
- min={0}
- max={1}
- step={0.05}
- value={params.wOpen}
- onChange={(v) => update({ wOpen: typeof v === 'number' ? v : 0.3 })}
- />
- <span style={{ width: 48, textAlign: 'right' }}>{params.wOpen.toFixed(2)}</span>
- </Row>
- <Row label="裂变率 wFission" tip="裂变率百分位的权重">
- <Slider
- min={0}
- max={1}
- step={0.05}
- value={params.wFission}
- onChange={(v) => update({ wFission: typeof v === 'number' ? v : 0.3 })}
- />
- <span style={{ width: 48, textAlign: 'right' }}>{params.wFission.toFixed(2)}</span>
- </Row>
- </>
- )
- }
- function Row({ label, tip, children }: { label: string; tip?: string; children: React.ReactNode }) {
- return (
- <div>
- <div style={{ marginBottom: 4, fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 }}>
- <Text style={{ fontSize: 12 }}>{label}</Text>
- {tip && (
- <Tooltip title={tip}>
- <QuestionCircleOutlined style={{ fontSize: 11, color: 'rgba(0,0,0,0.45)' }} />
- </Tooltip>
- )}
- </div>
- {children}
- </div>
- )
- }
|