diff --git a/src/web/pages/token-routes/AddChannelModal.tsx b/src/web/pages/token-routes/AddChannelModal.tsx index ddaefd1..36240a2 100644 --- a/src/web/pages/token-routes/AddChannelModal.tsx +++ b/src/web/pages/token-routes/AddChannelModal.tsx @@ -5,6 +5,7 @@ import { api } from '../../api.js'; import { useToast } from '../../components/Toast.js'; import { tr } from '../../i18n.js'; import type { RouteCandidateView, RouteAccountOption, RouteTokenOption } from '../helpers/routeModelCandidatesIndex.js'; +import type { RouteMissingTokenHint } from '../helpers/routeMissingTokenHints.js'; type ChannelSelection = { accountId: number; @@ -19,6 +20,9 @@ type AddChannelModalProps = { routeTitle: string; candidateView: RouteCandidateView; onSuccess: () => void; + missingTokenHints?: RouteMissingTokenHint[]; + onCreateTokenForMissing?: (accountId: number, modelName: string) => void; + existingChannelAccountIds?: Set; }; export default function AddChannelModal({ @@ -28,6 +32,9 @@ export default function AddChannelModal({ routeTitle, candidateView, onSuccess, + missingTokenHints, + onCreateTokenForMissing, + existingChannelAccountIds, }: AddChannelModalProps) { const toast = useToast(); const [searchQuery, setSearchQuery] = useState(''); @@ -42,6 +49,26 @@ export default function AddChannelModal({ ); }, [candidateView.accountOptions, searchQuery]); + const missingAccounts = useMemo(() => { + if (!missingTokenHints || missingTokenHints.length === 0) return []; + const seen = new Map(); + for (const hint of missingTokenHints) { + for (const account of hint.accounts) { + if (!seen.has(account.accountId)) { + const label = `${account.username || `account-${account.accountId}`} @ ${account.siteName}`; + seen.set(account.accountId, { accountId: account.accountId, label, modelName: hint.modelName }); + } + } + } + return Array.from(seen.values()); + }, [missingTokenHints]); + + const filteredMissingAccounts = useMemo(() => { + const q = searchQuery.trim().toLowerCase(); + if (!q) return missingAccounts; + return missingAccounts.filter((item) => item.label.toLowerCase().includes(q)); + }, [missingAccounts, searchQuery]); + const selectedCount = Object.keys(selectedAccounts).length; const toggleAccount = (account: RouteAccountOption) => { @@ -151,71 +178,110 @@ export default function AddChannelModal({
- {filteredAccounts.length === 0 ? ( + {filteredAccounts.length === 0 && filteredMissingAccounts.length === 0 ? (
- {candidateView.accountOptions.length === 0 ? tr('当前没有可用的账号') : tr('没有匹配的账号')} + {candidateView.accountOptions.length === 0 && missingAccounts.length === 0 + ? tr('当前没有可用的账号,请确认已有账号的令牌支持调用此模型') + : tr('没有匹配的账号')}
) : ( - filteredAccounts.map((account) => { - const isSelected = !!selectedAccounts[account.id]; - const tokens = candidateView.tokenOptionsByAccountId[account.id] || []; - const selection = selectedAccounts[account.id]; + <> + {filteredAccounts.map((account) => { + const isSelected = !!selectedAccounts[account.id]; + const tokens = candidateView.tokenOptionsByAccountId[account.id] || []; + const selection = selectedAccounts[account.id]; + const isExisting = existingChannelAccountIds?.has(account.id); - return ( -
+ return (
toggleAccount(account)} + key={account.id} + style={{ + padding: '8px 10px', + borderRadius: 'var(--radius-sm)', + border: `1px solid ${isSelected ? 'var(--color-primary)' : 'var(--color-border)'}`, + background: isSelected ? 'color-mix(in srgb, var(--color-primary) 6%, transparent)' : 'transparent', + cursor: 'pointer', + }} > - toggleAccount(account)} - style={{ cursor: 'pointer' }} - /> - {account.label} -
- - {isSelected && tokens.length > 0 && ( -
-
{tr('令牌')}:
- { - if (!selection?.tokenId) return '0'; - return `${selection.tokenId}::${selection.sourceModel || ''}`; - })()} - onChange={(nextValue) => { - if (nextValue === '0') { - updateTokenForAccount(account.id, 0, ''); - return; - } - const [tokenRaw, ...sourceParts] = nextValue.split('::'); - updateTokenForAccount(account.id, Number.parseInt(tokenRaw, 10) || 0, sourceParts.join('::')); - }} - options={[ - { value: '0', label: tr('默认令牌') }, - ...tokens.map((token: RouteTokenOption) => ({ - value: `${token.id}::${token.sourceModel || ''}`, - label: `${token.name}${token.isDefault ? '(默认)' : ''}${token.sourceModel ? ` [${token.sourceModel}]` : ''}`, - })), - ]} - placeholder={tr('选择令牌')} +
toggleAccount(account)} + > + toggleAccount(account)} + style={{ cursor: 'pointer' }} /> + {account.label} + {isExisting && ( + {tr('已添加')} + )}
- )} + + {isSelected && tokens.length > 0 && ( +
+
{tr('令牌')}:
+ { + if (!selection?.tokenId) return '0'; + return `${selection.tokenId}::${selection.sourceModel || ''}`; + })()} + onChange={(nextValue) => { + if (nextValue === '0') { + updateTokenForAccount(account.id, 0, ''); + return; + } + const [tokenRaw, ...sourceParts] = nextValue.split('::'); + updateTokenForAccount(account.id, Number.parseInt(tokenRaw, 10) || 0, sourceParts.join('::')); + }} + options={[ + { value: '0', label: tr('默认令牌') }, + ...tokens.map((token: RouteTokenOption) => ({ + value: `${token.id}::${token.sourceModel || ''}`, + label: `${token.name}${token.isDefault ? '(默认)' : ''}${token.sourceModel ? ` [${token.sourceModel}]` : ''}`, + })), + ]} + placeholder={tr('选择令牌')} + /> +
+ )} +
+ ); + })} + + {/* Missing token hints */} + {filteredMissingAccounts.length > 0 && ( +
+
+ {tr('以下账号可用此模型但缺少令牌')}: +
+ {filteredMissingAccounts.map((item) => ( +
+ {item.label} + {onCreateTokenForMissing && ( + + )} +
+ ))}
- ); - }) + )} + )}