mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-03-03 18:54:25 +08:00
Compare commits
34 Commits
f80da8a263
...
v2.15.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
463fa743e9 | ||
|
|
cda4494cec | ||
|
|
87d85c10c3 | ||
|
|
22f83c9e11 | ||
|
|
7f454cbcec | ||
|
|
426269d795 | ||
|
|
370f143157 | ||
|
|
103106bb93 | ||
|
|
2419083adf | ||
|
|
c25903bfb4 | ||
|
|
e34c266438 | ||
|
|
8c39a687b5 | ||
|
|
592f62005b | ||
|
|
12e7caa209 | ||
|
|
b016771555 | ||
|
|
a84383f919 | ||
|
|
7f68fb1ff2 | ||
|
|
8d2003fe68 | ||
|
|
9961b513cc | ||
|
|
819238acaf | ||
|
|
ad49916b1c | ||
|
|
d18bd8a48a | ||
|
|
4a1319f2c0 | ||
|
|
8fd843d228 | ||
|
|
6792d6e475 | ||
|
|
c139038e01 | ||
|
|
4a7fd3a380 | ||
|
|
c98dc31cdf | ||
|
|
bd43af3a8d | ||
|
|
be98aa2078 | ||
|
|
a0d4a04192 | ||
|
|
bd9de4dc4d | ||
|
|
2eebfcf6fe | ||
|
|
7d55a6d0e4 |
4
.github/workflows/deploy_preview.yml
vendored
4
.github/workflows/deploy_preview.yml
vendored
@@ -3,9 +3,7 @@ name: VercelPreviewDeployment
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- review_requested
|
||||
|
||||
env:
|
||||
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
|
||||
|
||||
18
.github/workflows/docker.yml
vendored
18
.github/workflows/docker.yml
vendored
@@ -19,26 +19,26 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
-
|
||||
|
||||
-
|
||||
name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_USERNAME }}/chatgpt-next-web
|
||||
images: yidadaa/chatgpt-next-web
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=ref,event=tag
|
||||
|
||||
-
|
||||
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
-
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
-
|
||||
|
||||
-
|
||||
name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -49,4 +49,4 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
|
||||
|
||||
39
.github/workflows/test.yml
vendored
Normal file
39
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "!*"
|
||||
pull_request:
|
||||
types:
|
||||
- review_requested
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "yarn"
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node_modules-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Run Jest tests
|
||||
run: yarn test:ci
|
||||
@@ -352,7 +352,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||
);
|
||||
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
|
||||
@@ -35,7 +35,6 @@ export function useCommand(commands: Commands = {}) {
|
||||
interface ChatCommands {
|
||||
new?: Command;
|
||||
newm?: Command;
|
||||
copy?: Command;
|
||||
next?: Command;
|
||||
prev?: Command;
|
||||
clear?: Command;
|
||||
|
||||
@@ -11,6 +11,7 @@ import Logo from "../icons/logo.svg";
|
||||
import { useMobileScreen } from "@/app/utils";
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { PasswordInput } from "./ui-lib";
|
||||
import LeftIcon from "@/app/icons/left.svg";
|
||||
import { safeLocalStorage } from "@/app/utils";
|
||||
import {
|
||||
@@ -60,36 +61,43 @@ export function AuthPage() {
|
||||
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
|
||||
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
|
||||
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Auth.Input}
|
||||
<PasswordInput
|
||||
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||
aria={Locale.Settings.ShowPassword}
|
||||
aria-label={Locale.Auth.Input}
|
||||
value={accessStore.accessCode}
|
||||
type="text"
|
||||
placeholder={Locale.Auth.Input}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.accessCode = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!accessStore.hideUserApiKey ? (
|
||||
<>
|
||||
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
<PasswordInput
|
||||
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||
aria={Locale.Settings.ShowPassword}
|
||||
aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
value={accessStore.openaiApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.openaiApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className={styles["auth-input-second"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
<PasswordInput
|
||||
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||
aria={Locale.Settings.ShowPassword}
|
||||
aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
value={accessStore.googleApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.googleApiKey = e.currentTarget.value),
|
||||
|
||||
@@ -70,7 +70,6 @@ import {
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
isDalle3,
|
||||
removeOutdatedEntries,
|
||||
showPlugins,
|
||||
safeLocalStorage,
|
||||
} from "../utils";
|
||||
@@ -116,11 +115,14 @@ import { getClientConfig } from "../config/client";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
import { MultimodalContent } from "../client/api";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
import { ClientApi } from "../client/api";
|
||||
import { createTTSPlayer } from "../utils/audio";
|
||||
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
||||
|
||||
import { isEmpty } from "lodash-es";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
const ttsPlayer = createTTSPlayer();
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
@@ -985,7 +987,6 @@ function _Chat() {
|
||||
const chatCommands = useChatCommand({
|
||||
new: () => chatStore.newSession(),
|
||||
newm: () => navigate(Path.NewChat),
|
||||
copy: () => chatStore.copySession(),
|
||||
prev: () => chatStore.nextSession(-1),
|
||||
next: () => chatStore.nextSession(1),
|
||||
clear: () =>
|
||||
@@ -1017,7 +1018,7 @@ function _Chat() {
|
||||
};
|
||||
|
||||
const doSubmit = (userInput: string) => {
|
||||
if (userInput.trim() === "") return;
|
||||
if (userInput.trim() === "" && isEmpty(attachImages)) return;
|
||||
const matchCommand = chatCommands.match(userInput);
|
||||
if (matchCommand.matched) {
|
||||
setUserInput("");
|
||||
@@ -1117,20 +1118,10 @@ function _Chat() {
|
||||
};
|
||||
|
||||
const deleteMessage = (msgId?: string) => {
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.deletedMessageIds &&
|
||||
removeOutdatedEntries(session.deletedMessageIds);
|
||||
session.messages = session.messages.filter((m) => {
|
||||
if (m.id !== msgId) {
|
||||
return true;
|
||||
}
|
||||
if (!session.deletedMessageIds) {
|
||||
session.deletedMessageIds = {} as Record<string, number>;
|
||||
}
|
||||
session.deletedMessageIds[m.id] = Date.now();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
chatStore.updateCurrentSession(
|
||||
(session) =>
|
||||
(session.messages = session.messages.filter((m) => m.id !== msgId)),
|
||||
);
|
||||
};
|
||||
|
||||
const onDelete = (msgId: string) => {
|
||||
|
||||
@@ -140,6 +140,9 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
&-narrow {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
|
||||
@@ -169,6 +169,12 @@ export function PreCode(props: { children: any }) {
|
||||
}
|
||||
|
||||
function CustomCode(props: { children: any; className?: string }) {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const config = useAppConfig();
|
||||
const enableCodeFold =
|
||||
session.mask?.enableCodeFold !== false && config.enableCodeFold;
|
||||
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [showToggle, setShowToggle] = useState(false);
|
||||
@@ -184,25 +190,30 @@ function CustomCode(props: { children: any; className?: string }) {
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsed((collapsed) => !collapsed);
|
||||
};
|
||||
const renderShowMoreButton = () => {
|
||||
if (showToggle && enableCodeFold && collapsed) {
|
||||
return (
|
||||
<div className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}>
|
||||
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<code
|
||||
className={props?.className}
|
||||
ref={ref}
|
||||
style={{
|
||||
maxHeight: collapsed ? "400px" : "none",
|
||||
maxHeight: enableCodeFold && collapsed ? "400px" : "none",
|
||||
overflowY: "hidden",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</code>
|
||||
{showToggle && collapsed && (
|
||||
<div
|
||||
className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}
|
||||
>
|
||||
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderShowMoreButton()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,6 +183,23 @@ export function MaskConfig(props: {
|
||||
></input>
|
||||
</ListItem>
|
||||
)}
|
||||
{globalConfig.enableCodeFold && (
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.CodeFold.Title}
|
||||
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.CodeFold.Title}
|
||||
type="checkbox"
|
||||
checked={props.mask.enableCodeFold !== false}
|
||||
onChange={(e) => {
|
||||
props.updateMask((mask) => {
|
||||
mask.enableCodeFold = e.currentTarget.checked;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{!props.shouldSyncFromGlobal ? (
|
||||
<ListItem
|
||||
|
||||
@@ -49,7 +49,7 @@ import Locale, {
|
||||
changeLang,
|
||||
getLang,
|
||||
} from "../locales";
|
||||
import { copyToClipboard } from "../utils";
|
||||
import { copyToClipboard, clientUpdate, semverCompare } from "../utils";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Anthropic,
|
||||
@@ -360,21 +360,6 @@ function SyncConfigModal(props: { onClose?: () => void }) {
|
||||
</select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.Config.EnableAutoSync.Title}
|
||||
subTitle={Locale.Settings.Sync.Config.EnableAutoSync.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={syncStore.enableAutoSync}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.enableAutoSync = e.currentTarget.checked),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.Config.Proxy.Title}
|
||||
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
|
||||
@@ -600,7 +585,7 @@ export function Settings() {
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
const currentVersion = updateStore.formatVersion(updateStore.version);
|
||||
const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
|
||||
const hasNewVersion = currentVersion !== remoteId;
|
||||
const hasNewVersion = semverCompare(currentVersion, remoteId) === -1;
|
||||
const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
|
||||
|
||||
function checkUpdate(force = false) {
|
||||
@@ -1372,9 +1357,17 @@ export function Settings() {
|
||||
{checkingUpdate ? (
|
||||
<LoadingIcon />
|
||||
) : hasNewVersion ? (
|
||||
<Link href={updateUrl} target="_blank" className="link">
|
||||
{Locale.Settings.Update.GoToUpdate}
|
||||
</Link>
|
||||
clientConfig?.isApp ? (
|
||||
<IconButton
|
||||
icon={<ResetIcon></ResetIcon>}
|
||||
text={Locale.Settings.Update.GoToUpdate}
|
||||
onClick={() => clientUpdate()}
|
||||
/>
|
||||
) : (
|
||||
<Link href={updateUrl} target="_blank" className="link">
|
||||
{Locale.Settings.Update.GoToUpdate}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<IconButton
|
||||
icon={<ResetIcon></ResetIcon>}
|
||||
@@ -1524,6 +1517,22 @@ export function Settings() {
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.CodeFold.Title}
|
||||
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.CodeFold.Title}
|
||||
type="checkbox"
|
||||
checked={config.enableCodeFold}
|
||||
data-testid="enable-code-fold-checkbox"
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) => (config.enableCodeFold = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<SyncItems />
|
||||
|
||||
@@ -165,11 +165,17 @@ export function SideBarHeader(props: {
|
||||
subTitle?: string | React.ReactNode;
|
||||
logo?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
shouldNarrow?: boolean;
|
||||
}) {
|
||||
const { title, subTitle, logo, children } = props;
|
||||
const { title, subTitle, logo, children, shouldNarrow } = props;
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
||||
<div
|
||||
className={`${styles["sidebar-header"]} ${
|
||||
shouldNarrow ? styles["sidebar-header-narrow"] : ""
|
||||
}`}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className={styles["sidebar-title-container"]}>
|
||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||
{title}
|
||||
@@ -227,6 +233,7 @@ export function SideBar(props: { className?: string }) {
|
||||
title="NextChat"
|
||||
subTitle="Build your own AI assistant."
|
||||
logo={<ChatGptIcon />}
|
||||
shouldNarrow={shouldNarrow}
|
||||
>
|
||||
<div className={styles["sidebar-header-bar"]}>
|
||||
<IconButton
|
||||
|
||||
7
app/global.d.ts
vendored
7
app/global.d.ts
vendored
@@ -26,6 +26,13 @@ declare interface Window {
|
||||
isPermissionGranted(): Promise<boolean>;
|
||||
sendNotification(options: string | Options): void;
|
||||
};
|
||||
updater: {
|
||||
checkUpdate(): Promise<UpdateResult>;
|
||||
installUpdate(): Promise<void>;
|
||||
onUpdaterEvent(
|
||||
handler: (status: UpdateStatusResult) => void,
|
||||
): Promise<UnlistenFn>;
|
||||
};
|
||||
http: {
|
||||
fetch<T>(
|
||||
url: string,
|
||||
|
||||
@@ -62,7 +62,6 @@ const cn = {
|
||||
Commands: {
|
||||
new: "新建聊天",
|
||||
newm: "从面具新建聊天",
|
||||
copy: "复制当前聊天",
|
||||
next: "下一个聊天",
|
||||
prev: "上一个聊天",
|
||||
clear: "清除上下文",
|
||||
@@ -206,6 +205,8 @@ const cn = {
|
||||
IsChecking: "正在检查更新...",
|
||||
FoundUpdate: (x: string) => `发现新版本:${x}`,
|
||||
GoToUpdate: "前往更新",
|
||||
Success: "更新成功!",
|
||||
Failed: "更新失败",
|
||||
},
|
||||
SendKey: "发送键",
|
||||
Theme: "主题",
|
||||
@@ -233,10 +234,6 @@ const cn = {
|
||||
Title: "同步类型",
|
||||
SubTitle: "选择喜爱的同步服务器",
|
||||
},
|
||||
EnableAutoSync: {
|
||||
Title: "自动同步设置",
|
||||
SubTitle: "在回复完成或删除消息后自动同步数据",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "启用代理",
|
||||
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
||||
@@ -500,8 +497,8 @@ const cn = {
|
||||
|
||||
Model: "模型 (model)",
|
||||
CompressModel: {
|
||||
Title: "压缩模型",
|
||||
SubTitle: "用于压缩历史记录的模型",
|
||||
Title: "对话摘要模型",
|
||||
SubTitle: "用于压缩历史记录、生成对话标题的模型",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "随机性 (temperature)",
|
||||
@@ -670,6 +667,10 @@ const cn = {
|
||||
Title: "启用Artifacts",
|
||||
SubTitle: "启用之后可以直接渲染HTML页面",
|
||||
},
|
||||
CodeFold: {
|
||||
Title: "启用代码折叠",
|
||||
SubTitle: "启用之后可以自动折叠/展开过长的代码块",
|
||||
},
|
||||
Share: {
|
||||
Title: "分享此面具",
|
||||
SubTitle: "生成此面具的直达链接",
|
||||
|
||||
@@ -63,7 +63,6 @@ const en: LocaleType = {
|
||||
Commands: {
|
||||
new: "Start a new chat",
|
||||
newm: "Start a new chat with mask",
|
||||
copy: "Copy the current Chat",
|
||||
next: "Next Chat",
|
||||
prev: "Previous Chat",
|
||||
clear: "Clear Context",
|
||||
@@ -208,6 +207,8 @@ const en: LocaleType = {
|
||||
IsChecking: "Checking update...",
|
||||
FoundUpdate: (x: string) => `Found new version: ${x}`,
|
||||
GoToUpdate: "Update",
|
||||
Success: "Update Successful.",
|
||||
Failed: "Update Failed.",
|
||||
},
|
||||
SendKey: "Send Key",
|
||||
Theme: "Theme",
|
||||
@@ -235,11 +236,6 @@ const en: LocaleType = {
|
||||
Title: "Sync Type",
|
||||
SubTitle: "Choose your favorite sync service",
|
||||
},
|
||||
EnableAutoSync: {
|
||||
Title: "Auto Sync Settings",
|
||||
SubTitle:
|
||||
"Automatically synchronize data after replying or deleting messages",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Enable CORS Proxy",
|
||||
SubTitle: "Enable a proxy to avoid cross-origin restrictions",
|
||||
@@ -506,8 +502,8 @@ const en: LocaleType = {
|
||||
|
||||
Model: "Model",
|
||||
CompressModel: {
|
||||
Title: "Compression Model",
|
||||
SubTitle: "Model used to compress history",
|
||||
Title: "Summary Model",
|
||||
SubTitle: "Model used to compress history and generate title",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Temperature",
|
||||
@@ -681,6 +677,11 @@ const en: LocaleType = {
|
||||
Title: "Enable Artifacts",
|
||||
SubTitle: "Can render HTML page when enable artifacts.",
|
||||
},
|
||||
CodeFold: {
|
||||
Title: "Enable CodeFold",
|
||||
SubTitle:
|
||||
"Automatically collapse/expand overly long code blocks when CodeFold is enabled",
|
||||
},
|
||||
Share: {
|
||||
Title: "Share This Mask",
|
||||
SubTitle: "Generate a link to this mask",
|
||||
|
||||
@@ -211,7 +211,7 @@ export const useAccessStore = createPersistStore(
|
||||
})
|
||||
.then((res: DangerConfig) => {
|
||||
console.log("[Config] got config from server", res);
|
||||
set(() => ({ lastUpdateTime: Date.now(), ...res }));
|
||||
set(() => ({ ...res }));
|
||||
})
|
||||
.catch(() => {
|
||||
console.error("[Config] failed to fetch config");
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
getMessageTextContent,
|
||||
trimTopic,
|
||||
removeOutdatedEntries,
|
||||
} from "../utils";
|
||||
import { getMessageTextContent, trimTopic } from "../utils";
|
||||
|
||||
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
||||
import { nanoid } from "nanoid";
|
||||
@@ -33,7 +29,6 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { useAccessStore } from "./access";
|
||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
import { useSyncStore } from "./sync";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
@@ -85,7 +80,6 @@ export interface ChatSession {
|
||||
lastUpdate: number;
|
||||
lastSummarizeIndex: number;
|
||||
clearContextIndex?: number;
|
||||
deletedMessageIds?: Record<string, number>;
|
||||
|
||||
mask: Mask;
|
||||
}
|
||||
@@ -109,7 +103,6 @@ function createEmptySession(): ChatSession {
|
||||
},
|
||||
lastUpdate: Date.now(),
|
||||
lastSummarizeIndex: 0,
|
||||
deletedMessageIds: {},
|
||||
|
||||
mask: createEmptyMask(),
|
||||
};
|
||||
@@ -195,19 +188,9 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
||||
return output;
|
||||
}
|
||||
|
||||
let cloudSyncTimer: any = null;
|
||||
function noticeCloudSync(): void {
|
||||
const syncStore = useSyncStore.getState();
|
||||
cloudSyncTimer && clearTimeout(cloudSyncTimer);
|
||||
cloudSyncTimer = setTimeout(() => {
|
||||
syncStore.autoSync();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
const DEFAULT_CHAT_STATE = {
|
||||
sessions: [createEmptySession()],
|
||||
currentSessionIndex: 0,
|
||||
deletedSessionIds: {} as Record<string, number>,
|
||||
lastInput: "",
|
||||
};
|
||||
|
||||
@@ -257,28 +240,6 @@ export const useChatStore = createPersistStore(
|
||||
});
|
||||
},
|
||||
|
||||
copySession() {
|
||||
set((state) => {
|
||||
const { sessions, currentSessionIndex } = state;
|
||||
const emptySession = createEmptySession();
|
||||
|
||||
// copy the session
|
||||
const curSession = JSON.parse(
|
||||
JSON.stringify(sessions[currentSessionIndex]),
|
||||
);
|
||||
curSession.id = emptySession.id;
|
||||
curSession.lastUpdate = emptySession.lastUpdate;
|
||||
|
||||
const newSessions = [...sessions];
|
||||
newSessions.splice(0, 0, curSession);
|
||||
|
||||
return {
|
||||
currentSessionIndex: 0,
|
||||
sessions: newSessions,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
moveSession(from: number, to: number) {
|
||||
set((state) => {
|
||||
const { sessions, currentSessionIndex: oldIndex } = state;
|
||||
@@ -341,18 +302,7 @@ export const useChatStore = createPersistStore(
|
||||
if (!deletedSession) return;
|
||||
|
||||
const sessions = get().sessions.slice();
|
||||
const deletedSessionIds = { ...get().deletedSessionIds };
|
||||
|
||||
removeOutdatedEntries(deletedSessionIds);
|
||||
|
||||
const hasDelSessions = sessions.splice(index, 1);
|
||||
if (hasDelSessions?.length) {
|
||||
hasDelSessions.forEach((session) => {
|
||||
if (session.messages.length > 0) {
|
||||
deletedSessionIds[session.id] = Date.now();
|
||||
}
|
||||
});
|
||||
}
|
||||
sessions.splice(index, 1);
|
||||
|
||||
const currentIndex = get().currentSessionIndex;
|
||||
let nextIndex = Math.min(
|
||||
@@ -369,24 +319,19 @@ export const useChatStore = createPersistStore(
|
||||
const restoreState = {
|
||||
currentSessionIndex: get().currentSessionIndex,
|
||||
sessions: get().sessions.slice(),
|
||||
deletedSessionIds: get().deletedSessionIds,
|
||||
};
|
||||
|
||||
set(() => ({
|
||||
currentSessionIndex: nextIndex,
|
||||
sessions,
|
||||
deletedSessionIds,
|
||||
}));
|
||||
|
||||
noticeCloudSync();
|
||||
|
||||
showToast(
|
||||
Locale.Home.DeleteToast,
|
||||
{
|
||||
text: Locale.Home.Revert,
|
||||
onClick() {
|
||||
set(() => restoreState);
|
||||
noticeCloudSync();
|
||||
},
|
||||
},
|
||||
5000,
|
||||
@@ -407,24 +352,6 @@ export const useChatStore = createPersistStore(
|
||||
return session;
|
||||
},
|
||||
|
||||
sortSessions() {
|
||||
const currentSession = get().currentSession();
|
||||
const sessions = get().sessions.slice();
|
||||
|
||||
sessions.sort(
|
||||
(a, b) =>
|
||||
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
||||
);
|
||||
const currentSessionIndex = sessions.findIndex((session) => {
|
||||
return session && currentSession && session.id === currentSession.id;
|
||||
});
|
||||
|
||||
set((state) => ({
|
||||
currentSessionIndex,
|
||||
sessions,
|
||||
}));
|
||||
},
|
||||
|
||||
onNewMessage(message: ChatMessage) {
|
||||
get().updateCurrentSession((session) => {
|
||||
session.messages = session.messages.concat();
|
||||
@@ -432,8 +359,6 @@ export const useChatStore = createPersistStore(
|
||||
});
|
||||
get().updateStat(message);
|
||||
get().summarizeSession();
|
||||
get().sortSessions();
|
||||
noticeCloudSync();
|
||||
},
|
||||
|
||||
async onUserInput(content: string, attachImages?: string[]) {
|
||||
@@ -447,22 +372,16 @@ export const useChatStore = createPersistStore(
|
||||
|
||||
if (attachImages && attachImages.length > 0) {
|
||||
mContent = [
|
||||
{
|
||||
type: "text",
|
||||
text: userContent,
|
||||
},
|
||||
...(userContent
|
||||
? [{ type: "text" as const, text: userContent }]
|
||||
: []),
|
||||
...attachImages.map((url) => ({
|
||||
type: "image_url" as const,
|
||||
image_url: { url },
|
||||
})),
|
||||
];
|
||||
mContent = mContent.concat(
|
||||
attachImages.map((url) => {
|
||||
return {
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: url,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let userMessage: ChatMessage = createMessage({
|
||||
role: "user",
|
||||
content: mContent,
|
||||
|
||||
@@ -52,6 +52,8 @@ export const DEFAULT_CONFIG = {
|
||||
|
||||
enableArtifacts: true, // show artifacts config
|
||||
|
||||
enableCodeFold: true, // code fold config
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
|
||||
|
||||
@@ -19,6 +19,7 @@ export type Mask = {
|
||||
builtin: boolean;
|
||||
plugin?: string[];
|
||||
enableArtifacts?: boolean;
|
||||
enableCodeFold?: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_MASK_STATE = {
|
||||
|
||||
@@ -24,7 +24,6 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||
|
||||
const DEFAULT_SYNC_STATE = {
|
||||
provider: ProviderType.WebDAV,
|
||||
enableAutoSync: true,
|
||||
useProxy: true,
|
||||
proxyUrl: ApiPath.Cors as string,
|
||||
|
||||
@@ -44,8 +43,6 @@ const DEFAULT_SYNC_STATE = {
|
||||
lastProvider: "",
|
||||
};
|
||||
|
||||
let lastSyncTime = 0;
|
||||
|
||||
export const useSyncStore = createPersistStore(
|
||||
DEFAULT_SYNC_STATE,
|
||||
(set, get) => ({
|
||||
@@ -92,16 +89,6 @@ export const useSyncStore = createPersistStore(
|
||||
},
|
||||
|
||||
async sync() {
|
||||
if (lastSyncTime && lastSyncTime >= Date.now() - 800) {
|
||||
return;
|
||||
}
|
||||
lastSyncTime = Date.now();
|
||||
|
||||
const enableAutoSync = get().enableAutoSync;
|
||||
if (!enableAutoSync) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localState = getLocalAppState();
|
||||
const provider = get().provider;
|
||||
const config = get()[provider];
|
||||
@@ -116,7 +103,9 @@ export const useSyncStore = createPersistStore(
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
const parsedRemoteState = JSON.parse(remoteState) as AppState;
|
||||
const parsedRemoteState = JSON.parse(
|
||||
await client.get(config.username),
|
||||
) as AppState;
|
||||
mergeAppState(localState, parsedRemoteState);
|
||||
setLocalAppState(localState);
|
||||
}
|
||||
@@ -134,14 +123,6 @@ export const useSyncStore = createPersistStore(
|
||||
const client = this.getClient();
|
||||
return await client.check();
|
||||
},
|
||||
|
||||
async autoSync() {
|
||||
const { lastSyncTime, provider } = get();
|
||||
const syncStore = useSyncStore.getState();
|
||||
if (lastSyncTime && syncStore.cloudSync()) {
|
||||
syncStore.sync();
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Sync,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "../constant";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { clientUpdate } from "../utils";
|
||||
import ChatGptIcon from "../icons/chatgpt.png";
|
||||
import Locale from "../locales";
|
||||
import { ClientApi } from "../client/api";
|
||||
@@ -119,6 +120,7 @@ export const useUpdateStore = createPersistStore(
|
||||
icon: `${ChatGptIcon.src}`,
|
||||
sound: "Default",
|
||||
});
|
||||
clientUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
47
app/utils.ts
47
app/utils.ts
@@ -274,19 +274,6 @@ export function isDalle3(model: string) {
|
||||
return "dall-e-3" === model;
|
||||
}
|
||||
|
||||
export function removeOutdatedEntries(
|
||||
timeMap: Record<string, number>,
|
||||
): Record<string, number> {
|
||||
const oneMonthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
||||
// Delete data from a month ago
|
||||
Object.keys(timeMap).forEach((id) => {
|
||||
if (timeMap[id] < oneMonthAgo) {
|
||||
delete timeMap[id];
|
||||
}
|
||||
});
|
||||
return timeMap;
|
||||
}
|
||||
|
||||
export function showPlugins(provider: ServiceProvider, model: string) {
|
||||
if (
|
||||
provider == ServiceProvider.OpenAI ||
|
||||
@@ -399,3 +386,37 @@ export function getOperationId(operation: {
|
||||
`${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}`
|
||||
);
|
||||
}
|
||||
|
||||
export function clientUpdate() {
|
||||
// this a wild for updating client app
|
||||
return window.__TAURI__?.updater
|
||||
.checkUpdate()
|
||||
.then((updateResult) => {
|
||||
if (updateResult.shouldUpdate) {
|
||||
window.__TAURI__?.updater
|
||||
.installUpdate()
|
||||
.then((result) => {
|
||||
showToast(Locale.Settings.Update.Success);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("[Install Update Error]", e);
|
||||
showToast(Locale.Settings.Update.Failed);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("[Check Update Error]", e);
|
||||
showToast(Locale.Settings.Update.Failed);
|
||||
});
|
||||
}
|
||||
|
||||
// https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb
|
||||
export function semverCompare(a: string, b: string) {
|
||||
if (a.startsWith(b + "-")) return -1;
|
||||
if (b.startsWith(a + "-")) return 1;
|
||||
return a.localeCompare(b, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: "case",
|
||||
caseFirst: "upper",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useMaskStore } from "../store/mask";
|
||||
import { usePromptStore } from "../store/prompt";
|
||||
import { StoreKey } from "../constant";
|
||||
import { merge } from "./merge";
|
||||
import { removeOutdatedEntries } from "@/app/utils";
|
||||
|
||||
type NonFunctionKeys<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
|
||||
@@ -66,10 +65,7 @@ type StateMerger = {
|
||||
const MergeStates: StateMerger = {
|
||||
[StoreKey.Chat]: (localState, remoteState) => {
|
||||
// merge sessions
|
||||
const currentSession = useChatStore.getState().currentSession();
|
||||
|
||||
const localSessions: Record<string, ChatSession> = {};
|
||||
const localDeletedSessionIds = localState.deletedSessionIds || {};
|
||||
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
||||
|
||||
remoteState.sessions.forEach((remoteSession) => {
|
||||
@@ -79,98 +75,29 @@ const MergeStates: StateMerger = {
|
||||
const localSession = localSessions[remoteSession.id];
|
||||
if (!localSession) {
|
||||
// if remote session is new, just merge it
|
||||
if (
|
||||
(localDeletedSessionIds[remoteSession.id] || -1) <
|
||||
remoteSession.lastUpdate
|
||||
) {
|
||||
localState.sessions.push(remoteSession);
|
||||
}
|
||||
localState.sessions.push(remoteSession);
|
||||
} else {
|
||||
// if both have the same session id, merge the messages
|
||||
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
|
||||
const localDeletedMessageIds = localSession.deletedMessageIds || {};
|
||||
remoteSession.messages.forEach((m) => {
|
||||
if (!localMessageIds.has(m.id)) {
|
||||
if (
|
||||
!localDeletedMessageIds[m.id] ||
|
||||
new Date(localDeletedMessageIds[m.id]).toLocaleString() < m.date
|
||||
) {
|
||||
localSession.messages.push(m);
|
||||
}
|
||||
localSession.messages.push(m);
|
||||
}
|
||||
});
|
||||
|
||||
const remoteDeletedMessageIds = remoteSession.deletedMessageIds || {};
|
||||
localSession.messages = localSession.messages.filter((localMessage) => {
|
||||
return (
|
||||
!remoteDeletedMessageIds[localMessage.id] ||
|
||||
new Date(localDeletedMessageIds[localMessage.id]).toLocaleString() <
|
||||
localMessage.date
|
||||
);
|
||||
});
|
||||
|
||||
// sort local messages with date field in asc order
|
||||
localSession.messages.sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
||||
);
|
||||
localSession.lastUpdate = Math.max(
|
||||
remoteSession.lastUpdate,
|
||||
localSession.lastUpdate,
|
||||
);
|
||||
|
||||
const deletedMessageIds = {
|
||||
...remoteDeletedMessageIds,
|
||||
...localDeletedMessageIds,
|
||||
};
|
||||
removeOutdatedEntries(deletedMessageIds);
|
||||
localSession.deletedMessageIds = deletedMessageIds;
|
||||
}
|
||||
});
|
||||
|
||||
const remoteDeletedSessionIds = remoteState.deletedSessionIds || {};
|
||||
|
||||
const finalIds: Record<string, any> = {};
|
||||
localState.sessions = localState.sessions.filter((localSession) => {
|
||||
// 去除掉重复的会话
|
||||
if (finalIds[localSession.id]) {
|
||||
return false;
|
||||
}
|
||||
finalIds[localSession.id] = true;
|
||||
|
||||
// 去除掉非首个空会话,避免多个空会话在中间,不方便管理
|
||||
if (
|
||||
localSession.messages.length === 0 &&
|
||||
localSession != localState.sessions[0]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 去除云端删除并且删除时间小于本地修改时间的会话
|
||||
return (
|
||||
(remoteDeletedSessionIds[localSession.id] || -1) <=
|
||||
localSession.lastUpdate
|
||||
);
|
||||
});
|
||||
|
||||
// sort local sessions with date field in desc order
|
||||
localState.sessions.sort(
|
||||
(a, b) =>
|
||||
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
||||
);
|
||||
|
||||
const deletedSessionIds = {
|
||||
...remoteDeletedSessionIds,
|
||||
...localDeletedSessionIds,
|
||||
};
|
||||
removeOutdatedEntries(deletedSessionIds);
|
||||
localState.deletedSessionIds = deletedSessionIds;
|
||||
|
||||
localState.currentSessionIndex = localState.sessions.findIndex(
|
||||
(session) => {
|
||||
return session && currentSession && session.id === currentSession.id;
|
||||
},
|
||||
);
|
||||
|
||||
return localState;
|
||||
},
|
||||
[StoreKey.Prompt]: (localState, remoteState) => {
|
||||
@@ -226,9 +153,9 @@ export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
|
||||
remoteState: T,
|
||||
) {
|
||||
const localUpdateTime = localState.lastUpdateTime ?? 0;
|
||||
const remoteUpdateTime = remoteState.lastUpdateTime ?? 1;
|
||||
const remoteUpdateTime = localState.lastUpdateTime ?? 1;
|
||||
|
||||
if (localUpdateTime >= remoteUpdateTime) {
|
||||
if (localUpdateTime < remoteUpdateTime) {
|
||||
merge(remoteState, localState);
|
||||
return { ...remoteState };
|
||||
} else {
|
||||
|
||||
10
package.json
10
package.json
@@ -6,13 +6,13 @@
|
||||
"mask": "npx tsx app/masks/build.ts",
|
||||
"mask:watch": "npx watch \"yarn mask\" app/masks",
|
||||
"dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"",
|
||||
"build": "yarn test:ci && yarn mask && cross-env BUILD_MODE=standalone next build",
|
||||
"build": "yarn mask && cross-env BUILD_MODE=standalone next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "yarn test:ci && yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
||||
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
||||
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
||||
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
||||
"app:build": "yarn test:ci && yarn mask && yarn tauri build",
|
||||
"app:build": "yarn mask && yarn tauri build",
|
||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||
"prepare": "husky install",
|
||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@tauri-apps/cli": "1.5.11",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.30",
|
||||
@@ -88,4 +88,4 @@
|
||||
"lint-staged/yaml": "^2.2.2"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "NextChat",
|
||||
"version": "2.15.4"
|
||||
"version": "2.15.5"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
@@ -99,7 +99,7 @@
|
||||
"endpoints": [
|
||||
"https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/releases/latest/download/latest.json"
|
||||
],
|
||||
"dialog": false,
|
||||
"dialog": true,
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
},
|
||||
|
||||
@@ -2263,10 +2263,10 @@
|
||||
dependencies:
|
||||
"@types/istanbul-lib-report" "*"
|
||||
|
||||
"@types/jest@^29.5.12":
|
||||
version "29.5.12"
|
||||
resolved "https://registry.npmmirror.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544"
|
||||
integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==
|
||||
"@types/jest@^29.5.13":
|
||||
version "29.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.13.tgz#8bc571659f401e6a719a7bf0dbcb8b78c71a8adc"
|
||||
integrity sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==
|
||||
dependencies:
|
||||
expect "^29.0.0"
|
||||
pretty-format "^29.0.0"
|
||||
|
||||
Reference in New Issue
Block a user