File size: 6,927 Bytes
a21c316 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | import { Pin, Check } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { PinnedQuotaModelsConfig } from '../../types/config';
import { MODEL_CONFIG } from '../../config/modelConfig';
import { useAccountStore } from '../../stores/useAccountStore';
interface PinnedQuotaModelsProps {
config: PinnedQuotaModelsConfig;
onChange: (config: PinnedQuotaModelsConfig) => void;
}
const PinnedQuotaModels = ({ config, onChange }: PinnedQuotaModelsProps) => {
const { t } = useTranslation();
const toggleModel = (model: string) => {
const currentModels = config.models || [];
let newModels: string[];
if (currentModels.includes(model)) {
// 至少保留一个模型
if (currentModels.length <= 1) return;
newModels = currentModels.filter(m => m !== model);
} else {
newModels = [...currentModels, model];
}
onChange({ ...config, models: newModels });
};
const { accounts } = useAccountStore();
const uniqueIds = new Set<string>();
// 先收集所有已知模型的 id 和 protectedKey,防止他们作为未知的 "动态抽出模型" 出现
Object.entries(MODEL_CONFIG).forEach(([id, cfg]) => {
uniqueIds.add(id.toLowerCase());
if (cfg.protectedKey) {
uniqueIds.add(cfg.protectedKey.toLowerCase());
}
});
const addedDisplayLabels = new Set<string>();
// 基础内置配置模型
const baseModels = Object.entries(MODEL_CONFIG)
.filter(([id, cfg]) => {
// 隐藏思考变体
if (id.includes('thinking')) return false;
const labelKey = (cfg.shortLabel || cfg.label).toLowerCase();
// 在这一层,如果展示用的 labelKey 已经被加过了,就不要重复加到外派的选项里了
if (addedDisplayLabels.has(labelKey)) return false;
addedDisplayLabels.add(labelKey);
return true;
})
.map(([id, cfg]) => ({
id,
label: id,
desc: cfg.shortLabel || cfg.label || t(cfg.i18nDescKey || cfg.i18nKey, cfg.label)
}));
// 提取所有账号的历史动态模型
const dynamicModels = accounts.flatMap(a => a.quota?.models || [])
.filter(m => {
const id = m.name.toLowerCase();
if (id.includes('thinking')) return false;
// 查重:避免内置里已经包含的模型或同名 id 重复
if (uniqueIds.has(id)) return false;
uniqueIds.add(id);
return true;
})
.map(m => ({
id: m.name.toLowerCase(),
label: m.name.toLowerCase(),
desc: m.display_name || t('settings.pinned_quota_models.dynamic', 'Dynamic Extracted Model')
}));
const modelOptions = [...baseModels, ...dynamicModels];
// [FIX] Ensure previously pinned but unknown/hidden models are still rendered so users can un-pin them
const currentChecked = config.models || [];
currentChecked.forEach(modelId => {
if (!modelOptions.some(m => m.id === modelId)) {
// 尝试在历史配额中找到它的真实名字 (为了应对如 thinking 模型被隐藏但在关注列表里等情况)
const quotaModel = accounts.flatMap(a => a.quota?.models || []).find(m => m.name.toLowerCase() === modelId.toLowerCase());
const cfg = MODEL_CONFIG[modelId.toLowerCase()];
modelOptions.push({
id: modelId,
label: modelId,
desc: quotaModel?.display_name || cfg?.shortLabel || cfg?.label || t('common.unknown', '未知')
});
}
});
return (
<div className="animate-in fade-in duration-500">
<div className="flex items-center gap-4">
{/* 图标部分 - 使用蓝紫色调 */}
<div className="w-10 h-10 rounded-xl bg-indigo-50 dark:bg-indigo-900/20 flex items-center justify-center text-indigo-500 group-hover:bg-indigo-500 group-hover:text-white transition-all duration-300">
<Pin size={20} />
</div>
<div>
<div className="font-bold text-gray-900 dark:text-gray-100">
{t('settings.pinned_quota_models.title')}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{t('settings.pinned_quota_models.desc')}
</p>
</div>
</div>
{/* 模型选择区域 */}
<div className="mt-5 pt-5 border-t border-gray-100 dark:border-base-200 space-y-4">
<div className="grid grid-cols-4 gap-2">
{modelOptions.map((model) => {
const isSelected = config.models?.includes(model.id);
return (
<div
key={model.id}
onClick={() => toggleModel(model.id)}
className={`
flex items-center justify-between p-2 rounded-lg border cursor-pointer transition-all duration-200
${isSelected
? 'bg-indigo-50 dark:bg-indigo-900/10 border-indigo-200 dark:border-indigo-800/50 text-indigo-700 dark:text-indigo-400'
: 'bg-gray-50/50 dark:bg-base-200/50 border-gray-100 dark:border-base-300/50 text-gray-500 hover:border-gray-200 dark:hover:border-base-300'}
`}
>
<div className="flex flex-col min-w-0">
<span className="text-[11px] font-bold truncate">
{model.label}
</span>
<span className="text-[9px] text-gray-400 dark:text-gray-500 mt-0.5 truncate">
{model.desc}
</span>
</div>
<div className={`
w-4 h-4 rounded-full flex items-center justify-center transition-all duration-300 flex-shrink-0 ml-1
${isSelected ? 'bg-indigo-500 text-white scale-100' : 'bg-gray-200 dark:bg-base-300 text-transparent scale-75 opacity-0'}
`}>
<Check size={10} strokeWidth={4} />
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
export default PinnedQuotaModels;
|