feat: add multi-model support

This commit is contained in:
Yidadaa
2023-09-26 00:19:21 +08:00
parent b90dfb48ee
commit 5610f423d0
62 changed files with 1439 additions and 940 deletions

View File

@@ -3,7 +3,7 @@ import { IconButton } from "./button";
import { useNavigate } from "react-router-dom";
import { Path } from "../constant";
import { useAccessStore } from "../store";
import { useAccessStore, useAppConfig, useChatStore } from "../store";
import Locale from "../locales";
import BotIcon from "../icons/bot.svg";
@@ -13,10 +13,14 @@ import { getClientConfig } from "../config/client";
export function AuthPage() {
const navigate = useNavigate();
const access = useAccessStore();
const config = useAppConfig();
const goHome = () => navigate(Path.Home);
const goChat = () => navigate(Path.Chat);
const resetAccessCode = () => { access.updateCode(""); access.updateToken(""); }; // Reset access code to empty string
const resetAccessCode = () => {
access.update((config) => (config.accessCode = ""));
config.update((config) => (config.providerConfig.openai.apiKey = ""));
}; // Reset access code to empty string
useEffect(() => {
if (getClientConfig()?.isApp) {
@@ -40,7 +44,9 @@ export function AuthPage() {
placeholder={Locale.Auth.Input}
value={access.accessCode}
onChange={(e) => {
access.updateCode(e.currentTarget.value);
access.update(
(config) => (config.accessCode = e.currentTarget.value),
);
}}
/>
{!access.hideUserApiKey ? (
@@ -50,9 +56,12 @@ export function AuthPage() {
className={styles["auth-input"]}
type="password"
placeholder={Locale.Settings.Token.Placeholder}
value={access.token}
value={config.providerConfig.openai.apiKey}
onChange={(e) => {
access.updateToken(e.currentTarget.value);
config.update(
(config) =>
(config.providerConfig.openai.apiKey = e.currentTarget.value),
);
}}
/>
</>

View File

@@ -39,6 +39,9 @@ export function ChatItem(props: {
});
}
}, [props.selected]);
const modelConfig = useChatStore().extractModelConfig(props.mask.config);
return (
<Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => (
@@ -60,7 +63,10 @@ export function ChatItem(props: {
{props.narrow ? (
<div className={styles["chat-item-narrow"]}>
<div className={styles["chat-item-avatar"] + " no-dark"}>
<MaskAvatar mask={props.mask} />
<MaskAvatar
avatar={props.mask.avatar}
model={modelConfig.model}
/>
</div>
<div className={styles["chat-item-narrow-count"]}>
{props.count}

View File

@@ -1,12 +1,5 @@
import { useDebouncedCallback } from "use-debounce";
import React, {
useState,
useRef,
useEffect,
useMemo,
useCallback,
Fragment,
} from "react";
import React, { useState, useRef, useEffect, useMemo, Fragment } from "react";
import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg";
@@ -37,15 +30,12 @@ import RobotIcon from "../icons/robot.svg";
import {
ChatMessage,
SubmitKey,
useChatStore,
BOT_HELLO,
createMessage,
useAccessStore,
Theme,
useAppConfig,
DEFAULT_TOPIC,
ModelType,
} from "../store";
import {
@@ -57,7 +47,7 @@ import {
import dynamic from "next/dynamic";
import { ChatControllerPool } from "../client/controller";
import { ChatControllerPool } from "../client/common/controller";
import { Prompt, usePromptStore } from "../store/prompt";
import Locale from "../locales";
@@ -73,11 +63,10 @@ import {
showPrompt,
showToast,
} from "./ui-lib";
import { useLocation, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import {
CHAT_PAGE_SIZE,
LAST_INPUT_KEY,
MAX_RENDER_MSG_COUNT,
Path,
REQUEST_TIMEOUT_MS,
UNFINISHED_INPUT,
@@ -89,6 +78,8 @@ import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
import { prettyObject } from "../utils/format";
import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client";
import { deepClone } from "../utils/clone";
import { SubmitKey, Theme } from "../typing";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
@@ -142,7 +133,7 @@ export function SessionConfigModel(props: { onClose: () => void }) {
}}
shouldSyncFromGlobal
extraListItems={
session.mask.modelConfig.sendMemory ? (
session.mask.config.chatConfig.sendMemory ? (
<ListItem
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
@@ -427,17 +418,19 @@ export function ChatActions(props: {
// stop all responses
const couldStop = ChatControllerPool.hasPending();
const stopAll = () => ChatControllerPool.stopAll();
const client = chatStore.getClient();
const modelConfig = chatStore.getCurrentModelConfig();
const currentModel = modelConfig.model;
// switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model;
const models = useMemo(
() =>
config
.allModels()
.filter((m) => m.available)
.map((m) => m.name),
[config],
);
const [models, setModels] = useState<string[]>([]);
useEffect(() => {
client
.models()
.then((_models) =>
setModels(_models.filter((v) => v.available).map((v) => v.name)),
);
}, []);
const [showModelSelector, setShowModelSelector] = useState(false);
return (
@@ -526,7 +519,7 @@ export function ChatActions(props: {
onSelection={(s) => {
if (s.length === 0) return;
chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.model = s[0] as ModelType;
chatStore.extractModelConfig(session.mask.config).model = s[0];
session.mask.syncGlobalConfig = false;
});
showToast(s[0]);
@@ -603,6 +596,9 @@ function _Chat() {
type RenderMessage = ChatMessage & { preview?: boolean };
const chatStore = useChatStore();
const modelConfig = chatStore.getCurrentModelConfig();
const maskConfig = chatStore.getCurrentMaskConfig();
const session = chatStore.currentSession();
const config = useAppConfig();
const fontSize = config.fontSize;
@@ -747,7 +743,7 @@ function _Chat() {
// auto sync mask config from global config
if (session.mask.syncGlobalConfig) {
console.log("[Mask] syncing from global, name = ", session.mask.name);
session.mask.modelConfig = { ...config.modelConfig };
session.mask.config = deepClone(config.globalMaskConfig);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -979,7 +975,7 @@ function _Chat() {
console.log("[Command] got code from url: ", text);
showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => {
if (res) {
accessStore.updateCode(text);
accessStore.update((config) => (config.accessCode = text));
}
});
},
@@ -999,10 +995,10 @@ function _Chat() {
).then((res) => {
if (!res) return;
if (payload.key) {
accessStore.updateToken(payload.key);
// TODO: auto-fill openai api key here, must specific provider type
}
if (payload.url) {
accessStore.updateOpenAiUrl(payload.url);
// TODO: auto-fill openai url here, must specific provider type
}
});
}
@@ -1159,7 +1155,10 @@ function _Chat() {
{["system"].includes(message.role) ? (
<Avatar avatar="2699-fe0f" />
) : (
<MaskAvatar mask={session.mask} />
<MaskAvatar
avatar={session.mask.avatar}
model={modelConfig.model}
/>
)}
</>
)}

View File

@@ -0,0 +1,171 @@
import {
ChatConfig,
LLMProvider,
LLMProviders,
ModelConfig,
ProviderConfig,
} from "@/app/store";
import { Updater } from "@/app/typing";
import { OpenAIModelConfig } from "./openai/model";
import { OpenAIProviderConfig } from "./openai/provider";
import { ListItem, Select } from "../ui-lib";
import Locale from "@/app/locales";
import { InputRange } from "../input-range";
export function ModelConfigList(props: {
provider: LLMProvider;
config: ModelConfig;
updateConfig: Updater<ModelConfig>;
}) {
if (props.provider === "openai") {
return (
<OpenAIModelConfig
config={props.config.openai}
updateConfig={(update) => {
props.updateConfig((config) => update(config.openai));
}}
models={[
{
name: "gpt-3.5-turbo",
available: true,
},
{
name: "gpt-4",
available: true,
},
]}
/>
);
}
return null;
}
export function ProviderConfigList(props: {
provider: LLMProvider;
config: ProviderConfig;
updateConfig: Updater<ProviderConfig>;
}) {
if (props.provider === "openai") {
return (
<OpenAIProviderConfig
config={props.config.openai}
updateConfig={(update) => {
props.updateConfig((config) => update(config.openai));
}}
/>
);
}
return null;
}
export function ProviderSelectItem(props: {
value: LLMProvider;
update: (value: LLMProvider) => void;
}) {
return (
<ListItem title="服务提供商" subTitle="切换不同的模型提供商">
<Select
value={props.value}
onChange={(e) => {
props.update(e.target.value as LLMProvider);
}}
>
{LLMProviders.map(([k, v]) => (
<option value={v} key={k}>
{k}
</option>
))}
</Select>
</ListItem>
);
}
export function ChatConfigList(props: {
config: ChatConfig;
updateConfig: (updater: (config: ChatConfig) => void) => void;
}) {
return (
<>
<ListItem
title={Locale.Settings.InjectSystemPrompts.Title}
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
>
<input
type="checkbox"
checked={props.config.enableInjectSystemPrompts}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.enableInjectSystemPrompts = e.currentTarget.checked),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.InputTemplate.Title}
subTitle={Locale.Settings.InputTemplate.SubTitle}
>
<input
type="text"
value={props.config.template}
onChange={(e) =>
props.updateConfig(
(config) => (config.template = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
>
<InputRange
title={props.config.historyMessageCount.toString()}
value={props.config.historyMessageCount}
min="0"
max="64"
step="1"
onChange={(e) =>
props.updateConfig(
(config) => (config.historyMessageCount = e.target.valueAsNumber),
)
}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.CompressThreshold.Title}
subTitle={Locale.Settings.CompressThreshold.SubTitle}
>
<input
type="number"
min={500}
max={4000}
value={props.config.compressMessageLengthThreshold}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.compressMessageLengthThreshold =
e.currentTarget.valueAsNumber),
)
}
></input>
</ListItem>
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
<input
type="checkbox"
checked={props.config.sendMemory}
onChange={(e) =>
props.updateConfig(
(config) => (config.sendMemory = e.currentTarget.checked),
)
}
></input>
</ListItem>
</>
);
}

View File

@@ -0,0 +1,113 @@
import { ModelConfig } from "@/app/store";
import { ModelConfigProps } from "../types";
import { ListItem, Select } from "../../ui-lib";
import Locale from "@/app/locales";
import { InputRange } from "../../input-range";
export function OpenAIModelConfig(
props: ModelConfigProps<ModelConfig["openai"]>,
) {
return (
<>
<ListItem title={Locale.Settings.Model}>
<Select
value={props.config.model}
onChange={(e) => {
props.updateConfig(
(config) => (config.model = e.currentTarget.value),
);
}}
>
{props.models.map((v, i) => (
<option value={v.name} key={i} disabled={!v.available}>
{v.name}
</option>
))}
</Select>
</ListItem>
<ListItem
title={Locale.Settings.Temperature.Title}
subTitle={Locale.Settings.Temperature.SubTitle}
>
<InputRange
value={props.config.temperature?.toFixed(1)}
min="0"
max="1" // lets limit it to 0-1
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) => (config.temperature = e.currentTarget.valueAsNumber),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.TopP.Title}
subTitle={Locale.Settings.TopP.SubTitle}
>
<InputRange
value={(props.config.top_p ?? 1).toFixed(1)}
min="0"
max="1"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) => (config.top_p = e.currentTarget.valueAsNumber),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.MaxTokens.Title}
subTitle={Locale.Settings.MaxTokens.SubTitle}
>
<input
type="number"
min={100}
max={100000}
value={props.config.max_tokens}
onChange={(e) =>
props.updateConfig(
(config) => (config.max_tokens = e.currentTarget.valueAsNumber),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.PresencePenalty.Title}
subTitle={Locale.Settings.PresencePenalty.SubTitle}
>
<InputRange
value={props.config.presence_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.presence_penalty = e.currentTarget.valueAsNumber),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.FrequencyPenalty.Title}
subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
>
<InputRange
value={props.config.frequency_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.frequency_penalty = e.currentTarget.valueAsNumber),
);
}}
></InputRange>
</ListItem>
</>
);
}

View File

@@ -0,0 +1,71 @@
import { ProviderConfig } from "@/app/store";
import { ProviderConfigProps } from "../types";
import { ListItem, PasswordInput } from "../../ui-lib";
import Locale from "@/app/locales";
import { REMOTE_API_HOST } from "@/app/constant";
export function OpenAIProviderConfig(
props: ProviderConfigProps<ProviderConfig["openai"]>,
) {
return (
<>
<ListItem
title={Locale.Settings.Endpoint.Title}
subTitle={Locale.Settings.Endpoint.SubTitle}
>
<input
type="text"
value={props.config.endpoint}
placeholder={REMOTE_API_HOST}
onChange={(e) =>
props.updateConfig(
(config) => (config.endpoint = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Token.Title}
subTitle={Locale.Settings.Token.SubTitle}
>
<PasswordInput
value={props.config.apiKey}
type="text"
placeholder={Locale.Settings.Token.Placeholder}
onChange={(e) => {
props.updateConfig(
(config) => (config.apiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.CustomModel.Title}
subTitle={Locale.Settings.CustomModel.SubTitle}
>
<input
type="text"
value={props.config.customModels}
placeholder="model1,model2,model3"
onChange={(e) =>
props.updateConfig(
(config) => (config.customModels = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem title="自动拉取可用模型" subTitle="尝试拉取所有可用模型">
<input
type="checkbox"
checked={props.config.autoFetchModels}
onChange={(e) =>
props.updateConfig(
(config) => (config.autoFetchModels = e.currentTarget.checked),
)
}
></input>
</ListItem>
</>
);
}

View File

@@ -0,0 +1,14 @@
import { LLMModel } from "@/app/client";
import { Updater } from "@/app/typing";
export type ModelConfigProps<T> = {
models: LLMModel[];
config: T;
updateConfig: Updater<T>;
};
export type ProviderConfigProps<T> = {
readonly?: boolean;
config: T;
updateConfig: Updater<T>;
};

View File

@@ -28,7 +28,7 @@ export function AvatarPicker(props: {
);
}
export function Avatar(props: { model?: ModelType; avatar?: string }) {
export function Avatar(props: { model?: string; avatar?: string }) {
if (props.model) {
return (
<div className="no-dark">

View File

@@ -27,12 +27,12 @@ import { Avatar } from "./emoji";
import dynamic from "next/dynamic";
import NextImage from "next/image";
import { toBlob, toJpeg, toPng } from "html-to-image";
import { toBlob, toPng } from "html-to-image";
import { DEFAULT_MASK_AVATAR } from "../store/mask";
import { api } from "../client/api";
import { prettyObject } from "../utils/format";
import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
import { getClientConfig } from "../config/client";
import { api } from "../client";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
@@ -290,7 +290,7 @@ export function PreviewActions(props: {
setShouldExport(false);
api
.share(msgs)
.shareToShareGPT(msgs)
.then((res) => {
if (!res) return;
showModal({
@@ -403,6 +403,7 @@ export function ImagePreviewer(props: {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const mask = session.mask;
const modelConfig = chatStore.getCurrentModelConfig();
const config = useAppConfig();
const previewRef = useRef<HTMLDivElement>(null);
@@ -437,13 +438,13 @@ export function ImagePreviewer(props: {
showToast(Locale.Export.Image.Toast);
const dom = previewRef.current;
if (!dom) return;
const isApp = getClientConfig()?.isApp;
try {
const blob = await toPng(dom);
if (!blob) return;
if (isMobile || (isApp && window.__TAURI__)) {
if (isApp && window.__TAURI__) {
const result = await window.__TAURI__.dialog.save({
@@ -459,7 +460,7 @@ export function ImagePreviewer(props: {
},
],
});
if (result !== null) {
const response = await fetch(blob);
const buffer = await response.arrayBuffer();
@@ -526,7 +527,7 @@ export function ImagePreviewer(props: {
</div>
<div>
<div className={styles["chat-info-item"]}>
{Locale.Exporter.Model}: {mask.modelConfig.model}
{Locale.Exporter.Model}: {modelConfig.model}
</div>
<div className={styles["chat-info-item"]}>
{Locale.Exporter.Messages}: {props.messages.length}

View File

@@ -27,7 +27,6 @@ import { SideBar } from "./sidebar";
import { useAppConfig } from "../store/config";
import { AuthPage } from "./auth";
import { getClientConfig } from "../config/client";
import { api } from "../client/api";
import { useAccessStore } from "../store";
export function Loading(props: { noLogo?: boolean }) {
@@ -128,7 +127,8 @@ function Screen() {
const isHome = location.pathname === Path.Home;
const isAuth = location.pathname === Path.Auth;
const isMobileScreen = useMobileScreen();
const shouldTightBorder = getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
const shouldTightBorder =
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
useEffect(() => {
loadAsyncGoogleFont();
@@ -170,10 +170,7 @@ export function useLoadData() {
const config = useAppConfig();
useEffect(() => {
(async () => {
const models = await api.llm.models();
config.mergeModels(models);
})();
// TODO: fetch available models from server
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}
@@ -185,7 +182,7 @@ export function Home() {
useEffect(() => {
console.log("[Config] got config from build time", getClientConfig());
useAccessStore.getState().fetch();
useAccessStore.getState().fetchConfig();
}, []);
if (!useHasHydrated()) {

View File

@@ -21,7 +21,6 @@ import {
useAppConfig,
useChatStore,
} from "../store";
import { ROLES } from "../client/api";
import {
Input,
List,
@@ -36,19 +35,20 @@ import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
import { useNavigate } from "react-router-dom";
import chatStyle from "./chat.module.scss";
import { useEffect, useState } from "react";
import { useState } from "react";
import { copyToClipboard, downloadAs, readFromFile } from "../utils";
import { Updater } from "../typing";
import { ModelConfigList } from "./model-config";
import { FileName, Path } from "../constant";
import { BUILTIN_MASK_STORE } from "../masks";
import { nanoid } from "nanoid";
import {
DragDropContext,
Droppable,
Draggable,
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { ROLES } from "../client";
import { deepClone } from "../utils/clone";
import { ChatConfigList, ModelConfigList, ProviderSelectItem } from "./config";
// drag and drop helper function
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
@@ -58,11 +58,11 @@ function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
return result;
}
export function MaskAvatar(props: { mask: Mask }) {
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
<Avatar avatar={props.mask.avatar} />
export function MaskAvatar(props: { avatar: string; model: string }) {
return props.avatar !== DEFAULT_MASK_AVATAR ? (
<Avatar avatar={props.avatar} />
) : (
<Avatar model={props.mask.modelConfig.model} />
<Avatar model={props.model} />
);
}
@@ -74,14 +74,15 @@ export function MaskConfig(props: {
shouldSyncFromGlobal?: boolean;
}) {
const [showPicker, setShowPicker] = useState(false);
const modelConfig = useChatStore().extractModelConfig(props.mask.config);
const updateConfig = (updater: (config: ModelConfig) => void) => {
if (props.readonly) return;
const config = { ...props.mask.modelConfig };
updater(config);
const config = deepClone(props.mask.config);
updater(config.modelConfig);
props.updateMask((mask) => {
mask.modelConfig = config;
mask.config = config;
// if user changed current session mask, it will disable auto sync
mask.syncGlobalConfig = false;
});
@@ -123,7 +124,10 @@ export function MaskConfig(props: {
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
>
<MaskAvatar mask={props.mask} />
<MaskAvatar
avatar={props.mask.avatar}
model={modelConfig.model}
/>
</div>
</Popover>
</ListItem>
@@ -182,7 +186,7 @@ export function MaskConfig(props: {
) {
props.updateMask((mask) => {
mask.syncGlobalConfig = checked;
mask.modelConfig = { ...globalConfig.modelConfig };
mask.config = deepClone(globalConfig.globalMaskConfig);
});
} else if (!checked) {
props.updateMask((mask) => {
@@ -196,10 +200,28 @@ export function MaskConfig(props: {
</List>
<List>
<ProviderSelectItem
value={props.mask.config.provider}
update={(value) => {
props.updateMask((mask) => (mask.config.provider = value));
}}
/>
<ModelConfigList
modelConfig={{ ...props.mask.modelConfig }}
provider={props.mask.config.provider}
config={props.mask.config.modelConfig}
updateConfig={updateConfig}
/>
</List>
<List>
<ChatConfigList
config={props.mask.config.chatConfig}
updateConfig={(updater) => {
const chatConfig = deepClone(props.mask.config.chatConfig);
updater(chatConfig);
props.updateMask((mask) => (mask.config.chatConfig = chatConfig));
}}
/>
{props.extraListItems}
</List>
</>
@@ -398,7 +420,7 @@ export function MaskPage() {
setSearchText(text);
if (text.length > 0) {
const result = allMasks.filter((m) =>
m.name.toLowerCase().includes(text.toLowerCase())
m.name.toLowerCase().includes(text.toLowerCase()),
);
setSearchMasks(result);
} else {
@@ -523,14 +545,17 @@ export function MaskPage() {
<div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}>
<div className={styles["mask-icon"]}>
<MaskAvatar mask={m} />
<MaskAvatar
avatar={m.avatar}
model={chatStore.extractModelConfig(m.config).model}
/>
</div>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{m.name}</div>
<div className={styles["mask-info"] + " one-line"}>
{`${Locale.Mask.Item.Info(m.context.length)} / ${
ALL_LANG_OPTIONS[m.lang]
} / ${m.modelConfig.model}`}
} / ${chatStore.extractModelConfig(m.config).model}`}
</div>
</div>
</div>

View File

@@ -71,6 +71,7 @@ export function MessageSelector(props: {
onSelected?: (messages: ChatMessage[]) => void;
}) {
const chatStore = useChatStore();
const modelConfig = chatStore.getCurrentModelConfig();
const session = chatStore.currentSession();
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
const messages = session.messages.filter(
@@ -195,7 +196,10 @@ export function MessageSelector(props: {
{m.role === "user" ? (
<Avatar avatar={config.avatar}></Avatar>
) : (
<MaskAvatar mask={session.mask} />
<MaskAvatar
avatar={session.mask.avatar}
model={modelConfig.model}
/>
)}
</div>
<div className={styles["body"]}>

View File

@@ -4,10 +4,12 @@ import Locale from "../locales";
import { InputRange } from "./input-range";
import { ListItem, Select } from "./ui-lib";
export function ModelConfigList(props: {
export function _ModelConfigList(props: {
modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void;
}) {
return null;
/*
const config = useAppConfig();
return (
@@ -130,84 +132,8 @@ export function ModelConfigList(props: {
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.InjectSystemPrompts.Title}
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
>
<input
type="checkbox"
checked={props.modelConfig.enableInjectSystemPrompts}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.enableInjectSystemPrompts = e.currentTarget.checked),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.InputTemplate.Title}
subTitle={Locale.Settings.InputTemplate.SubTitle}
>
<input
type="text"
value={props.modelConfig.template}
onChange={(e) =>
props.updateConfig(
(config) => (config.template = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
>
<InputRange
title={props.modelConfig.historyMessageCount.toString()}
value={props.modelConfig.historyMessageCount}
min="0"
max="64"
step="1"
onChange={(e) =>
props.updateConfig(
(config) => (config.historyMessageCount = e.target.valueAsNumber),
)
}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.CompressThreshold.Title}
subTitle={Locale.Settings.CompressThreshold.SubTitle}
>
<input
type="number"
min={500}
max={4000}
value={props.modelConfig.compressMessageLengthThreshold}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.compressMessageLengthThreshold =
e.currentTarget.valueAsNumber),
)
}
></input>
</ListItem>
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
<input
type="checkbox"
checked={props.modelConfig.sendMemory}
onChange={(e) =>
props.updateConfig(
(config) => (config.sendMemory = e.currentTarget.checked),
)
}
></input>
</ListItem>
</>
);
*/
}

View File

@@ -29,9 +29,11 @@ function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
}
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
const modelConfig = useChatStore().extractModelConfig(props.mask.config);
return (
<div className={styles["mask"]} onClick={props.onClick}>
<MaskAvatar mask={props.mask} />
<MaskAvatar avatar={props.mask.avatar} model={modelConfig.model} />
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
</div>
);

View File

@@ -30,16 +30,15 @@ import {
showConfirm,
showToast,
} from "./ui-lib";
import { ModelConfigList } from "./model-config";
import { IconButton } from "./button";
import {
SubmitKey,
useChatStore,
Theme,
useUpdateStore,
useAccessStore,
useAppConfig,
LLMProvider,
LLMProviders,
} from "../store";
import Locale, {
@@ -61,6 +60,14 @@ import { useSyncStore } from "../store/sync";
import { nanoid } from "nanoid";
import { useMaskStore } from "../store/mask";
import { ProviderType } from "../utils/cloud";
import {
ChatConfigList,
ModelConfigList,
ProviderConfigList,
ProviderSelectItem,
} from "./config";
import { SubmitKey, Theme } from "../typing";
import { deepClone } from "../utils/clone";
function EditPromptModal(props: { id: string; onClose: () => void }) {
const promptStore = usePromptStore();
@@ -757,8 +764,7 @@ export function Settings() {
step="1"
onChange={(e) =>
updateConfig(
(config) =>
(config.fontSize = Number.parseInt(e.currentTarget.value)),
(config) => (config.fontSize = e.currentTarget.valueAsNumber),
)
}
></InputRange>
@@ -770,11 +776,14 @@ export function Settings() {
>
<input
type="checkbox"
checked={config.enableAutoGenerateTitle}
checked={
config.globalMaskConfig.chatConfig.enableAutoGenerateTitle
}
onChange={(e) =>
updateConfig(
(config) =>
(config.enableAutoGenerateTitle = e.currentTarget.checked),
(config.globalMaskConfig.chatConfig.enableAutoGenerateTitle =
e.currentTarget.checked),
)
}
></input>
@@ -877,7 +886,9 @@ export function Settings() {
type="text"
placeholder={Locale.Settings.AccessCode.Placeholder}
onChange={(e) => {
accessStore.updateCode(e.currentTarget.value);
accessStore.update(
(config) => (config.accessCode = e.currentTarget.value),
);
}}
/>
</ListItem>
@@ -885,36 +896,7 @@ export function Settings() {
<></>
)}
{!accessStore.hideUserApiKey ? (
<>
<ListItem
title={Locale.Settings.Endpoint.Title}
subTitle={Locale.Settings.Endpoint.SubTitle}
>
<input
type="text"
value={accessStore.openaiUrl}
placeholder="https://api.openai.com/"
onChange={(e) =>
accessStore.updateOpenAiUrl(e.currentTarget.value)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Token.Title}
subTitle={Locale.Settings.Token.SubTitle}
>
<PasswordInput
value={accessStore.token}
type="text"
placeholder={Locale.Settings.Token.Placeholder}
onChange={(e) => {
accessStore.updateToken(e.currentTarget.value);
}}
/>
</ListItem>
</>
) : null}
{!accessStore.hideUserApiKey ? <></> : null}
{!accessStore.hideBalanceQuery ? (
<ListItem
@@ -941,31 +923,44 @@ export function Settings() {
)}
</ListItem>
) : null}
<ListItem
title={Locale.Settings.CustomModel.Title}
subTitle={Locale.Settings.CustomModel.SubTitle}
>
<input
type="text"
value={config.customModels}
placeholder="model1,model2,model3"
onChange={(e) =>
config.update(
(config) => (config.customModels = e.currentTarget.value),
)
}
></input>
</ListItem>
</List>
<List>
<ProviderSelectItem
value={config.globalMaskConfig.provider}
update={(value) =>
config.update((_config) => {
_config.globalMaskConfig.provider = value;
})
}
/>
<ProviderConfigList
provider={config.globalMaskConfig.provider}
config={config.providerConfig}
updateConfig={(update) => {
config.update((_config) => update(_config.providerConfig));
}}
/>
<ModelConfigList
modelConfig={config.modelConfig}
provider={config.globalMaskConfig.provider}
config={config.globalMaskConfig.modelConfig}
updateConfig={(updater) => {
const modelConfig = { ...config.modelConfig };
const modelConfig = { ...config.globalMaskConfig.modelConfig };
updater(modelConfig);
config.update((config) => (config.modelConfig = modelConfig));
config.update(
(config) => (config.globalMaskConfig.modelConfig = modelConfig),
);
}}
/>
<ChatConfigList
config={config.globalMaskConfig.chatConfig}
updateConfig={(updater) => {
const chatConfig = deepClone(config.globalMaskConfig.chatConfig);
updater(chatConfig);
config.update(
(config) => (config.globalMaskConfig.chatConfig = chatConfig),
);
}}
/>
</List>