feat(model): 增加模型服务商选择功能

- 在模型配置中添加服务商选择选项
- 实现服务商切换时自动选择该服务商下的第一个可用模型
- 优化模型选择界面,仅显示当前选中服务商的模型
- 添加摘要模型服务商选择功能
- 移除授权页面的 Saas 相关功能
- 调整聊天页面的模型选择逻辑
This commit is contained in:
Tianyi 2025-03-01 22:19:09 +08:00
parent c9ad2dd607
commit 5b8b6de3b5
5 changed files with 135 additions and 91 deletions

View File

@ -2,22 +2,16 @@ import styles from "./auth.module.scss";
import { IconButton } from "./button";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Path, SAAS_CHAT_URL } from "../constant";
import { Path } from "../constant";
import { useAccessStore } from "../store";
import Locale from "../locales";
import Delete from "../icons/close.svg";
import Arrow from "../icons/arrow.svg";
import Logo from "../icons/logo.svg";
import { useMobileScreen } from "@/app/utils";
import BotIcon from "../icons/bot.svg";
import { getClientConfig } from "../config/client";
import { PasswordInput } from "./ui-lib";
import LeftIcon from "@/app/icons/left.svg";
import { safeLocalStorage } from "@/app/utils";
import {
trackSettingsPageGuideToCPaymentClick,
trackAuthorizationPageButtonToCPaymentClick,
} from "../utils/auth-settings-events";
import clsx from "clsx";
const storage = safeLocalStorage();
@ -25,19 +19,8 @@ const storage = safeLocalStorage();
export function AuthPage() {
const navigate = useNavigate();
const accessStore = useAccessStore();
const goHome = () => navigate(Path.Home);
const goChat = () => navigate(Path.Chat);
const goSaas = () => {
trackAuthorizationPageButtonToCPaymentClick();
window.location.href = SAAS_CHAT_URL;
};
const resetAccessCode = () => {
accessStore.update((access) => {
access.openaiApiKey = "";
access.accessCode = "";
});
}; // Reset access code to empty string
// Reset access code to empty string
useEffect(() => {
if (getClientConfig()?.isApp) {
@ -115,17 +98,10 @@ export function AuthPage() {
type="primary"
onClick={goChat}
/>
<IconButton
text={Locale.Auth.SaasTips}
onClick={() => {
goSaas();
}}
/>
</div>
</div>
);
}
function TopBanner() {
const [isHovered, setIsHovered] = useState(false);
const [isVisible, setIsVisible] = useState(true);
@ -159,31 +135,5 @@ function TopBanner() {
if (!isVisible) {
return null;
}
return (
<div
className={styles["top-banner"]}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className={clsx(styles["top-banner-inner"], "no-dark")}>
<Logo className={styles["top-banner-logo"]}></Logo>
<span>
{Locale.Auth.TopTips}
<a
href={SAAS_CHAT_URL}
rel="stylesheet"
onClick={() => {
trackSettingsPageGuideToCPaymentClick();
}}
>
{Locale.Settings.Access.SaasStart.ChatNow}
<Arrow style={{ marginLeft: "4px" }} />
</a>
</span>
</div>
{(isHovered || isMobile) && (
<Delete className={styles["top-banner-close"]} onClick={handleClose} />
)}
</div>
);
return null;
}

View File

@ -67,14 +67,14 @@ import {
copyToClipboard,
getMessageImages,
getMessageTextContent,
getModelSizes,
isDalle3,
isVisionModel,
safeLocalStorage,
getModelSizes,
supportsCustomSize,
useMobileScreen,
selectOrCopy,
showPlugins,
supportsCustomSize,
useMobileScreen,
} from "../utils";
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
@ -535,11 +535,10 @@ export function ChatActions(props: {
const defaultModel = filteredModels.find((m) => m.isDefault);
if (defaultModel) {
const arr = [
return [
defaultModel,
...filteredModels.filter((m) => m !== defaultModel),
];
return arr;
} else {
return filteredModels;
}
@ -553,6 +552,7 @@ export function ChatActions(props: {
return model?.displayName ?? "";
}, [models, currentModel, currentProviderName]);
const [showModelSelector, setShowModelSelector] = useState(false);
const [showProviderSelector, setShowProviderSelector] = useState(false);
const [showPluginSelector, setShowPluginSelector] = useState(false);
const [showUploadImage, setShowUploadImage] = useState(false);
@ -673,23 +673,55 @@ export function ChatActions(props: {
}}
/>
<ChatAction
onClick={() => setShowProviderSelector(true)}
text={currentProviderName}
icon={<BrainIcon />}
/>
<ChatAction
onClick={() => setShowModelSelector(true)}
text={currentModelName}
icon={<RobotIcon />}
/>
{showProviderSelector && (
<Selector
defaultSelectedValue={currentProviderName}
items={Object.entries(ServiceProvider).map(([name, value]) => ({
title: name,
value: value,
}))}
onClose={() => setShowProviderSelector(false)}
onSelection={(s) => {
if (s.length === 0) return;
const provider = s[0] as ServiceProvider;
chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.providerName = provider;
const filteredModels = models.filter(
(m) => m.available && m.provider?.providerName === provider,
);
if (filteredModels.length > 0) {
// 选择新的服务商后,自动选择该服务商下的第一个模型
session.mask.modelConfig.model = filteredModels[0]
.name as ModelType;
session.mask.syncGlobalConfig = false;
}
});
showToast(s[0]);
}}
/>
)}
{showModelSelector && (
<Selector
defaultSelectedValue={`${currentModel}@${currentProviderName}`}
items={models.map((m) => ({
title: `${m.displayName}${
m?.provider?.providerName
? " (" + m?.provider?.providerName + ")"
: ""
}`,
value: `${m.name}@${m?.provider?.providerName}`,
}))}
items={models
.filter((m) => m.provider?.providerName === currentProviderName)
.map((m) => ({
title: m.displayName,
value: `${m.name}@${m?.provider?.providerName}`,
}))}
onClose={() => setShowModelSelector(false)}
onSelection={(s) => {
if (s.length === 0) return;

View File

@ -5,7 +5,6 @@ import Locale from "../locales";
import { InputRange } from "./input-range";
import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks";
import { groupBy } from "lodash-es";
import styles from "./model-config.module.scss";
import { getModelProvider } from "../utils/model";
@ -14,15 +13,43 @@ export function ModelConfigList(props: {
updateConfig: (updater: (config: ModelConfig) => void) => void;
}) {
const allModels = useAllModels();
const groupModels = groupBy(
allModels.filter((v) => v.available),
"provider.providerName",
const filteredModels = allModels.filter(
(v) =>
v.available &&
v.provider?.providerName === props.modelConfig.providerName,
);
const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
return (
<>
<ListItem title={Locale.Settings.Access.Provider.Title}>
<Select
aria-label={Locale.Settings.Access.Provider.Title}
value={props.modelConfig.providerName}
onChange={(e) => {
const provider = e.currentTarget.value as ServiceProvider;
props.updateConfig((config) => {
config.providerName = provider;
const firstModelForProvider = allModels.find(
(m) => m.available && m.provider?.providerName === provider,
);
if (firstModelForProvider) {
config.model = ModalConfigValidator.model(
firstModelForProvider.name,
);
}
});
}}
>
{Object.entries(ServiceProvider).map(([k, v]) => (
<option value={v} key={k}>
{k}
</option>
))}
</Select>
</ListItem>
<ListItem title={Locale.Settings.Model}>
<Select
aria-label={Locale.Settings.Model}
@ -38,14 +65,10 @@ export function ModelConfigList(props: {
});
}}
>
{Object.keys(groupModels).map((providerName, index) => (
<optgroup label={providerName} key={index}>
{groupModels[providerName].map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}
</option>
))}
</optgroup>
{filteredModels.map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}
</option>
))}
</Select>
</ListItem>
@ -241,6 +264,37 @@ export function ModelConfigList(props: {
}
></input>
</ListItem>
<ListItem title={Locale.Settings.CompressProvider.Title}>
<Select
aria-label={Locale.Settings.CompressProvider.Title}
value={
props.modelConfig.compressProviderName || ServiceProvider.OpenAI
}
onChange={(e) => {
const provider = e.currentTarget.value as ServiceProvider;
props.updateConfig((config) => {
config.compressProviderName = provider;
// 如果选择了新的提供商,自动选择该提供商的第一个可用模型
if (provider) {
const firstModelForProvider = allModels.find(
(m) => m.available && m.provider?.providerName === provider,
);
if (firstModelForProvider) {
config.compressModel = ModalConfigValidator.model(
firstModelForProvider.name,
);
}
}
});
}}
>
{Object.entries(ServiceProvider).map(([k, v]) => (
<option value={v} key={k}>
{k}
</option>
))}
</Select>
</ListItem>
<ListItem
title={Locale.Settings.CompressModel.Title}
subTitle={Locale.Settings.CompressModel.SubTitle}
@ -260,10 +314,19 @@ export function ModelConfigList(props: {
}}
>
{allModels
.filter((v) => v.available)
.filter(
(v) =>
v.available &&
(!props.modelConfig.compressProviderName ||
v.provider?.providerName ===
props.modelConfig.compressProviderName),
)
.map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName})
{v.displayName}
{!props.modelConfig.compressProviderName
? `(${v.provider?.providerName})`
: ""}
</option>
))}
</Select>

View File

@ -25,9 +25,6 @@ const cn = {
Input: "在此处填写访问码",
Confirm: "确认",
Later: "稍后再说",
// SaasTips: "配置太麻烦,想要立即使用",
// TopTips:
// "🥳 NextChat AI 首发优惠,立刻解锁 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`,
@ -540,14 +537,15 @@ const cn = {
},
},
// 增加厂商选项
ServiceProvider: "服务提供商",
Model: "模型 (model)",
CompressModel: {
Title: "对话摘要模型",
SubTitle: "用于压缩历史记录、生成对话标题的模型",
},
CompressProvider: {
Title: "摘要模型服务商",
SubTitle: "选择生成摘要的模型服务商",
},
Temperature: {
Title: "随机性 (temperature)",
SubTitle: "值越大,回复越随机",
@ -629,7 +627,7 @@ const cn = {
Prompt: {
History: (content: string) => "这是历史聊天总结作为前情提要:" + content,
Topic:
"使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,不要加粗,如果没有主题,请直接返回“闲聊”",
'使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,不要加粗,如果没有主题,请直接返回"闲聊"',
Summarize:
"简要总结一下对话内容,用作后续的上下文提示 prompt控制在 200 字以内",
},

View File

@ -26,9 +26,6 @@ const en: LocaleType = {
Input: "access code",
Confirm: "Confirm",
Later: "Later",
SaasTips: "Too Complex, Use Immediately Now",
TopTips:
"🥳 NextChat AI launch promotion: Instantly unlock the latest models like OpenAI o1, GPT-4o, Claude-3.5!",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} messages`,
@ -626,6 +623,10 @@ const en: LocaleType = {
SubTitle: "Higher values result in more random responses",
},
},
CompressProvider: {
Title: "Summary Model Provider",
SubTitle: "Select a provider for the summary model",
},
},
Store: {
DefaultTopic: "New Conversation",