mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-22 13:40:16 +09:00
feat(model): 增加模型服务商选择功能
- 在模型配置中添加服务商选择选项 - 实现服务商切换时自动选择该服务商下的第一个可用模型 - 优化模型选择界面,仅显示当前选中服务商的模型 - 添加摘要模型服务商选择功能 - 移除授权页面的 Saas 相关功能 - 调整聊天页面的模型选择逻辑
This commit is contained in:
parent
c9ad2dd607
commit
5b8b6de3b5
@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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 字以内",
|
||||||
},
|
},
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user