This commit is contained in:
GH Action - Upstream Sync 2024-11-07 00:56:39 +00:00
commit 8de4b55cad
25 changed files with 254 additions and 151 deletions

View File

@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../config/server"; import { getServerSideConfig } from "../config/server";
import { OPENAI_BASE_URL, ServiceProvider } from "../constant"; import { OPENAI_BASE_URL, ServiceProvider } from "../constant";
import { isModelAvailableInServer } from "../utils/model";
import { cloudflareAIGatewayUrl } from "../utils/cloudflare"; import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
import { getModelProvider, isModelAvailableInServer } from "../utils/model";
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
@ -71,7 +71,7 @@ export async function requestOpenai(req: NextRequest) {
.filter((v) => !!v && !v.startsWith("-") && v.includes(modelName)) .filter((v) => !!v && !v.startsWith("-") && v.includes(modelName))
.forEach((m) => { .forEach((m) => {
const [fullName, displayName] = m.split("="); const [fullName, displayName] = m.split("=");
const [_, providerName] = fullName.split("@"); const [_, providerName] = getModelProvider(fullName);
if (providerName === "azure" && !displayName) { if (providerName === "azure" && !displayName) {
const [_, deployId] = (serverConfig?.azureUrl ?? "").split( const [_, deployId] = (serverConfig?.azureUrl ?? "").split(
"deployments/", "deployments/",

View File

@ -18,6 +18,8 @@ import {
trackSettingsPageGuideToCPaymentClick, trackSettingsPageGuideToCPaymentClick,
trackAuthorizationPageButtonToCPaymentClick, trackAuthorizationPageButtonToCPaymentClick,
} from "../utils/auth-settings-events"; } from "../utils/auth-settings-events";
import clsx from "clsx";
const storage = safeLocalStorage(); const storage = safeLocalStorage();
export function AuthPage() { export function AuthPage() {
@ -54,7 +56,7 @@ export function AuthPage() {
onClick={() => navigate(Path.Home)} onClick={() => navigate(Path.Home)}
></IconButton> ></IconButton>
</div> </div>
<div className={`no-dark ${styles["auth-logo"]}`}> <div className={clsx("no-dark", styles["auth-logo"])}>
<BotIcon /> <BotIcon />
</div> </div>
@ -163,7 +165,7 @@ function TopBanner() {
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >
<div className={`${styles["top-banner-inner"]} no-dark`}> <div className={clsx(styles["top-banner-inner"], "no-dark")}>
<Logo className={styles["top-banner-logo"]}></Logo> <Logo className={styles["top-banner-logo"]}></Logo>
<span> <span>
{Locale.Auth.TopTips} {Locale.Auth.TopTips}

View File

@ -2,6 +2,7 @@ import * as React from "react";
import styles from "./button.module.scss"; import styles from "./button.module.scss";
import { CSSProperties } from "react"; import { CSSProperties } from "react";
import clsx from "clsx";
export type ButtonType = "primary" | "danger" | null; export type ButtonType = "primary" | "danger" | null;
@ -22,12 +23,16 @@ export function IconButton(props: {
}) { }) {
return ( return (
<button <button
className={ className={clsx(
styles["icon-button"] + "clickable",
` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${ styles["icon-button"],
props.className ?? "" {
} clickable ${styles[props.type ?? ""]}` [styles.border]: props.bordered,
} [styles.shadow]: props.shadow,
},
styles[props.type ?? ""],
props.className,
)}
onClick={props.onClick} onClick={props.onClick}
title={props.title} title={props.title}
disabled={props.disabled} disabled={props.disabled}
@ -40,10 +45,9 @@ export function IconButton(props: {
{props.icon && ( {props.icon && (
<div <div
aria-label={props.text || props.title} aria-label={props.text || props.title}
className={ className={clsx(styles["icon-button-icon"], {
styles["icon-button-icon"] + "no-dark": props.type === "primary",
` ${props.type === "primary" && "no-dark"}` })}
}
> >
{props.icon} {props.icon}
</div> </div>

View File

@ -18,6 +18,7 @@ import { Mask } from "../store/mask";
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import { showConfirm } from "./ui-lib"; import { showConfirm } from "./ui-lib";
import { useMobileScreen } from "../utils"; import { useMobileScreen } from "../utils";
import clsx from "clsx";
export function ChatItem(props: { export function ChatItem(props: {
onClick?: () => void; onClick?: () => void;
@ -45,11 +46,11 @@ export function ChatItem(props: {
<Draggable draggableId={`${props.id}`} index={props.index}> <Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => ( {(provided) => (
<div <div
className={`${styles["chat-item"]} ${ className={clsx(styles["chat-item"], {
props.selected && [styles["chat-item-selected"]]:
(currentPath === Path.Chat || currentPath === Path.Home) && props.selected &&
styles["chat-item-selected"] (currentPath === Path.Chat || currentPath === Path.Home),
}`} })}
onClick={props.onClick} onClick={props.onClick}
ref={(ele) => { ref={(ele) => {
draggableRef.current = ele; draggableRef.current = ele;
@ -63,7 +64,7 @@ export function ChatItem(props: {
> >
{props.narrow ? ( {props.narrow ? (
<div className={styles["chat-item-narrow"]}> <div className={styles["chat-item-narrow"]}>
<div className={styles["chat-item-avatar"] + " no-dark"}> <div className={clsx(styles["chat-item-avatar"], "no-dark")}>
<MaskAvatar <MaskAvatar
avatar={props.mask.avatar} avatar={props.mask.avatar}
model={props.mask.modelConfig.model} model={props.mask.modelConfig.model}

View File

@ -120,6 +120,8 @@ import { createTTSPlayer } from "../utils/audio";
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts"; import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
import { isEmpty } from "lodash-es"; import { isEmpty } from "lodash-es";
import { getModelProvider } from "../utils/model";
import clsx from "clsx";
const localStorage = safeLocalStorage(); const localStorage = safeLocalStorage();
@ -148,7 +150,8 @@ export function SessionConfigModel(props: { onClose: () => void }) {
text={Locale.Chat.Config.Reset} text={Locale.Chat.Config.Reset}
onClick={async () => { onClick={async () => {
if (await showConfirm(Locale.Memory.ResetConfirm)) { if (await showConfirm(Locale.Memory.ResetConfirm)) {
chatStore.updateCurrentSession( chatStore.updateTargetSession(
session,
(session) => (session.memoryPrompt = ""), (session) => (session.memoryPrompt = ""),
); );
} }
@ -173,7 +176,10 @@ export function SessionConfigModel(props: { onClose: () => void }) {
updateMask={(updater) => { updateMask={(updater) => {
const mask = { ...session.mask }; const mask = { ...session.mask };
updater(mask); updater(mask);
chatStore.updateCurrentSession((session) => (session.mask = mask)); chatStore.updateTargetSession(
session,
(session) => (session.mask = mask),
);
}} }}
shouldSyncFromGlobal shouldSyncFromGlobal
extraListItems={ extraListItems={
@ -206,7 +212,7 @@ function PromptToast(props: {
<div className={styles["prompt-toast"]} key="prompt-toast"> <div className={styles["prompt-toast"]} key="prompt-toast">
{props.showToast && context.length > 0 && ( {props.showToast && context.length > 0 && (
<div <div
className={styles["prompt-toast-inner"] + " clickable"} className={clsx(styles["prompt-toast-inner"], "clickable")}
role="button" role="button"
onClick={() => props.setShowModal(true)} onClick={() => props.setShowModal(true)}
> >
@ -327,10 +333,9 @@ export function PromptHints(props: {
{props.prompts.map((prompt, i) => ( {props.prompts.map((prompt, i) => (
<div <div
ref={i === selectIndex ? selectedRef : null} ref={i === selectIndex ? selectedRef : null}
className={ className={clsx(styles["prompt-hint"], {
styles["prompt-hint"] + [styles["prompt-hint-selected"]]: i === selectIndex,
` ${i === selectIndex ? styles["prompt-hint-selected"] : ""}` })}
}
key={prompt.title + i.toString()} key={prompt.title + i.toString()}
onClick={() => props.onPromptSelect(prompt)} onClick={() => props.onPromptSelect(prompt)}
onMouseEnter={() => setSelectIndex(i)} onMouseEnter={() => setSelectIndex(i)}
@ -345,12 +350,14 @@ export function PromptHints(props: {
function ClearContextDivider() { function ClearContextDivider() {
const chatStore = useChatStore(); const chatStore = useChatStore();
const session = chatStore.currentSession();
return ( return (
<div <div
className={styles["clear-context"]} className={styles["clear-context"]}
onClick={() => onClick={() =>
chatStore.updateCurrentSession( chatStore.updateTargetSession(
session,
(session) => (session.clearContextIndex = undefined), (session) => (session.clearContextIndex = undefined),
) )
} }
@ -388,7 +395,7 @@ export function ChatAction(props: {
return ( return (
<div <div
className={`${styles["chat-input-action"]} clickable`} className={clsx(styles["chat-input-action"], "clickable")}
onClick={() => { onClick={() => {
props.onClick(); props.onClick();
setTimeout(updateWidth, 1); setTimeout(updateWidth, 1);
@ -460,6 +467,7 @@ export function ChatActions(props: {
const navigate = useNavigate(); const navigate = useNavigate();
const chatStore = useChatStore(); const chatStore = useChatStore();
const pluginStore = usePluginStore(); const pluginStore = usePluginStore();
const session = chatStore.currentSession();
// switch themes // switch themes
const theme = config.theme; const theme = config.theme;
@ -476,10 +484,9 @@ export function ChatActions(props: {
const stopAll = () => ChatControllerPool.stopAll(); const stopAll = () => ChatControllerPool.stopAll();
// switch model // switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model; const currentModel = session.mask.modelConfig.model;
const currentProviderName = const currentProviderName =
chatStore.currentSession().mask.modelConfig?.providerName || session.mask.modelConfig?.providerName || ServiceProvider.OpenAI;
ServiceProvider.OpenAI;
const allModels = useAllModels(); const allModels = useAllModels();
const models = useMemo(() => { const models = useMemo(() => {
const filteredModels = allModels.filter((m) => m.available); const filteredModels = allModels.filter((m) => m.available);
@ -513,12 +520,9 @@ export function ChatActions(props: {
const dalle3Sizes: DalleSize[] = ["1024x1024", "1792x1024", "1024x1792"]; const dalle3Sizes: DalleSize[] = ["1024x1024", "1792x1024", "1024x1792"];
const dalle3Qualitys: DalleQuality[] = ["standard", "hd"]; const dalle3Qualitys: DalleQuality[] = ["standard", "hd"];
const dalle3Styles: DalleStyle[] = ["vivid", "natural"]; const dalle3Styles: DalleStyle[] = ["vivid", "natural"];
const currentSize = const currentSize = session.mask.modelConfig?.size ?? "1024x1024";
chatStore.currentSession().mask.modelConfig?.size ?? "1024x1024"; const currentQuality = session.mask.modelConfig?.quality ?? "standard";
const currentQuality = const currentStyle = session.mask.modelConfig?.style ?? "vivid";
chatStore.currentSession().mask.modelConfig?.quality ?? "standard";
const currentStyle =
chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
@ -536,7 +540,7 @@ export function ChatActions(props: {
if (isUnavailableModel && models.length > 0) { if (isUnavailableModel && models.length > 0) {
// show next model to default model if exist // show next model to default model if exist
let nextModel = models.find((model) => model.isDefault) || models[0]; let nextModel = models.find((model) => model.isDefault) || models[0];
chatStore.updateCurrentSession((session) => { chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.model = nextModel.name; session.mask.modelConfig.model = nextModel.name;
session.mask.modelConfig.providerName = nextModel?.provider session.mask.modelConfig.providerName = nextModel?.provider
?.providerName as ServiceProvider; ?.providerName as ServiceProvider;
@ -547,7 +551,7 @@ export function ChatActions(props: {
: nextModel.name, : nextModel.name,
); );
} }
}, [chatStore, currentModel, models]); }, [chatStore, currentModel, models, session]);
return ( return (
<div className={styles["chat-input-actions"]}> <div className={styles["chat-input-actions"]}>
@ -614,7 +618,7 @@ export function ChatActions(props: {
text={Locale.Chat.InputActions.Clear} text={Locale.Chat.InputActions.Clear}
icon={<BreakIcon />} icon={<BreakIcon />}
onClick={() => { onClick={() => {
chatStore.updateCurrentSession((session) => { chatStore.updateTargetSession(session, (session) => {
if (session.clearContextIndex === session.messages.length) { if (session.clearContextIndex === session.messages.length) {
session.clearContextIndex = undefined; session.clearContextIndex = undefined;
} else { } else {
@ -645,8 +649,8 @@ export function ChatActions(props: {
onClose={() => setShowModelSelector(false)} onClose={() => setShowModelSelector(false)}
onSelection={(s) => { onSelection={(s) => {
if (s.length === 0) return; if (s.length === 0) return;
const [model, providerName] = s[0].split("@"); const [model, providerName] = getModelProvider(s[0]);
chatStore.updateCurrentSession((session) => { chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.model = model as ModelType; session.mask.modelConfig.model = model as ModelType;
session.mask.modelConfig.providerName = session.mask.modelConfig.providerName =
providerName as ServiceProvider; providerName as ServiceProvider;
@ -684,7 +688,7 @@ export function ChatActions(props: {
onSelection={(s) => { onSelection={(s) => {
if (s.length === 0) return; if (s.length === 0) return;
const size = s[0]; const size = s[0];
chatStore.updateCurrentSession((session) => { chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.size = size; session.mask.modelConfig.size = size;
}); });
showToast(size); showToast(size);
@ -711,7 +715,7 @@ export function ChatActions(props: {
onSelection={(q) => { onSelection={(q) => {
if (q.length === 0) return; if (q.length === 0) return;
const quality = q[0]; const quality = q[0];
chatStore.updateCurrentSession((session) => { chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.quality = quality; session.mask.modelConfig.quality = quality;
}); });
showToast(quality); showToast(quality);
@ -738,7 +742,7 @@ export function ChatActions(props: {
onSelection={(s) => { onSelection={(s) => {
if (s.length === 0) return; if (s.length === 0) return;
const style = s[0]; const style = s[0];
chatStore.updateCurrentSession((session) => { chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.style = style; session.mask.modelConfig.style = style;
}); });
showToast(style); showToast(style);
@ -769,7 +773,7 @@ export function ChatActions(props: {
}))} }))}
onClose={() => setShowPluginSelector(false)} onClose={() => setShowPluginSelector(false)}
onSelection={(s) => { onSelection={(s) => {
chatStore.updateCurrentSession((session) => { chatStore.updateTargetSession(session, (session) => {
session.mask.plugin = s as string[]; session.mask.plugin = s as string[];
}); });
}} }}
@ -812,7 +816,8 @@ export function EditMessageModal(props: { onClose: () => void }) {
icon={<ConfirmIcon />} icon={<ConfirmIcon />}
key="ok" key="ok"
onClick={() => { onClick={() => {
chatStore.updateCurrentSession( chatStore.updateTargetSession(
session,
(session) => (session.messages = messages), (session) => (session.messages = messages),
); );
props.onClose(); props.onClose();
@ -829,7 +834,8 @@ export function EditMessageModal(props: { onClose: () => void }) {
type="text" type="text"
value={session.topic} value={session.topic}
onInput={(e) => onInput={(e) =>
chatStore.updateCurrentSession( chatStore.updateTargetSession(
session,
(session) => (session.topic = e.currentTarget.value), (session) => (session.topic = e.currentTarget.value),
) )
} }
@ -990,7 +996,8 @@ function _Chat() {
prev: () => chatStore.nextSession(-1), prev: () => chatStore.nextSession(-1),
next: () => chatStore.nextSession(1), next: () => chatStore.nextSession(1),
clear: () => clear: () =>
chatStore.updateCurrentSession( chatStore.updateTargetSession(
session,
(session) => (session.clearContextIndex = session.messages.length), (session) => (session.clearContextIndex = session.messages.length),
), ),
fork: () => chatStore.forkSession(), fork: () => chatStore.forkSession(),
@ -1061,7 +1068,7 @@ function _Chat() {
}; };
useEffect(() => { useEffect(() => {
chatStore.updateCurrentSession((session) => { chatStore.updateTargetSession(session, (session) => {
const stopTiming = Date.now() - REQUEST_TIMEOUT_MS; const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
session.messages.forEach((m) => { session.messages.forEach((m) => {
// check if should stop all stale messages // check if should stop all stale messages
@ -1087,7 +1094,7 @@ function _Chat() {
} }
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [session]);
// check if should send message // check if should send message
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@ -1118,7 +1125,8 @@ function _Chat() {
}; };
const deleteMessage = (msgId?: string) => { const deleteMessage = (msgId?: string) => {
chatStore.updateCurrentSession( chatStore.updateTargetSession(
session,
(session) => (session) =>
(session.messages = session.messages.filter((m) => m.id !== msgId)), (session.messages = session.messages.filter((m) => m.id !== msgId)),
); );
@ -1185,7 +1193,7 @@ function _Chat() {
}; };
const onPinMessage = (message: ChatMessage) => { const onPinMessage = (message: ChatMessage) => {
chatStore.updateCurrentSession((session) => chatStore.updateTargetSession(session, (session) =>
session.mask.context.push(message), session.mask.context.push(message),
); );
@ -1588,9 +1596,12 @@ function _Chat() {
</div> </div>
)} )}
<div className={`window-header-title ${styles["chat-body-title"]}`}> <div className={clsx("window-header-title", styles["chat-body-title"])}>
<div <div
className={`window-header-main-title ${styles["chat-body-main-title"]}`} className={clsx(
"window-header-main-title",
styles["chat-body-main-title"],
)}
onClickCapture={() => setIsEditingMessage(true)} onClickCapture={() => setIsEditingMessage(true)}
> >
{!session.topic ? DEFAULT_TOPIC : session.topic} {!session.topic ? DEFAULT_TOPIC : session.topic}
@ -1711,14 +1722,17 @@ function _Chat() {
}); });
} }
} }
chatStore.updateCurrentSession((session) => { chatStore.updateTargetSession(
const m = session.mask.context session,
.concat(session.messages) (session) => {
.find((m) => m.id === message.id); const m = session.mask.context
if (m) { .concat(session.messages)
m.content = newContent; .find((m) => m.id === message.id);
} if (m) {
}); m.content = newContent;
}
},
);
}} }}
></IconButton> ></IconButton>
</div> </div>
@ -1861,7 +1875,7 @@ function _Chat() {
)} )}
{getMessageImages(message).length > 1 && ( {getMessageImages(message).length > 1 && (
<div <div
className={styles["chat-message-item-images"]} className={clsx(styles["chat-message-item-images"])}
style={ style={
{ {
"--image-count": getMessageImages(message).length, "--image-count": getMessageImages(message).length,
@ -1923,11 +1937,10 @@ function _Chat() {
setUserInput={setUserInput} setUserInput={setUserInput}
/> />
<label <label
className={`${styles["chat-input-panel-inner"]} ${ className={clsx(styles["chat-input-panel-inner"], {
attachImages.length != 0 [styles["chat-input-panel-inner-attach"]]:
? styles["chat-input-panel-inner-attach"] attachImages.length !== 0,
: "" })}
}`}
htmlFor="chat-input" htmlFor="chat-input"
> >
<textarea <textarea

View File

@ -40,6 +40,7 @@ import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { type ClientApi, getClientApi } from "../client/api"; import { type ClientApi, getClientApi } from "../client/api";
import { getMessageTextContent } from "../utils"; import { getMessageTextContent } from "../utils";
import clsx from "clsx";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />, loading: () => <LoadingIcon />,
@ -118,9 +119,10 @@ function Steps<
return ( return (
<div <div
key={i} key={i}
className={`${styles["step"]} ${ className={clsx("clickable", styles["step"], {
styles[i <= props.index ? "step-finished" : ""] [styles["step-finished"]]: i <= props.index,
} ${i === props.index && styles["step-current"]} clickable`} [styles["step-current"]]: i === props.index,
})}
onClick={() => { onClick={() => {
props.onStepChange?.(i); props.onStepChange?.(i);
}} }}
@ -525,11 +527,11 @@ export function ImagePreviewer(props: {
messages={props.messages} messages={props.messages}
/> />
<div <div
className={`${styles["preview-body"]} ${styles["default-theme"]}`} className={clsx(styles["preview-body"], styles["default-theme"])}
ref={previewRef} ref={previewRef}
> >
<div className={styles["chat-info"]}> <div className={styles["chat-info"]}>
<div className={styles["logo"] + " no-dark"}> <div className={clsx(styles["logo"], "no-dark")}>
<NextImage <NextImage
src={ChatGptIcon.src} src={ChatGptIcon.src}
alt="logo" alt="logo"
@ -570,7 +572,7 @@ export function ImagePreviewer(props: {
{props.messages.map((m, i) => { {props.messages.map((m, i) => {
return ( return (
<div <div
className={styles["message"] + " " + styles["message-" + m.role]} className={clsx(styles["message"], styles["message-" + m.role])}
key={i} key={i}
> >
<div className={styles["avatar"]}> <div className={styles["avatar"]}>

View File

@ -3,7 +3,6 @@
require("../polyfill"); require("../polyfill");
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
import BotIcon from "../icons/bot.svg"; import BotIcon from "../icons/bot.svg";
@ -29,10 +28,11 @@ import { AuthPage } from "./auth";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { type ClientApi, getClientApi } from "../client/api"; import { type ClientApi, getClientApi } from "../client/api";
import { useAccessStore } from "../store"; import { useAccessStore } from "../store";
import clsx from "clsx";
export function Loading(props: { noLogo?: boolean }) { export function Loading(props: { noLogo?: boolean }) {
return ( return (
<div className={styles["loading-content"] + " no-dark"}> <div className={clsx("no-dark", styles["loading-content"])}>
{!props.noLogo && <BotIcon />} {!props.noLogo && <BotIcon />}
<LoadingIcon /> <LoadingIcon />
</div> </div>
@ -179,7 +179,11 @@ function Screen() {
if (isSdNew) return <Sd />; if (isSdNew) return <Sd />;
return ( return (
<> <>
<SideBar className={isHome ? styles["sidebar-show"] : ""} /> <SideBar
className={clsx({
[styles["sidebar-show"]]: isHome,
})}
/>
<WindowContent> <WindowContent>
<Routes> <Routes>
<Route path={Path.Home} element={<Chat />} /> <Route path={Path.Home} element={<Chat />} />
@ -197,9 +201,10 @@ function Screen() {
return ( return (
<div <div
className={`${styles.container} ${ className={clsx(styles.container, {
shouldTightBorder ? styles["tight-container"] : styles.container [styles["tight-container"]]: shouldTightBorder,
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`} [styles["rtl-screen"]]: getLang() === "ar",
})}
> >
{renderContent()} {renderContent()}
</div> </div>

View File

@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import styles from "./input-range.module.scss"; import styles from "./input-range.module.scss";
import clsx from "clsx";
interface InputRangeProps { interface InputRangeProps {
onChange: React.ChangeEventHandler<HTMLInputElement>; onChange: React.ChangeEventHandler<HTMLInputElement>;
@ -23,7 +24,7 @@ export function InputRange({
aria, aria,
}: InputRangeProps) { }: InputRangeProps) {
return ( return (
<div className={styles["input-range"] + ` ${className ?? ""}`}> <div className={clsx(styles["input-range"], className)}>
{title || value} {title || value}
<input <input
aria-label={aria} aria-label={aria}

View File

@ -23,6 +23,7 @@ import { useChatStore } from "../store";
import { IconButton } from "./button"; import { IconButton } from "./button";
import { useAppConfig } from "../store/config"; import { useAppConfig } from "../store/config";
import clsx from "clsx";
export function Mermaid(props: { code: string }) { export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -57,7 +58,7 @@ export function Mermaid(props: { code: string }) {
return ( return (
<div <div
className="no-dark mermaid" className={clsx("no-dark", "mermaid")}
style={{ style={{
cursor: "pointer", cursor: "pointer",
overflow: "auto", overflow: "auto",
@ -193,7 +194,12 @@ function CustomCode(props: { children: any; className?: string }) {
const renderShowMoreButton = () => { const renderShowMoreButton = () => {
if (showToggle && enableCodeFold && collapsed) { if (showToggle && enableCodeFold && collapsed) {
return ( return (
<div className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}> <div
className={clsx("show-hide-button", {
collapsed,
expanded: !collapsed,
})}
>
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button> <button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
</div> </div>
); );
@ -203,7 +209,7 @@ function CustomCode(props: { children: any; className?: string }) {
return ( return (
<> <>
<code <code
className={props?.className} className={clsx(props?.className)}
ref={ref} ref={ref}
style={{ style={{
maxHeight: enableCodeFold && collapsed ? "400px" : "none", maxHeight: enableCodeFold && collapsed ? "400px" : "none",

View File

@ -55,6 +55,7 @@ import {
OnDragEndResponder, OnDragEndResponder,
} from "@hello-pangea/dnd"; } from "@hello-pangea/dnd";
import { getMessageTextContent } from "../utils"; import { getMessageTextContent } from "../utils";
import clsx from "clsx";
// drag and drop helper function // drag and drop helper function
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] { function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
@ -588,7 +589,7 @@ export function MaskPage() {
</div> </div>
<div className={styles["mask-title"]}> <div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{m.name}</div> <div className={styles["mask-name"]}>{m.name}</div>
<div className={styles["mask-info"] + " one-line"}> <div className={clsx(styles["mask-info"], "one-line")}>
{`${Locale.Mask.Item.Info(m.context.length)} / ${ {`${Locale.Mask.Item.Info(m.context.length)} / ${
ALL_LANG_OPTIONS[m.lang] ALL_LANG_OPTIONS[m.lang]
} / ${m.modelConfig.model}`} } / ${m.modelConfig.model}`}

View File

@ -8,6 +8,7 @@ import Locale from "../locales";
import styles from "./message-selector.module.scss"; import styles from "./message-selector.module.scss";
import { getMessageTextContent } from "../utils"; import { getMessageTextContent } from "../utils";
import clsx from "clsx";
function useShiftRange() { function useShiftRange() {
const [startIndex, setStartIndex] = useState<number>(); const [startIndex, setStartIndex] = useState<number>();
@ -71,6 +72,7 @@ export function MessageSelector(props: {
defaultSelectAll?: boolean; defaultSelectAll?: boolean;
onSelected?: (messages: ChatMessage[]) => void; onSelected?: (messages: ChatMessage[]) => void;
}) { }) {
const LATEST_COUNT = 4;
const chatStore = useChatStore(); const chatStore = useChatStore();
const session = chatStore.currentSession(); const session = chatStore.currentSession();
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming; const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
@ -141,15 +143,13 @@ export function MessageSelector(props: {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [startIndex, endIndex]); }, [startIndex, endIndex]);
const LATEST_COUNT = 4;
return ( return (
<div className={styles["message-selector"]}> <div className={styles["message-selector"]}>
<div className={styles["message-filter"]}> <div className={styles["message-filter"]}>
<input <input
type="text" type="text"
placeholder={Locale.Select.Search} placeholder={Locale.Select.Search}
className={styles["filter-item"] + " " + styles["search-bar"]} className={clsx(styles["filter-item"], styles["search-bar"])}
value={searchInput} value={searchInput}
onInput={(e) => { onInput={(e) => {
setSearchInput(e.currentTarget.value); setSearchInput(e.currentTarget.value);
@ -196,9 +196,9 @@ export function MessageSelector(props: {
return ( return (
<div <div
className={`${styles["message"]} ${ className={clsx(styles["message"], {
props.selection.has(m.id!) && styles["message-selected"] [styles["message-selected"]]: props.selection.has(m.id!),
}`} })}
key={i} key={i}
onClick={() => { onClick={() => {
props.updateSelection((selection) => { props.updateSelection((selection) => {
@ -221,7 +221,7 @@ export function MessageSelector(props: {
<div className={styles["date"]}> <div className={styles["date"]}>
{new Date(m.date).toLocaleString()} {new Date(m.date).toLocaleString()}
</div> </div>
<div className={`${styles["content"]} one-line`}> <div className={clsx(styles["content"], "one-line")}>
{getMessageTextContent(m)} {getMessageTextContent(m)}
</div> </div>
</div> </div>

View File

@ -7,6 +7,7 @@ import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import { groupBy } from "lodash-es"; import { groupBy } from "lodash-es";
import styles from "./model-config.module.scss"; import styles from "./model-config.module.scss";
import { getModelProvider } from "../utils/model";
export function ModelConfigList(props: { export function ModelConfigList(props: {
modelConfig: ModelConfig; modelConfig: ModelConfig;
@ -28,7 +29,9 @@ export function ModelConfigList(props: {
value={value} value={value}
align="left" align="left"
onChange={(e) => { onChange={(e) => {
const [model, providerName] = e.currentTarget.value.split("@"); const [model, providerName] = getModelProvider(
e.currentTarget.value,
);
props.updateConfig((config) => { props.updateConfig((config) => {
config.model = ModalConfigValidator.model(model); config.model = ModalConfigValidator.model(model);
config.providerName = providerName as ServiceProvider; config.providerName = providerName as ServiceProvider;
@ -247,7 +250,9 @@ export function ModelConfigList(props: {
aria-label={Locale.Settings.CompressModel.Title} aria-label={Locale.Settings.CompressModel.Title}
value={compressModelValue} value={compressModelValue}
onChange={(e) => { onChange={(e) => {
const [model, providerName] = e.currentTarget.value.split("@"); const [model, providerName] = getModelProvider(
e.currentTarget.value,
);
props.updateConfig((config) => { props.updateConfig((config) => {
config.compressModel = ModalConfigValidator.model(model); config.compressModel = ModalConfigValidator.model(model);
config.compressProviderName = providerName as ServiceProvider; config.compressProviderName = providerName as ServiceProvider;

View File

@ -16,6 +16,7 @@ import { MaskAvatar } from "./mask";
import { useCommand } from "../command"; import { useCommand } from "../command";
import { showConfirm } from "./ui-lib"; import { showConfirm } from "./ui-lib";
import { BUILTIN_MASK_STORE } from "../masks"; import { BUILTIN_MASK_STORE } from "../masks";
import clsx from "clsx";
function MaskItem(props: { mask: Mask; onClick?: () => void }) { function MaskItem(props: { mask: Mask; onClick?: () => void }) {
return ( return (
@ -24,7 +25,9 @@ function MaskItem(props: { mask: Mask; onClick?: () => void }) {
avatar={props.mask.avatar} avatar={props.mask.avatar}
model={props.mask.modelConfig.model} model={props.mask.modelConfig.model}
/> />
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div> <div className={clsx(styles["mask-name"], "one-line")}>
{props.mask.name}
</div>
</div> </div>
); );
} }

View File

@ -28,6 +28,7 @@ import {
import Locale from "../locales"; import Locale from "../locales";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useState } from "react"; import { useState } from "react";
import clsx from "clsx";
export function PluginPage() { export function PluginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -199,7 +200,7 @@ export function PluginPage() {
<div className={styles["mask-name"]}> <div className={styles["mask-name"]}>
{m.title}@<small>{m.version}</small> {m.title}@<small>{m.version}</small>
</div> </div>
<div className={styles["mask-info"] + " one-line"}> <div className={clsx(styles["mask-info"], "one-line")}>
{Locale.Plugin.Item.Info( {Locale.Plugin.Item.Info(
FunctionToolService.add(m).length, FunctionToolService.add(m).length,
)} )}
@ -335,7 +336,10 @@ export function PluginPage() {
<ListItem <ListItem
subTitle={ subTitle={
<div <div
className={`markdown-body ${pluginStyles["plugin-content"]}`} className={clsx(
"markdown-body",
pluginStyles["plugin-content"],
)}
dir="auto" dir="auto"
> >
<pre> <pre>

View File

@ -4,6 +4,7 @@ import { Select } from "@/app/components/ui-lib";
import { IconButton } from "@/app/components/button"; import { IconButton } from "@/app/components/button";
import Locale from "@/app/locales"; import Locale from "@/app/locales";
import { useSdStore } from "@/app/store/sd"; import { useSdStore } from "@/app/store/sd";
import clsx from "clsx";
export const params = [ export const params = [
{ {
@ -136,7 +137,7 @@ export function ControlParamItem(props: {
className?: string; className?: string;
}) { }) {
return ( return (
<div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}> <div className={clsx(styles["ctrl-param-item"], props.className)}>
<div className={styles["ctrl-param-item-header"]}> <div className={styles["ctrl-param-item-header"]}>
<div className={styles["ctrl-param-item-title"]}> <div className={styles["ctrl-param-item-title"]}>
<div> <div>

View File

@ -36,6 +36,7 @@ import { removeImage } from "@/app/utils/chat";
import { SideBar } from "./sd-sidebar"; import { SideBar } from "./sd-sidebar";
import { WindowContent } from "@/app/components/home"; import { WindowContent } from "@/app/components/home";
import { params } from "./sd-panel"; import { params } from "./sd-panel";
import clsx from "clsx";
function getSdTaskStatus(item: any) { function getSdTaskStatus(item: any) {
let s: string; let s: string;
@ -104,7 +105,7 @@ export function Sd() {
return ( return (
<> <>
<SideBar className={isSd ? homeStyles["sidebar-show"] : ""} /> <SideBar className={clsx({ [homeStyles["sidebar-show"]]: isSd })} />
<WindowContent> <WindowContent>
<div className={chatStyles.chat} key={"1"}> <div className={chatStyles.chat} key={"1"}>
<div className="window-header" data-tauri-drag-region> <div className="window-header" data-tauri-drag-region>
@ -121,7 +122,10 @@ export function Sd() {
</div> </div>
)} )}
<div <div
className={`window-header-title ${chatStyles["chat-body-title"]}`} className={clsx(
"window-header-title",
chatStyles["chat-body-title"],
)}
> >
<div className={`window-header-main-title`}>Stability AI</div> <div className={`window-header-main-title`}>Stability AI</div>
<div className="window-header-sub-title"> <div className="window-header-sub-title">

View File

@ -30,6 +30,7 @@ import { Link, useNavigate } from "react-router-dom";
import { isIOS, useMobileScreen } from "../utils"; import { isIOS, useMobileScreen } from "../utils";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { showConfirm, Selector } from "./ui-lib"; import { showConfirm, Selector } from "./ui-lib";
import clsx from "clsx";
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => null, loading: () => null,
@ -141,9 +142,9 @@ export function SideBarContainer(props: {
const { children, className, onDragStart, shouldNarrow } = props; const { children, className, onDragStart, shouldNarrow } = props;
return ( return (
<div <div
className={`${styles.sidebar} ${className} ${ className={clsx(styles.sidebar, className, {
shouldNarrow && styles["narrow-sidebar"] [styles["narrow-sidebar"]]: shouldNarrow,
}`} })}
style={{ style={{
// #3016 disable transition on ios mobile screen // #3016 disable transition on ios mobile screen
transition: isMobileScreen && isIOSMobile ? "none" : undefined, transition: isMobileScreen && isIOSMobile ? "none" : undefined,
@ -171,9 +172,9 @@ export function SideBarHeader(props: {
return ( return (
<Fragment> <Fragment>
<div <div
className={`${styles["sidebar-header"]} ${ className={clsx(styles["sidebar-header"], {
shouldNarrow ? styles["sidebar-header-narrow"] : "" [styles["sidebar-header-narrow"]]: shouldNarrow,
}`} })}
data-tauri-drag-region data-tauri-drag-region
> >
<div className={styles["sidebar-title-container"]}> <div className={styles["sidebar-title-container"]}>
@ -182,7 +183,7 @@ export function SideBarHeader(props: {
</div> </div>
<div className={styles["sidebar-sub-title"]}>{subTitle}</div> <div className={styles["sidebar-sub-title"]}>{subTitle}</div>
</div> </div>
<div className={styles["sidebar-logo"] + " no-dark"}>{logo}</div> <div className={clsx(styles["sidebar-logo"], "no-dark")}>{logo}</div>
</div> </div>
{children} {children}
</Fragment> </Fragment>
@ -286,7 +287,7 @@ export function SideBar(props: { className?: string }) {
<SideBarTail <SideBarTail
primaryAction={ primaryAction={
<> <>
<div className={styles["sidebar-action"] + " " + styles.mobile}> <div className={clsx(styles["sidebar-action"], styles.mobile)}>
<IconButton <IconButton
icon={<DeleteIcon />} icon={<DeleteIcon />}
onClick={async () => { onClick={async () => {

View File

@ -23,6 +23,7 @@ import React, {
useRef, useRef,
} from "react"; } from "react";
import { IconButton } from "./button"; import { IconButton } from "./button";
import clsx from "clsx";
export function Popover(props: { export function Popover(props: {
children: JSX.Element; children: JSX.Element;
@ -45,7 +46,7 @@ export function Popover(props: {
export function Card(props: { children: JSX.Element[]; className?: string }) { export function Card(props: { children: JSX.Element[]; className?: string }) {
return ( return (
<div className={styles.card + " " + props.className}>{props.children}</div> <div className={clsx(styles.card, props.className)}>{props.children}</div>
); );
} }
@ -60,11 +61,13 @@ export function ListItem(props: {
}) { }) {
return ( return (
<div <div
className={ className={clsx(
styles["list-item"] + styles["list-item"],
` ${props.vertical ? styles["vertical"] : ""} ` + {
` ${props.className || ""}` [styles["vertical"]]: props.vertical,
} },
props.className,
)}
onClick={props.onClick} onClick={props.onClick}
> >
<div className={styles["list-header"]}> <div className={styles["list-header"]}>
@ -135,9 +138,9 @@ export function Modal(props: ModalProps) {
return ( return (
<div <div
className={ className={clsx(styles["modal-container"], {
styles["modal-container"] + ` ${isMax && styles["modal-container-max"]}` [styles["modal-container-max"]]: isMax,
} })}
> >
<div className={styles["modal-header"]}> <div className={styles["modal-header"]}>
<div className={styles["modal-title"]}>{props.title}</div> <div className={styles["modal-title"]}>{props.title}</div>
@ -260,7 +263,7 @@ export function Input(props: InputProps) {
return ( return (
<textarea <textarea
{...props} {...props}
className={`${styles["input"]} ${props.className}`} className={clsx(styles["input"], props.className)}
></textarea> ></textarea>
); );
} }
@ -301,9 +304,13 @@ export function Select(
const { className, children, align, ...otherProps } = props; const { className, children, align, ...otherProps } = props;
return ( return (
<div <div
className={`${styles["select-with-icon"]} ${ className={clsx(
align === "left" ? styles["left-align-option"] : "" styles["select-with-icon"],
} ${className}`} {
[styles["left-align-option"]]: align === "left",
},
className,
)}
> >
<select className={styles["select-with-icon-select"]} {...otherProps}> <select className={styles["select-with-icon-select"]} {...otherProps}>
{children} {children}
@ -509,9 +516,9 @@ export function Selector<T>(props: {
const selected = selectedValues.includes(item.value); const selected = selectedValues.includes(item.value);
return ( return (
<ListItem <ListItem
className={`${styles["selector-item"]} ${ className={clsx(styles["selector-item"], {
item.disable && styles["selector-item-disabled"] [styles["selector-item-disabled"]]: item.disable,
}`} })}
key={i} key={i}
title={item.title} title={item.title}
subTitle={item.subTitle} subTitle={item.subTitle}

View File

@ -232,7 +232,7 @@ export const XAI = {
export const ChatGLM = { export const ChatGLM = {
ExampleEndpoint: CHATGLM_BASE_URL, ExampleEndpoint: CHATGLM_BASE_URL,
ChatPath: "/api/paas/v4/chat/completions", ChatPath: "api/paas/v4/chat/completions",
}; };
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang

View File

@ -21,6 +21,7 @@ import { getClientConfig } from "../config/client";
import { createPersistStore } from "../utils/store"; import { createPersistStore } from "../utils/store";
import { ensure } from "../utils/clone"; import { ensure } from "../utils/clone";
import { DEFAULT_CONFIG } from "./config"; import { DEFAULT_CONFIG } from "./config";
import { getModelProvider } from "../utils/model";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
@ -226,9 +227,9 @@ export const useAccessStore = createPersistStore(
.then((res) => { .then((res) => {
const defaultModel = res.defaultModel ?? ""; const defaultModel = res.defaultModel ?? "";
if (defaultModel !== "") { if (defaultModel !== "") {
const [model, providerName] = defaultModel.split("@"); const [model, providerName] = getModelProvider(defaultModel);
DEFAULT_CONFIG.modelConfig.model = model; DEFAULT_CONFIG.modelConfig.model = model;
DEFAULT_CONFIG.modelConfig.providerName = providerName; DEFAULT_CONFIG.modelConfig.providerName = providerName as any;
} }
return res; return res;

View File

@ -357,7 +357,7 @@ export const useChatStore = createPersistStore(
session.messages = session.messages.concat(); session.messages = session.messages.concat();
session.lastUpdate = Date.now(); session.lastUpdate = Date.now();
}); });
get().updateStat(message); get().updateStat(message, targetSession);
get().summarizeSession(false, targetSession); get().summarizeSession(false, targetSession);
}, },
@ -396,10 +396,10 @@ export const useChatStore = createPersistStore(
// get recent messages // get recent messages
const recentMessages = get().getMessagesWithMemory(); const recentMessages = get().getMessagesWithMemory();
const sendMessages = recentMessages.concat(userMessage); const sendMessages = recentMessages.concat(userMessage);
const messageIndex = get().currentSession().messages.length + 1; const messageIndex = session.messages.length + 1;
// save user's and bot's message // save user's and bot's message
get().updateCurrentSession((session) => { get().updateTargetSession(session, (session) => {
const savedUserMessage = { const savedUserMessage = {
...userMessage, ...userMessage,
content: mContent, content: mContent,
@ -420,7 +420,7 @@ export const useChatStore = createPersistStore(
if (message) { if (message) {
botMessage.content = message; botMessage.content = message;
} }
get().updateCurrentSession((session) => { get().updateTargetSession(session, (session) => {
session.messages = session.messages.concat(); session.messages = session.messages.concat();
}); });
}, },
@ -428,13 +428,14 @@ export const useChatStore = createPersistStore(
botMessage.streaming = false; botMessage.streaming = false;
if (message) { if (message) {
botMessage.content = message; botMessage.content = message;
botMessage.date = new Date().toLocaleString();
get().onNewMessage(botMessage, session); get().onNewMessage(botMessage, session);
} }
ChatControllerPool.remove(session.id, botMessage.id); ChatControllerPool.remove(session.id, botMessage.id);
}, },
onBeforeTool(tool: ChatMessageTool) { onBeforeTool(tool: ChatMessageTool) {
(botMessage.tools = botMessage?.tools || []).push(tool); (botMessage.tools = botMessage?.tools || []).push(tool);
get().updateCurrentSession((session) => { get().updateTargetSession(session, (session) => {
session.messages = session.messages.concat(); session.messages = session.messages.concat();
}); });
}, },
@ -444,7 +445,7 @@ export const useChatStore = createPersistStore(
tools[i] = { ...tool }; tools[i] = { ...tool };
} }
}); });
get().updateCurrentSession((session) => { get().updateTargetSession(session, (session) => {
session.messages = session.messages.concat(); session.messages = session.messages.concat();
}); });
}, },
@ -459,7 +460,7 @@ export const useChatStore = createPersistStore(
botMessage.streaming = false; botMessage.streaming = false;
userMessage.isError = !isAborted; userMessage.isError = !isAborted;
botMessage.isError = !isAborted; botMessage.isError = !isAborted;
get().updateCurrentSession((session) => { get().updateTargetSession(session, (session) => {
session.messages = session.messages.concat(); session.messages = session.messages.concat();
}); });
ChatControllerPool.remove( ChatControllerPool.remove(
@ -591,8 +592,8 @@ export const useChatStore = createPersistStore(
set(() => ({ sessions })); set(() => ({ sessions }));
}, },
resetSession() { resetSession(session: ChatSession) {
get().updateCurrentSession((session) => { get().updateTargetSession(session, (session) => {
session.messages = []; session.messages = [];
session.memoryPrompt = ""; session.memoryPrompt = "";
}); });
@ -736,19 +737,12 @@ export const useChatStore = createPersistStore(
} }
}, },
updateStat(message: ChatMessage) { updateStat(message: ChatMessage, session: ChatSession) {
get().updateCurrentSession((session) => { get().updateTargetSession(session, (session) => {
session.stat.charCount += message.content.length; session.stat.charCount += message.content.length;
// TODO: should update chat count and word count // TODO: should update chat count and word count
}); });
}, },
updateCurrentSession(updater: (session: ChatSession) => void) {
const sessions = get().sessions;
const index = get().currentSessionIndex;
updater(sessions[index]);
set(() => ({ sessions }));
},
updateTargetSession( updateTargetSession(
targetSession: ChatSession, targetSession: ChatSession,
updater: (session: ChatSession) => void, updater: (session: ChatSession) => void,

View File

@ -37,6 +37,17 @@ const sortModelTable = (models: ReturnType<typeof collectModels>) =>
} }
}); });
/**
* get model name and provider from a formatted string,
* e.g. `gpt-4@OpenAi` or `claude-3-5-sonnet@20240620@Google`
* @param modelWithProvider model name with provider separated by last `@` char,
* @returns [model, provider] tuple, if no `@` char found, provider is undefined
*/
export function getModelProvider(modelWithProvider: string): [string, string?] {
const [model, provider] = modelWithProvider.split(/@(?!.*@)/);
return [model, provider];
}
export function collectModelTable( export function collectModelTable(
models: readonly LLMModel[], models: readonly LLMModel[],
customModels: string, customModels: string,
@ -79,10 +90,10 @@ export function collectModelTable(
); );
} else { } else {
// 1. find model by name, and set available value // 1. find model by name, and set available value
const [customModelName, customProviderName] = name.split("@"); const [customModelName, customProviderName] = getModelProvider(name);
let count = 0; let count = 0;
for (const fullName in modelTable) { for (const fullName in modelTable) {
const [modelName, providerName] = fullName.split("@"); const [modelName, providerName] = getModelProvider(fullName);
if ( if (
customModelName == modelName && customModelName == modelName &&
(customProviderName === undefined || (customProviderName === undefined ||
@ -102,7 +113,7 @@ export function collectModelTable(
} }
// 2. if model not exists, create new model with available value // 2. if model not exists, create new model with available value
if (count === 0) { if (count === 0) {
let [customModelName, customProviderName] = name.split("@"); let [customModelName, customProviderName] = getModelProvider(name);
const provider = customProvider( const provider = customProvider(
customProviderName || customModelName, customProviderName || customModelName,
); );
@ -139,7 +150,7 @@ export function collectModelTableWithDefaultModel(
for (const key of Object.keys(modelTable)) { for (const key of Object.keys(modelTable)) {
if ( if (
modelTable[key].available && modelTable[key].available &&
key.split("@").shift() == defaultModel getModelProvider(key)[0] == defaultModel
) { ) {
modelTable[key].isDefault = true; modelTable[key].isDefault = true;
break; break;

View File

@ -27,6 +27,7 @@
"@vercel/analytics": "^0.1.11", "@vercel/analytics": "^0.1.11",
"@vercel/speed-insights": "^1.0.2", "@vercel/speed-insights": "^1.0.2",
"axios": "^1.7.5", "axios": "^1.7.5",
"clsx": "^2.1.1",
"emoji-picker-react": "^4.9.2", "emoji-picker-react": "^4.9.2",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"heic2any": "^0.0.4", "heic2any": "^0.0.4",

View File

@ -0,0 +1,31 @@
import { getModelProvider } from "../app/utils/model";
describe("getModelProvider", () => {
test("should return model and provider when input contains '@'", () => {
const input = "model@provider";
const [model, provider] = getModelProvider(input);
expect(model).toBe("model");
expect(provider).toBe("provider");
});
test("should return model and undefined provider when input does not contain '@'", () => {
const input = "model";
const [model, provider] = getModelProvider(input);
expect(model).toBe("model");
expect(provider).toBeUndefined();
});
test("should handle multiple '@' characters correctly", () => {
const input = "model@provider@extra";
const [model, provider] = getModelProvider(input);
expect(model).toBe("model@provider");
expect(provider).toBe("extra");
});
test("should return empty strings when input is empty", () => {
const input = "";
const [model, provider] = getModelProvider(input);
expect(model).toBe("");
expect(provider).toBeUndefined();
});
});

View File

@ -3189,6 +3189,11 @@ cliui@^8.0.1:
strip-ansi "^6.0.1" strip-ansi "^6.0.1"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
co@^4.6.0: co@^4.6.0:
version "4.6.0" version "4.6.0"
resolved "https://registry.npmmirror.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" resolved "https://registry.npmmirror.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"