From b43423bffcb3c28e9ace63733fab5c8c9cf5e54e Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sat, 21 Jun 2025 20:24:52 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ratio-sync):=20support=20/api/?= =?UTF-8?q?pricing=20parsing,=20confidence=20verification=20&=20UI=20enhan?= =?UTF-8?q?cements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend - controller/ratio_sync.go • Parse /api/pricing response and convert to ratio / price maps. • Introduce confidence heuristic (model_ratio = 37.5 && completion_ratio = 1) to flag unreliable data. • Include confidence map when building differences and filter “same”/empty entries. - dto/ratio_sync.go • Add `ID` to UpstreamDTO, `upstreams` to UpstreamRequest, and `Confidence` to DifferenceItem. Frontend - ChannelSelectorModal.js • Re-implement with table layout, pagination, search, endpoint-type selector and mobile support. - UpstreamRatioSync.js • Send full upstream objects, add ratio-type filter, confidence badges/tooltips, retain endpoints. • Leverage ChannelSelectorModal’s pagination reset. - ChannelsTable.js – fix tag color for disabled status. - en.json – add translations for new UI labels. Motivation These changes let users sync model ratios / prices from different upstream endpoints and visually identify potentially unreliable data, improving operational safety and flexibility. --- controller/ratio_sync.go | 174 +++++++++- dto/ratio_sync.go | 17 +- .../settings/ChannelSelectorModal.js | 299 ++++++++++++------ web/src/components/table/ChannelsTable.js | 2 +- web/src/i18n/locales/en.json | 11 +- .../pages/Setting/Ratio/UpstreamRatioSync.js | 177 ++++++++--- 6 files changed, 507 insertions(+), 173 deletions(-) diff --git a/controller/ratio_sync.go b/controller/ratio_sync.go index f749f384..0453870d 100644 --- a/controller/ratio_sync.go +++ b/controller/ratio_sync.go @@ -3,6 +3,7 @@ package controller import ( "context" "encoding/json" + "fmt" "net/http" "strings" "sync" @@ -43,7 +44,17 @@ func FetchUpstreamRatios(c *gin.Context) { var upstreams []dto.UpstreamDTO - if len(req.ChannelIDs) > 0 { + if len(req.Upstreams) > 0 { + for _, u := range req.Upstreams { + if strings.HasPrefix(u.BaseURL, "http") { + if u.Endpoint == "" { + u.Endpoint = defaultEndpoint + } + u.BaseURL = strings.TrimRight(u.BaseURL, "/") + upstreams = append(upstreams, u) + } + } + } else if len(req.ChannelIDs) > 0 { intIds := make([]int, 0, len(req.ChannelIDs)) for _, id64 := range req.ChannelIDs { intIds = append(intIds, int(id64)) @@ -57,6 +68,7 @@ func FetchUpstreamRatios(c *gin.Context) { for _, ch := range dbChannels { if base := ch.GetBaseURL(); strings.HasPrefix(base, "http") { upstreams = append(upstreams, dto.UpstreamDTO{ + ID: ch.Id, Name: ch.Name, BaseURL: strings.TrimRight(base, "/"), Endpoint: "", @@ -93,43 +105,125 @@ func FetchUpstreamRatios(c *gin.Context) { } fullURL := chItem.BaseURL + endpoint + uniqueName := chItem.Name + if chItem.ID != 0 { + uniqueName = fmt.Sprintf("%s(%d)", chItem.Name, chItem.ID) + } + ctx, cancel := context.WithTimeout(c.Request.Context(), time.Duration(req.Timeout)*time.Second) defer cancel() httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) if err != nil { common.LogWarn(c.Request.Context(), "build request failed: "+err.Error()) - ch <- upstreamResult{Name: chItem.Name, Err: err.Error()} + ch <- upstreamResult{Name: uniqueName, Err: err.Error()} return } resp, err := client.Do(httpReq) if err != nil { common.LogWarn(c.Request.Context(), "http error on "+chItem.Name+": "+err.Error()) - ch <- upstreamResult{Name: chItem.Name, Err: err.Error()} + ch <- upstreamResult{Name: uniqueName, Err: err.Error()} return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { common.LogWarn(c.Request.Context(), "non-200 from "+chItem.Name+": "+resp.Status) - ch <- upstreamResult{Name: chItem.Name, Err: resp.Status} + ch <- upstreamResult{Name: uniqueName, Err: resp.Status} return } + // 兼容两种上游接口格式: + // type1: /api/ratio_config -> data 为 map[string]any,包含 model_ratio/completion_ratio/cache_ratio/model_price + // type2: /api/pricing -> data 为 []Pricing 列表,需要转换为与 type1 相同的 map 格式 var body struct { - Success bool `json:"success"` - Data map[string]any `json:"data"` - Message string `json:"message"` + Success bool `json:"success"` + Data json.RawMessage `json:"data"` + Message string `json:"message"` } + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { common.LogWarn(c.Request.Context(), "json decode failed from "+chItem.Name+": "+err.Error()) - ch <- upstreamResult{Name: chItem.Name, Err: err.Error()} + ch <- upstreamResult{Name: uniqueName, Err: err.Error()} return } + if !body.Success { - ch <- upstreamResult{Name: chItem.Name, Err: body.Message} + ch <- upstreamResult{Name: uniqueName, Err: body.Message} return } - ch <- upstreamResult{Name: chItem.Name, Data: body.Data} + + // 尝试按 type1 解析 + var type1Data map[string]any + if err := json.Unmarshal(body.Data, &type1Data); err == nil { + // 如果包含至少一个 ratioTypes 字段,则认为是 type1 + isType1 := false + for _, rt := range ratioTypes { + if _, ok := type1Data[rt]; ok { + isType1 = true + break + } + } + if isType1 { + ch <- upstreamResult{Name: uniqueName, Data: type1Data} + return + } + } + + // 如果不是 type1,则尝试按 type2 (/api/pricing) 解析 + var pricingItems []struct { + ModelName string `json:"model_name"` + QuotaType int `json:"quota_type"` + ModelRatio float64 `json:"model_ratio"` + ModelPrice float64 `json:"model_price"` + CompletionRatio float64 `json:"completion_ratio"` + } + if err := json.Unmarshal(body.Data, &pricingItems); err != nil { + common.LogWarn(c.Request.Context(), "unrecognized data format from "+chItem.Name+": "+err.Error()) + ch <- upstreamResult{Name: uniqueName, Err: "无法解析上游返回数据"} + return + } + + modelRatioMap := make(map[string]float64) + completionRatioMap := make(map[string]float64) + modelPriceMap := make(map[string]float64) + + for _, item := range pricingItems { + if item.QuotaType == 1 { + modelPriceMap[item.ModelName] = item.ModelPrice + } else { + modelRatioMap[item.ModelName] = item.ModelRatio + // completionRatio 可能为 0,此时也直接赋值,保持与上游一致 + completionRatioMap[item.ModelName] = item.CompletionRatio + } + } + + converted := make(map[string]any) + + if len(modelRatioMap) > 0 { + ratioAny := make(map[string]any, len(modelRatioMap)) + for k, v := range modelRatioMap { + ratioAny[k] = v + } + converted["model_ratio"] = ratioAny + } + + if len(completionRatioMap) > 0 { + compAny := make(map[string]any, len(completionRatioMap)) + for k, v := range completionRatioMap { + compAny[k] = v + } + converted["completion_ratio"] = compAny + } + + if len(modelPriceMap) > 0 { + priceAny := make(map[string]any, len(modelPriceMap)) + for k, v := range modelPriceMap { + priceAny[k] = v + } + converted["model_price"] = priceAny + } + + ch <- upstreamResult{Name: uniqueName, Data: converted} }(chn) } @@ -202,6 +296,43 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { } } + confidenceMap := make(map[string]map[string]bool) + + // 预处理阶段:检查pricing接口的可信度 + for _, channel := range successfulChannels { + confidenceMap[channel.name] = make(map[string]bool) + + modelRatios, hasModelRatio := channel.data["model_ratio"].(map[string]any) + completionRatios, hasCompletionRatio := channel.data["completion_ratio"].(map[string]any) + + if hasModelRatio && hasCompletionRatio { + // 遍历所有模型,检查是否满足不可信条件 + for modelName := range allModels { + // 默认为可信 + confidenceMap[channel.name][modelName] = true + + // 检查是否满足不可信条件:model_ratio为37.5且completion_ratio为1 + if modelRatioVal, ok := modelRatios[modelName]; ok { + if completionRatioVal, ok := completionRatios[modelName]; ok { + // 转换为float64进行比较 + if modelRatioFloat, ok := modelRatioVal.(float64); ok { + if completionRatioFloat, ok := completionRatioVal.(float64); ok { + if modelRatioFloat == 37.5 && completionRatioFloat == 1.0 { + confidenceMap[channel.name][modelName] = false + } + } + } + } + } + } + } else { + // 如果不是从pricing接口获取的数据,则全部标记为可信 + for modelName := range allModels { + confidenceMap[channel.name][modelName] = true + } + } + } + for modelName := range allModels { for _, ratioType := range ratioTypes { var localValue interface{} = nil @@ -214,6 +345,7 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { } upstreamValues := make(map[string]interface{}) + confidenceValues := make(map[string]bool) hasUpstreamValue := false hasDifference := false @@ -241,6 +373,8 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { } upstreamValues[channel.name] = upstreamValue + + confidenceValues[channel.name] = confidenceMap[channel.name][modelName] } shouldInclude := false @@ -262,6 +396,7 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { differences[modelName][ratioType] = dto.DifferenceItem{ Current: localValue, Upstreams: upstreamValues, + Confidence: confidenceValues, } } } @@ -283,9 +418,26 @@ func buildDifferences(localData map[string]any, successfulChannels []struct { for chName := range item.Upstreams { if !channelHasDiff[chName] { delete(item.Upstreams, chName) + delete(item.Confidence, chName) } } - differences[modelName][ratioType] = item + + allSame := true + for _, v := range item.Upstreams { + if v != "same" { + allSame = false + break + } + } + if len(item.Upstreams) == 0 || allSame { + delete(ratioMap, ratioType) + } else { + differences[modelName][ratioType] = item + } + } + + if len(ratioMap) == 0 { + delete(differences, modelName) } } diff --git a/dto/ratio_sync.go b/dto/ratio_sync.go index 55a89025..6315f31a 100644 --- a/dto/ratio_sync.go +++ b/dto/ratio_sync.go @@ -1,18 +1,7 @@ package dto -// UpstreamDTO 提交到后端同步倍率的上游渠道信息 -// Endpoint 可以为空,后端会默认使用 /api/ratio_config -// BaseURL 必须以 http/https 开头,不要以 / 结尾 -// 例如: https://api.example.com -// Endpoint: /api/ratio_config -// 提交示例: -// { -// "name": "openai", -// "base_url": "https://api.openai.com", -// "endpoint": "/ratio_config" -// } - type UpstreamDTO struct { + ID int `json:"id,omitempty"` Name string `json:"name" binding:"required"` BaseURL string `json:"base_url" binding:"required"` Endpoint string `json:"endpoint"` @@ -20,6 +9,7 @@ type UpstreamDTO struct { type UpstreamRequest struct { ChannelIDs []int64 `json:"channel_ids"` + Upstreams []UpstreamDTO `json:"upstreams"` Timeout int `json:"timeout"` } @@ -37,10 +27,9 @@ type TestResult struct { type DifferenceItem struct { Current interface{} `json:"current"` Upstreams map[string]interface{} `json:"upstreams"` + Confidence map[string]bool `json:"confidence"` } -// SyncableChannel 可同步的渠道信息(base_url 不为空) - type SyncableChannel struct { ID int `json:"id"` Name string `json:"name"` diff --git a/web/src/components/settings/ChannelSelectorModal.js b/web/src/components/settings/ChannelSelectorModal.js index 573329b3..a09eff1c 100644 --- a/web/src/components/settings/ChannelSelectorModal.js +++ b/web/src/components/settings/ChannelSelectorModal.js @@ -1,115 +1,183 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; +import { isMobile } from '../../helpers'; import { Modal, - Transfer, + Table, Input, Space, - Checkbox, - Avatar, Highlight, + Select, + Tag, } from '@douyinfe/semi-ui'; -import { IconClose } from '@douyinfe/semi-icons'; +import { IconSearch } from '@douyinfe/semi-icons'; +import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react'; -const CHANNEL_STATUS_CONFIG = { - 1: { color: 'green', text: '启用' }, - 2: { color: 'red', text: '禁用' }, - 3: { color: 'amber', text: '自禁' }, - default: { color: 'grey', text: '未知' } -}; - -const getChannelStatusConfig = (status) => { - return CHANNEL_STATUS_CONFIG[status] || CHANNEL_STATUS_CONFIG.default; -}; - -export default function ChannelSelectorModal({ - t, +const ChannelSelectorModal = forwardRef(({ visible, onCancel, onOk, - allChannels = [], - selectedChannelIds = [], + allChannels, + selectedChannelIds, setSelectedChannelIds, channelEndpoints, updateChannelEndpoint, -}) { + t, +}, ref) => { const [searchText, setSearchText] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); - const ChannelInfo = ({ item, showEndpoint = false, isSelected = false }) => { - const channelId = item.key || item.value; - const currentEndpoint = channelEndpoints[channelId]; - const baseUrl = item._originalData?.base_url || ''; - const status = item._originalData?.status || 0; - const statusConfig = getChannelStatusConfig(status); + const [filteredData, setFilteredData] = useState([]); - return ( - <> - - {statusConfig.text} - -
-
- {isSelected ? ( - item.label - ) : ( - - )} -
-
- - {isSelected ? ( - baseUrl - ) : ( - - )} - - {showEndpoint && ( - updateChannelEndpoint(channelId, value)} - placeholder="/api/ratio_config" - className="flex-1 text-xs" - style={{ fontSize: '12px' }} - /> - )} - {isSelected && !showEndpoint && ( - - {currentEndpoint} - - )} -
-
- - ); + useImperativeHandle(ref, () => ({ + resetPagination: () => { + setCurrentPage(1); + setSearchText(''); + }, + })); + + useEffect(() => { + if (!allChannels) return; + + const searchLower = searchText.trim().toLowerCase(); + const matched = searchLower + ? allChannels.filter((item) => { + const name = (item.label || '').toLowerCase(); + const baseUrl = (item._originalData?.base_url || '').toLowerCase(); + return name.includes(searchLower) || baseUrl.includes(searchLower); + }) + : allChannels; + + setFilteredData(matched); + }, [allChannels, searchText]); + + const total = filteredData.length; + + const paginatedData = filteredData.slice( + (currentPage - 1) * pageSize, + currentPage * pageSize, + ); + + const updateEndpoint = (channelId, endpoint) => { + if (typeof updateChannelEndpoint === 'function') { + updateChannelEndpoint(channelId, endpoint); + } }; - const renderSourceItem = (item) => { + const renderEndpointCell = (text, record) => { + const channelId = record.key || record.value; + const currentEndpoint = channelEndpoints[channelId] || ''; + + const getEndpointType = (ep) => { + if (ep === '/api/ratio_config') return 'ratio_config'; + if (ep === '/api/pricing') return 'pricing'; + return 'custom'; + }; + + const currentType = getEndpointType(currentEndpoint); + + const handleTypeChange = (val) => { + if (val === 'ratio_config') { + updateEndpoint(channelId, '/api/ratio_config'); + } else if (val === 'pricing') { + updateEndpoint(channelId, '/api/pricing'); + } else { + if (currentType !== 'custom') { + updateEndpoint(channelId, ''); + } + } + }; + return ( -
- - - +
+ updateEndpoint(channelId, val)} + placeholder="/your/endpoint" + style={{ width: 160, fontSize: 12 }} + /> + )}
); }; - const renderSelectedItem = (item) => { - return ( -
- - -
- ); + const renderStatusCell = (status) => { + switch (status) { + case 1: + return ( + }> + {t('已启用')} + + ); + case 2: + return ( + }> + {t('已禁用')} + + ); + case 3: + return ( + }> + {t('自动禁用')} + + ); + default: + return ( + }> + {t('未知状态')} + + ); + } }; - const channelFilter = (input, item) => { - const searchLower = input.toLowerCase(); - return item.label.toLowerCase().includes(searchLower) || - (item._originalData?.base_url || '').toLowerCase().includes(searchLower); + const renderNameCell = (text) => ( + + ); + + const renderBaseUrlCell = (text) => ( + + ); + + const columns = [ + { + title: t('名称'), + dataIndex: 'label', + render: renderNameCell, + }, + { + title: t('源地址'), + dataIndex: '_originalData.base_url', + render: (_, record) => renderBaseUrlCell(record._originalData?.base_url || ''), + }, + { + title: t('状态'), + dataIndex: '_originalData.status', + render: (_, record) => renderStatusCell(record._originalData?.status || 0), + }, + { + title: t('同步接口'), + dataIndex: 'endpoint', + fixed: 'right', + render: renderEndpointCell, + }, + ]; + + const rowSelection = { + selectedRowKeys: selectedChannelIds, + onChange: (keys) => setSelectedChannelIds(keys), }; return ( @@ -118,26 +186,51 @@ export default function ChannelSelectorModal({ onCancel={onCancel} onOk={onOk} title={{t('选择同步渠道')}} - width={1000} + size={isMobile() ? 'full-width' : 'large'} + keepDOM + lazyRender={false} > - } + placeholder={t('搜索渠道名称或地址')} + value={searchText} + onChange={setSearchText} + showClear + className="!rounded-full" + /> + + t('第 {{start}} - {{end}} 条,共 {{total}} 条', { + start: page.currentStart, + end: page.currentEnd, + total: total, + }), + onChange: (page, size) => { + setCurrentPage(page); + setPageSize(size); + }, + onShowSizeChange: (curr, size) => { + setCurrentPage(1); + setPageSize(size); + }, }} + size="small" /> ); -} \ No newline at end of file +}); + +export default ChannelSelectorModal; \ No newline at end of file diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index 90921460..7aef69ce 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -114,7 +114,7 @@ const ChannelsTable = () => { ); case 2: return ( - }> + }> {t('已禁用')} ); diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index d9cfe1d8..70ce272d 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1701,5 +1701,14 @@ "充值分组倍率": "Recharge group ratio", "充值方式设置": "Recharge method settings", "更新支付设置": "Update payment settings", - "通知": "Notice" + "通知": "Notice", + "源地址": "Source address", + "同步接口": "Synchronization interface", + "置信度": "Confidence", + "谨慎": "Cautious", + "该数据可能不可信,请谨慎使用": "This data may not be reliable, please use with caution", + "可信": "Reliable", + "所有上游数据均可信": "All upstream data is reliable", + "以下上游数据可能不可信:": "The following upstream data may not be reliable: ", + "按倍率类型筛选": "Filter by ratio type" } \ No newline at end of file diff --git a/web/src/pages/Setting/Ratio/UpstreamRatioSync.js b/web/src/pages/Setting/Ratio/UpstreamRatioSync.js index c246a3fe..656e31ae 100644 --- a/web/src/pages/Setting/Ratio/UpstreamRatioSync.js +++ b/web/src/pages/Setting/Ratio/UpstreamRatioSync.js @@ -7,11 +7,15 @@ import { Checkbox, Form, Input, + Tooltip, + Select, } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; import { RefreshCcw, CheckSquare, + AlertTriangle, + CheckCircle, } from 'lucide-react'; import { API, showError, showSuccess, showWarning, stringToColor } from '../../../helpers'; import { DEFAULT_ENDPOINT } from '../../../constants'; @@ -49,6 +53,11 @@ export default function UpstreamRatioSync(props) { // 搜索相关状态 const [searchKeyword, setSearchKeyword] = useState(''); + // 倍率类型过滤 + const [ratioTypeFilter, setRatioTypeFilter] = useState(''); + + const channelSelectorRef = React.useRef(null); + const fetchAllChannels = async () => { setLoading(true); try { @@ -67,11 +76,16 @@ export default function UpstreamRatioSync(props) { setAllChannels(transferData); - const initialEndpoints = {}; - transferData.forEach(channel => { - initialEndpoints[channel.key] = DEFAULT_ENDPOINT; + // 合并已有 endpoints,避免每次打开弹窗都重置 + setChannelEndpoints(prev => { + const merged = { ...prev }; + transferData.forEach(channel => { + if (!merged[channel.key]) { + merged[channel.key] = DEFAULT_ENDPOINT; + } + }); + return merged; }); - setChannelEndpoints(initialEndpoints); } else { showError(res.data.message); } @@ -99,8 +113,15 @@ export default function UpstreamRatioSync(props) { const fetchRatiosFromChannels = async (channelList) => { setSyncLoading(true); + const upstreams = channelList.map(ch => ({ + id: ch.id, + name: ch.name, + base_url: ch.base_url, + endpoint: channelEndpoints[ch.id] || DEFAULT_ENDPOINT, + })); + const payload = { - channel_ids: channelList.map(ch => parseInt(ch.id)), + upstreams: upstreams, timeout: 10, }; @@ -215,13 +236,15 @@ export default function UpstreamRatioSync(props) { const renderHeader = () => (
-
+
@@ -268,6 +307,7 @@ export default function UpstreamRatioSync(props) { ratioType, current: diff.current, upstreams: diff.upstreams, + confidence: diff.confidence || {}, }); }); }); @@ -276,15 +316,20 @@ export default function UpstreamRatioSync(props) { }, [differences]); const filteredDataSource = useMemo(() => { - if (!searchKeyword.trim()) { + if (!searchKeyword.trim() && !ratioTypeFilter) { return dataSource; } - const keyword = searchKeyword.toLowerCase().trim(); - return dataSource.filter(item => - item.model.toLowerCase().includes(keyword) - ); - }, [dataSource, searchKeyword]); + return dataSource.filter(item => { + const matchesKeyword = !searchKeyword.trim() || + item.model.toLowerCase().includes(searchKeyword.toLowerCase().trim()); + + const matchesRatioType = !ratioTypeFilter || + item.ratioType === ratioTypeFilter; + + return matchesKeyword && matchesRatioType; + }); + }, [dataSource, searchKeyword, ratioTypeFilter]); const upstreamNames = useMemo(() => { const set = new Set(); @@ -330,6 +375,36 @@ export default function UpstreamRatioSync(props) { return {typeMap[text] || text}; }, }, + { + title: t('置信度'), + dataIndex: 'confidence', + render: (_, record) => { + const allConfident = Object.values(record.confidence || {}).every(v => v !== false); + + if (allConfident) { + return ( + + }> + {t('可信')} + + + ); + } else { + const untrustedSources = Object.entries(record.confidence || {}) + .filter(([_, isConfident]) => isConfident === false) + .map(([name]) => name) + .join(', '); + + return ( + + }> + {t('谨慎')} + + + ); + } + }, + }, { title: t('当前值'), dataIndex: 'current', @@ -404,6 +479,7 @@ export default function UpstreamRatioSync(props) { dataIndex: upName, render: (_, record) => { const upstreamVal = record.upstreams?.[upName]; + const isConfident = record.confidence?.[upName] !== false; if (upstreamVal === null || upstreamVal === undefined) { return {t('未设置')}; @@ -416,28 +492,35 @@ export default function UpstreamRatioSync(props) { const isSelected = resolutions[record.model]?.[record.ratioType] === upstreamVal; return ( - { - const isChecked = e.target.checked; - if (isChecked) { - selectValue(record.model, record.ratioType, upstreamVal); - } else { - setResolutions((prev) => { - const newRes = { ...prev }; - if (newRes[record.model]) { - delete newRes[record.model][record.ratioType]; - if (Object.keys(newRes[record.model]).length === 0) { - delete newRes[record.model]; +
+ { + const isChecked = e.target.checked; + if (isChecked) { + selectValue(record.model, record.ratioType, upstreamVal); + } else { + setResolutions((prev) => { + const newRes = { ...prev }; + if (newRes[record.model]) { + delete newRes[record.model][record.ratioType]; + if (Object.keys(newRes[record.model]).length === 0) { + delete newRes[record.model]; + } } - } - return newRes; - }); - } - }} - > - {upstreamVal} - + return newRes; + }); + } + }} + > + {upstreamVal} + + {!isConfident && ( + + + + )} +
); }, }; @@ -481,6 +564,13 @@ export default function UpstreamRatioSync(props) { setChannelEndpoints(prev => ({ ...prev, [channelId]: endpoint })); }, []); + const handleModalClose = () => { + setModalVisible(false); + if (channelSelectorRef.current) { + channelSelectorRef.current.resetPagination(); + } + }; + return ( <> @@ -488,9 +578,10 @@ export default function UpstreamRatioSync(props) { setModalVisible(false)} + onCancel={handleModalClose} onOk={confirmChannelSelection} allChannels={allChannels} selectedChannelIds={selectedChannelIds}