Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web
This commit is contained in:
commit
a538fbb423
|
@ -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/",
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}`}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue