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 { IconButton } from "./button";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Path, SAAS_CHAT_URL } from "../constant"; import { Path } from "../constant";
import { useAccessStore } from "../store"; import { useAccessStore } from "../store";
import Locale from "../locales"; 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 { useMobileScreen } from "@/app/utils";
import BotIcon from "../icons/bot.svg"; import BotIcon from "../icons/bot.svg";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { PasswordInput } from "./ui-lib"; import { PasswordInput } from "./ui-lib";
import LeftIcon from "@/app/icons/left.svg"; import LeftIcon from "@/app/icons/left.svg";
import { safeLocalStorage } from "@/app/utils"; import { safeLocalStorage } from "@/app/utils";
import {
trackSettingsPageGuideToCPaymentClick,
trackAuthorizationPageButtonToCPaymentClick,
} from "../utils/auth-settings-events";
import clsx from "clsx"; import clsx from "clsx";
const storage = safeLocalStorage(); const storage = safeLocalStorage();
@ -25,19 +19,8 @@ const storage = safeLocalStorage();
export function AuthPage() { export function AuthPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const goHome = () => navigate(Path.Home);
const goChat = () => navigate(Path.Chat); const goChat = () => navigate(Path.Chat);
const goSaas = () => { // Reset access code to empty string
trackAuthorizationPageButtonToCPaymentClick();
window.location.href = SAAS_CHAT_URL;
};
const resetAccessCode = () => {
accessStore.update((access) => {
access.openaiApiKey = "";
access.accessCode = "";
});
}; // Reset access code to empty string
useEffect(() => { useEffect(() => {
if (getClientConfig()?.isApp) { if (getClientConfig()?.isApp) {
@ -115,17 +98,10 @@ export function AuthPage() {
type="primary" type="primary"
onClick={goChat} onClick={goChat}
/> />
<IconButton
text={Locale.Auth.SaasTips}
onClick={() => {
goSaas();
}}
/>
</div> </div>
</div> </div>
); );
} }
function TopBanner() { function TopBanner() {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [isVisible, setIsVisible] = useState(true); const [isVisible, setIsVisible] = useState(true);
@ -159,31 +135,5 @@ function TopBanner() {
if (!isVisible) { if (!isVisible) {
return null; return null;
} }
return ( return null;
<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>
);
} }

View File

@ -67,14 +67,14 @@ import {
copyToClipboard, copyToClipboard,
getMessageImages, getMessageImages,
getMessageTextContent, getMessageTextContent,
getModelSizes,
isDalle3, isDalle3,
isVisionModel, isVisionModel,
safeLocalStorage, safeLocalStorage,
getModelSizes,
supportsCustomSize,
useMobileScreen,
selectOrCopy, selectOrCopy,
showPlugins, showPlugins,
supportsCustomSize,
useMobileScreen,
} from "../utils"; } from "../utils";
import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
@ -535,11 +535,10 @@ export function ChatActions(props: {
const defaultModel = filteredModels.find((m) => m.isDefault); const defaultModel = filteredModels.find((m) => m.isDefault);
if (defaultModel) { if (defaultModel) {
const arr = [ return [
defaultModel, defaultModel,
...filteredModels.filter((m) => m !== defaultModel), ...filteredModels.filter((m) => m !== defaultModel),
]; ];
return arr;
} else { } else {
return filteredModels; return filteredModels;
} }
@ -553,6 +552,7 @@ export function ChatActions(props: {
return model?.displayName ?? ""; return model?.displayName ?? "";
}, [models, currentModel, currentProviderName]); }, [models, currentModel, currentProviderName]);
const [showModelSelector, setShowModelSelector] = useState(false); const [showModelSelector, setShowModelSelector] = useState(false);
const [showProviderSelector, setShowProviderSelector] = useState(false);
const [showPluginSelector, setShowPluginSelector] = useState(false); const [showPluginSelector, setShowPluginSelector] = useState(false);
const [showUploadImage, setShowUploadImage] = useState(false); const [showUploadImage, setShowUploadImage] = useState(false);
@ -673,23 +673,55 @@ export function ChatActions(props: {
}} }}
/> />
<ChatAction
onClick={() => setShowProviderSelector(true)}
text={currentProviderName}
icon={<BrainIcon />}
/>
<ChatAction <ChatAction
onClick={() => setShowModelSelector(true)} onClick={() => setShowModelSelector(true)}
text={currentModelName} text={currentModelName}
icon={<RobotIcon />} 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 && ( {showModelSelector && (
<Selector <Selector
defaultSelectedValue={`${currentModel}@${currentProviderName}`} defaultSelectedValue={`${currentModel}@${currentProviderName}`}
items={models.map((m) => ({ items={models
title: `${m.displayName}${ .filter((m) => m.provider?.providerName === currentProviderName)
m?.provider?.providerName .map((m) => ({
? " (" + m?.provider?.providerName + ")" title: m.displayName,
: "" value: `${m.name}@${m?.provider?.providerName}`,
}`, }))}
value: `${m.name}@${m?.provider?.providerName}`,
}))}
onClose={() => setShowModelSelector(false)} onClose={() => setShowModelSelector(false)}
onSelection={(s) => { onSelection={(s) => {
if (s.length === 0) return; if (s.length === 0) return;

View File

@ -5,7 +5,6 @@ import Locale from "../locales";
import { InputRange } from "./input-range"; import { InputRange } from "./input-range";
import { ListItem, Select } from "./ui-lib"; import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import { groupBy } from "lodash-es";
import styles from "./model-config.module.scss"; import styles from "./model-config.module.scss";
import { getModelProvider } from "../utils/model"; import { getModelProvider } from "../utils/model";
@ -14,15 +13,43 @@ export function ModelConfigList(props: {
updateConfig: (updater: (config: ModelConfig) => void) => void; updateConfig: (updater: (config: ModelConfig) => void) => void;
}) { }) {
const allModels = useAllModels(); const allModels = useAllModels();
const groupModels = groupBy( const filteredModels = allModels.filter(
allModels.filter((v) => v.available), (v) =>
"provider.providerName", v.available &&
v.provider?.providerName === props.modelConfig.providerName,
); );
const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`; const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`; const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
return ( 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}> <ListItem title={Locale.Settings.Model}>
<Select <Select
aria-label={Locale.Settings.Model} aria-label={Locale.Settings.Model}
@ -38,14 +65,10 @@ export function ModelConfigList(props: {
}); });
}} }}
> >
{Object.keys(groupModels).map((providerName, index) => ( {filteredModels.map((v, i) => (
<optgroup label={providerName} key={index}> <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{groupModels[providerName].map((v, i) => ( {v.displayName}
<option value={`${v.name}@${v.provider?.providerName}`} key={i}> </option>
{v.displayName}
</option>
))}
</optgroup>
))} ))}
</Select> </Select>
</ListItem> </ListItem>
@ -241,6 +264,37 @@ export function ModelConfigList(props: {
} }
></input> ></input>
</ListItem> </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 <ListItem
title={Locale.Settings.CompressModel.Title} title={Locale.Settings.CompressModel.Title}
subTitle={Locale.Settings.CompressModel.SubTitle} subTitle={Locale.Settings.CompressModel.SubTitle}
@ -260,10 +314,19 @@ export function ModelConfigList(props: {
}} }}
> >
{allModels {allModels
.filter((v) => v.available) .filter(
(v) =>
v.available &&
(!props.modelConfig.compressProviderName ||
v.provider?.providerName ===
props.modelConfig.compressProviderName),
)
.map((v, i) => ( .map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}> <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName}) {v.displayName}
{!props.modelConfig.compressProviderName
? `(${v.provider?.providerName})`
: ""}
</option> </option>
))} ))}
</Select> </Select>

View File

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

View File

@ -26,9 +26,6 @@ const en: LocaleType = {
Input: "access code", Input: "access code",
Confirm: "Confirm", Confirm: "Confirm",
Later: "Later", 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: { ChatItem: {
ChatItemCount: (count: number) => `${count} messages`, ChatItemCount: (count: number) => `${count} messages`,
@ -626,6 +623,10 @@ const en: LocaleType = {
SubTitle: "Higher values result in more random responses", SubTitle: "Higher values result in more random responses",
}, },
}, },
CompressProvider: {
Title: "Summary Model Provider",
SubTitle: "Select a provider for the summary model",
},
}, },
Store: { Store: {
DefaultTopic: "New Conversation", DefaultTopic: "New Conversation",