feat: 可视化模型重定向设置中,允许用户从拉取的模型列表里直接选择模型 - #2610
Conversation
WalkthroughAdded a per-field suffix hook to the JSONEditor and updated its internal call signatures; introduced a SingleModelSelectModal and integrated it into EditChannelModal to allow selecting a model for individual model_mapping keys via a per-entry "select model" button and modal flow. Changes
Sequence DiagramsequenceDiagram
participant User
participant JSONEditor
participant EditChannelModal
participant SingleModelSelectModal
User->>JSONEditor: Interacts with model_mapping entry
JSONEditor->>User: Renders string input with suffix button
User->>EditChannelModal: Clicks suffix "select model" button
EditChannelModal->>SingleModelSelectModal: Open modal with models list
SingleModelSelectModal->>SingleModelSelectModal: Filter / categorize models
User->>SingleModelSelectModal: Selects model and confirms
SingleModelSelectModal->>EditChannelModal: onConfirm(selectedModel)
EditChannelModal->>EditChannelModal: Update model_mapping and form state
EditChannelModal->>JSONEditor: Re-render with updated mapping
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
web/src/components/table/channels/modals/SingleModelSelectModal.jsx (1)
165-186: Consider using justkeyas the unique identifier.The
keyanditemKeyuse${key}_${index}which includes the index. Sincekeyis already unique (it comes fromObject.entries(modelsByCategory)where keys are unique), the index suffix is redundant.♻️ Suggested simplification
- <Collapse.Panel - key={`${key}_${index}`} - itemKey={`${key}_${index}`} + <Collapse.Panel + key={key} + itemKey={key}web/src/components/table/channels/modals/EditChannelModal.jsx (1)
187-193: Consider consolidating modal state into a single object.Multiple related state variables for the same modal could be consolidated for easier state management and to avoid potential race conditions when updating multiple states.
♻️ Alternative pattern using a single state object
const [modelMappingValueModal, setModelMappingValueModal] = useState({ visible: false, models: [], key: '', selected: '', }); // Usage: setModelMappingValueModal({ visible: true, models: normalizedModelsToUse, key: mappingKey, selected: currentValue, });
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
web/src/components/common/ui/JSONEditor.jsxweb/src/components/table/channels/modals/EditChannelModal.jsxweb/src/components/table/channels/modals/SingleModelSelectModal.jsx
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: AAEE86
Repo: QuantumNous/new-api PR: 1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the applyModelMapping function transforms the models list by replacing original model names (mapping values) with display names (mapping keys). The database stores this transformed list containing mapped keys. On channel load, data.models contains these mapped display names, making the initialization filter if (data.models.includes(key)) correct.
Learnt from: AAEE86
Repo: QuantumNous/new-api PR: 1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the database stores mapped keys (display names) in the models field after applying model mapping transformations. When loading a channel, data.models contains the mapped keys, not the original model names. The filtering logic if (data.models.includes(key)) in the initialization is correct.
📚 Learning: 2025-08-27T02:15:25.448Z
Learnt from: AAEE86
Repo: QuantumNous/new-api PR: 1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the applyModelMapping function transforms the models list by replacing original model names (mapping values) with display names (mapping keys). The database stores this transformed list containing mapped keys. On channel load, data.models contains these mapped display names, making the initialization filter if (data.models.includes(key)) correct.
Applied to files:
web/src/components/table/channels/modals/SingleModelSelectModal.jsxweb/src/components/table/channels/modals/EditChannelModal.jsx
📚 Learning: 2025-08-27T02:15:25.448Z
Learnt from: AAEE86
Repo: QuantumNous/new-api PR: 1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the database stores mapped keys (display names) in the models field after applying model mapping transformations. When loading a channel, data.models contains the mapped keys, not the original model names. The filtering logic if (data.models.includes(key)) in the initialization is correct.
Applied to files:
web/src/components/table/channels/modals/SingleModelSelectModal.jsxweb/src/components/table/channels/modals/EditChannelModal.jsx
🧬 Code graph analysis (2)
web/src/components/table/channels/modals/SingleModelSelectModal.jsx (2)
web/src/components/common/ui/JSONEditor.jsx (2)
useTranslation(66-66)Typography(41-41)web/src/components/table/channels/modals/EditChannelModal.jsx (3)
useTranslation(126-126)isMobile(130-130)Typography(79-79)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
web/src/components/table/channels/modals/SingleModelSelectModal.jsx (2)
SingleModelSelectModal(38-193)selectedModel(55-55)
🔇 Additional comments (13)
web/src/components/common/ui/JSONEditor.jsx (3)
63-63: Well-designed optional extension point.The
renderStringValueSuffixprop provides a clean, composable way to inject custom suffix UI into string value inputs without modifying the component's core logic. The optional prop pattern maintains backward compatibility with existing consumers.
339-407: Clean implementation of per-field suffix rendering.The signature update to include
pairKeyand the suffix injection via optional chaining are both well-executed. The?.()pattern ensures graceful handling when the prop is not provided.
475-477: Call site properly updated.The internal call to
renderValueInputcorrectly passespair.keyas the second argument, aligning with the updated function signature.web/src/components/table/channels/modals/SingleModelSelectModal.jsx (3)
38-62: Well-structured component initialization.The component properly handles edge cases with default props, normalizes and deduplicates the model list, and resets state appropriately when the modal becomes visible. The use of
useMemofornormalizedModelsensures efficient recalculation only whenmodelsprop changes.
70-105: Robust category-based model grouping.The categorization logic correctly handles:
- Skipping the 'all' category
- Grouping uncategorized models under a translated "其他" label
- Building category metadata with labels, icons, and model lists
The memoization ensures this potentially expensive operation is only recomputed when
filteredModelsortchanges.
151-159: Defensive Radio.Group onChange handling.The handler correctly normalizes the value from either an event object (
val.target.value) or a direct value, ensuring compatibility with different Semi UI versions or usage patterns.web/src/components/table/channels/modals/EditChannelModal.jsx (7)
49-49: Imports properly added for new functionality.The new imports (Tooltip, SingleModelSelectModal, IconSearch) are correctly placed and support the new model mapping value selection feature.
Also applies to: 59-59, 74-74
205-222: Enhanced defensive parsing for model mapping.The additional type checks (
Array.isArray(parsed)check andtypeof value === 'string'guard) improve robustness against malformed model_mapping values.
741-748: fetchUpstreamModelList now returns fetched models.Returning the unique models list enables callers to use the fetched data directly without relying solely on state updates, which is useful for the new
openModelMappingValueModalflow.
750-784: Well-structured model selection modal opener.The function:
- Validates the mapping key early
- Only proceeds for fetchable channel types
- Falls back to fetching models if the cache is empty
- Normalizes and deduplicates the final model list
- Pre-selects the current value if it exists in the list
The silent fetch approach prevents disruptive error popups while still allowing the flow to proceed when cached models are available.
2716-2736: Proper suffix button implementation.The button is correctly:
- Disabled when pairKey is empty (preventing invalid selections)
- Using
e.stopPropagation()to prevent input focus issues- Wrapped in a Tooltip for discoverability
- Only rendered for fetchable channel types
3251-3296: Robust onConfirm handler with proper state synchronization.The handler:
- Validates selected model
- Handles edge case where mapping key becomes invalid
- Safely parses existing model_mapping with multiple fallback paths
- Updates both
inputsstate viahandleInputChangeand form viaformApiRef.current.setValueThe dual update at lines 3289-3292 ensures consistency between component state and form state, though this pattern could be simplified if the form were the single source of truth.
1722-1745: LGTM on IO.NET banner restructure.The IO.NET channel banner rendering maintains functionality while adapting to the new component structure. The
closeIcon={null}and conditional button rendering are appropriate.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
439-452: Invalidate cached fetched models when credentials/type change.
The new per-entry selector reusesfetchedModels; if users edittype,base_url, orkey, stale options can appear. Clearing the cache on those inputs keeps the modal accurate.🛠️ Suggested fix
if (formApiRef.current) { formApiRef.current.setValue(name, value); } + if (['type', 'key', 'base_url'].includes(name)) { + setFetchedModels([]); + } if (name === 'models' && Array.isArray(value)) { value = Array.from(new Set(value.map((m) => (m || '').trim()))); }
🤖 Fix all issues with AI agents
In `@web/src/components/table/channels/modals/EditChannelModal.jsx`:
- Around line 2779-2847: The copy button's onClick handler calls the async
copy() without awaiting its result, so change the handler to an async function,
await copy(inputs.models.join(',')), check the returned boolean (const ok =
await copy(...)), and call showSuccess only if ok is true otherwise call
showError; also wrap the await in try/catch to handle thrown errors and call
showError on exception. Target the onClick handler that references
inputs.models, copy, showSuccess, and showError in EditChannelModal.jsx.
| theme='light' | ||
| onClick={() => setOllamaModalVisible(true)} | ||
| > | ||
| {group.name} | ||
| {t('Ollama 模型管理')} | ||
| </Button> | ||
| ))} | ||
| </Space> | ||
| } | ||
| /> | ||
| )} | ||
| <Button | ||
| size='small' | ||
| type='warning' | ||
| onClick={() => handleInputChange('models', [])} | ||
| > | ||
| {t('清除所有模型')} | ||
| </Button> | ||
| <Button | ||
| size='small' | ||
| type='tertiary' | ||
| onClick={() => { | ||
| if (inputs.models.length === 0) { | ||
| showInfo(t('没有模型可以复制')); | ||
| return; | ||
| } | ||
| try { | ||
| copy(inputs.models.join(',')); | ||
| showSuccess(t('模型列表已复制到剪贴板')); | ||
| } catch (error) { | ||
| showError(t('复制失败')); | ||
| } | ||
| }} | ||
| > | ||
| {t('复制所有模型')} | ||
| </Button> | ||
| {modelGroups && | ||
| modelGroups.length > 0 && | ||
| modelGroups.map((group) => ( | ||
| <Button | ||
| key={group.id} | ||
| size='small' | ||
| type='primary' | ||
| onClick={() => { | ||
| let items = []; | ||
| try { | ||
| if (Array.isArray(group.items)) { | ||
| items = group.items; | ||
| } else if ( | ||
| typeof group.items === 'string' | ||
| ) { | ||
| const parsed = JSON.parse( | ||
| group.items || '[]', | ||
| ); | ||
| if (Array.isArray(parsed)) items = parsed; | ||
| } | ||
| } catch {} | ||
| const current = | ||
| formApiRef.current?.getValue('models') || | ||
| inputs.models || | ||
| []; | ||
| const merged = Array.from( | ||
| new Set( | ||
| [...current, ...items] | ||
| .map((m) => (m || '').trim()) | ||
| .filter(Boolean), | ||
| ), | ||
| ); | ||
| handleInputChange('models', merged); | ||
| }} | ||
| > | ||
| {group.name} | ||
| </Button> | ||
| ))} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# First, let's find where copy is imported/defined
echo "=== Searching for copy function definition ==="
rg -n "export (async )?function copy|export const copy|^const copy\s*=" web/src --type=js --type=jsx --type=ts --type=tsx -A 3
echo -e "\n=== Searching for copy function in common utility locations ==="
fd -e "js" -e "jsx" -e "ts" -e "tsx" "copy|clipboard" web/src | head -20Repository: QuantumNous/new-api
Length of output: 154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for copy function in the entire web/src directory
echo "=== Searching for copy function definition ==="
rg -n "function copy\s*\(|const copy\s*=" web/src --type=js --type=jsx --type=ts --type=tsx -B 2 -A 10 | head -100Repository: QuantumNous/new-api
Length of output: 154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the specific file and check imports and usages of copy
echo "=== Checking EditChannelModal.jsx imports and copy usages ==="
head -100 web/src/components/table/channels/modals/EditChannelModal.jsx | grep -E "import|copy"
echo -e "\n=== Searching for all copy() calls in EditChannelModal.jsx ==="
rg -n "copy\(" web/src/components/table/channels/modals/EditChannelModal.jsx -B 2 -A 2Repository: QuantumNous/new-api
Length of output: 1692
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for copy function definition across the entire repository
echo "=== Searching for copy function definition ==="
rg -n "function copy|const copy\s*=" web/src -A 10 --max-count=20
echo -e "\n=== Look for utils/clipboard files ==="
find web/src -name "*copy*" -o -name "*clipboard*" | head -20Repository: QuantumNous/new-api
Length of output: 821
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the imports in EditChannelModal to see where copy comes from
echo "=== Finding copy import source ==="
grep -n "from.*copy\|import.*copy" web/src/components/table/channels/modals/EditChannelModal.jsx
# Then search for that module
echo -e "\n=== Searching nearby imports ==="
head -50 web/src/components/table/channels/modals/EditChannelModal.jsx | grep -A 5 "import {"Repository: QuantumNous/new-api
Length of output: 97
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Get the full copy function
sed -n '72,100p' web/src/helpers/utils.jsxRepository: QuantumNous/new-api
Length of output: 930
Make the copy handler async and await the result.
The copy() function is async (defined in web/src/helpers/utils.jsx:72) and returns a boolean indicating success/failure. At line 2731 elsewhere in this file, it's correctly awaited with result checking (const ok = await copy(...)). However, at line 2801, the same function is called without awaiting, and success is shown immediately without checking the result. This means the user sees a success message before the copy operation completes, and errors are never detected.
🛠️ Suggested fix
- <Button
+ <Button
size='small'
type='tertiary'
- onClick={() => {
+ onClick={async () => {
if (inputs.models.length === 0) {
showInfo(t('没有模型可以复制'));
return;
}
try {
- copy(inputs.models.join(','));
- showSuccess(t('模型列表已复制到剪贴板'));
+ const ok = await copy(inputs.models.join(','));
+ if (ok) {
+ showSuccess(t('模型列表已复制到剪贴板'));
+ } else {
+ showError(t('复制失败'));
+ }
} catch (error) {
showError(t('复制失败'));
}
}}
>🤖 Prompt for AI Agents
In `@web/src/components/table/channels/modals/EditChannelModal.jsx` around lines
2779 - 2847, The copy button's onClick handler calls the async copy() without
awaiting its result, so change the handler to an async function, await
copy(inputs.models.join(',')), check the returned boolean (const ok = await
copy(...)), and call showSuccess only if ok is true otherwise call showError;
also wrap the await in try/catch to handle thrown errors and call showError on
exception. Target the onClick handler that references inputs.models, copy,
showSuccess, and showError in EditChannelModal.jsx.


Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.