Compare commits

..

34 Commits

Author SHA1 Message Date
lloydzhou
463fa743e9 update version
Some checks are pending
Run Tests / test (push) Waiting to run
2024-10-15 16:10:44 +08:00
Dogtiti
cda4494cec Merge pull request #5632 from ConnectAI-E/feature/H0llyW00dzZ-updater
Some checks are pending
Run Tests / test (push) Waiting to run
Feature/h0lly w00dz z updater
2024-10-15 14:31:49 +08:00
lloydzhou
87d85c10c3 update 2024-10-14 21:48:36 +08:00
Dogtiti
22f83c9e11 Merge pull request #5666 from ChatGPTNextWeb/dependabot/npm_and_yarn/types/jest-29.5.13
Some checks are pending
Run Tests / test (push) Waiting to run
Bump @types/jest from 29.5.12 to 29.5.13
2024-10-14 20:36:53 +08:00
dependabot[bot]
7f454cbcec Bump @types/jest from 29.5.12 to 29.5.13
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.12 to 29.5.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 10:49:46 +00:00
lloydzhou
426269d795 Merge remote-tracking branch 'connectai/main' into feature/H0llyW00dzZ-updater 2024-10-14 17:12:08 +08:00
Lloyd Zhou
370f143157 Merge pull request #5661 from ChatGPTNextWeb/remove-pr-preview
Some checks are pending
Run Tests / test (push) Waiting to run
update test run target
2024-10-14 17:11:26 +08:00
lloydzhou
103106bb93 update test run target 2024-10-14 17:10:02 +08:00
lloydzhou
2419083adf Merge remote-tracking branch 'connectai/main' into feature/H0llyW00dzZ-updater 2024-10-14 17:04:12 +08:00
Lloyd Zhou
c25903bfb4 Merge pull request #5658 from ccq18/main
fix  o1系列模型超时时间改为4分钟,
2024-10-14 16:57:29 +08:00
Lloyd Zhou
e34c266438 Merge pull request #5660 from ChatGPTNextWeb/remove-pr-preview
update deploy_preview run target
2024-10-14 16:55:48 +08:00
lloydzhou
8c39a687b5 update deploy_preview run target 2024-10-14 16:53:46 +08:00
ccq18
592f62005b 仅修改o1的超时时间为4分钟,减少o1系列模型请求失败的情况 2024-10-14 16:31:17 +08:00
ccq18
12e7caa209 fix 默认超时时间改为3分钟,支持o1-mini 2024-10-14 16:03:01 +08:00
Lloyd Zhou
b016771555 Merge pull request #5599 from ConnectAI-E/feature/allow-send-image-only
feat: allow send image only
2024-10-14 15:11:28 +08:00
Dogtiti
a84383f919 Merge pull request #5647 from code-october/fix/setting-locale
Some checks failed
Run Tests / test (push) Has been cancelled
修改“压缩模型”名称,增加“生成对话标题”的功能提示
2024-10-13 01:49:51 +08:00
code-october
7f68fb1ff2 修改“压缩模型”名称,增加“生成对话标题”的功能提示 2024-10-12 16:49:24 +00:00
Dogtiti
8d2003fe68 Merge pull request #5644 from ConnectAI-E/fix/siderbar-style
Some checks are pending
Run Tests / test (push) Waiting to run
fix: sidebar style
2024-10-12 14:56:01 +08:00
Dogtiti
9961b513cc fix: sidebar style 2024-10-12 14:54:22 +08:00
Dogtiti
819238acaf fix: i18n 2024-10-11 20:49:43 +08:00
Dogtiti
ad49916b1c Merge pull request #5638 from ConnectAI-E/chore/test-action
Some checks are pending
Run Tests / test (push) Waiting to run
chore: improve test
2024-10-11 20:44:20 +08:00
Dogtiti
d18bd8a48a Merge pull request #5640 from code-october/feature/enableCodeFold
支持前端使能/禁用代码折叠
2024-10-11 20:43:43 +08:00
code-october
4a1319f2c0 代码安全优化 2024-10-11 11:57:23 +00:00
code-october
8fd843d228 参考coderabbitai建议规范代码 2024-10-11 11:38:52 +00:00
code-october
6792d6e475 支持前端使能/禁用代码折叠 2024-10-11 11:19:36 +00:00
Lloyd Zhou
c139038e01 Merge pull request #5639 from code-october/fix/auth-ui
优化访问码输入框
2024-10-11 19:11:35 +08:00
code-october
4a7fd3a380 优化首页 api 输入框 2024-10-11 10:36:11 +00:00
code-october
c98dc31cdf 优化访问码输入框 2024-10-11 09:03:20 +00:00
Dogtiti
bd43af3a8d chore: cache node_modules 2024-10-11 15:41:46 +08:00
Dogtiti
be98aa2078 chore: improve test 2024-10-11 15:17:38 +08:00
lloydzhou
a0d4a04192 update 2024-10-11 11:52:24 +08:00
lloydzhou
bd9de4dc4d fix version compare 2024-10-11 11:42:36 +08:00
lloydzhou
2eebfcf6fe client app tauri updater #2966 2024-10-11 11:29:22 +08:00
Dogtiti
7d55a6d0e4 feat: allow send image only 2024-10-08 21:31:01 +08:00
26 changed files with 248 additions and 304 deletions

View File

@@ -3,9 +3,7 @@ name: VercelPreviewDeployment
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
- review_requested
env:
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}

View File

@@ -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
View 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

View File

@@ -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);

View File

@@ -35,7 +35,6 @@ export function useCommand(commands: Commands = {}) {
interface ChatCommands {
new?: Command;
newm?: Command;
copy?: Command;
next?: Command;
prev?: Command;
clear?: Command;

View File

@@ -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),

View File

@@ -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) => {

View File

@@ -140,6 +140,9 @@
display: flex;
justify-content: space-between;
align-items: center;
&-narrow {
justify-content: center;
}
}
.sidebar-logo {

View File

@@ -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()}
</>
);
}

View File

@@ -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

View File

@@ -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 />

View File

@@ -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
View File

@@ -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,

View File

@@ -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: "生成此面具的直达链接",

View File

@@ -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",

View File

@@ -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");

View File

@@ -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,

View File

@@ -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

View File

@@ -19,6 +19,7 @@ export type Mask = {
builtin: boolean;
plugin?: string[];
enableArtifacts?: boolean;
enableCodeFold?: boolean;
};
export const DEFAULT_MASK_STATE = {

View File

@@ -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,

View File

@@ -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();
}
}
});

View File

@@ -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",
});
}

View File

@@ -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 {

View File

@@ -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"
}
}

View File

@@ -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"
},

View File

@@ -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"