mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-18 06:53:41 +08:00
Compare commits
22 Commits
35f52886c4
...
v2.15.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df62736ff6 | ||
|
|
6a464b3e5f | ||
|
|
57fcda80df | ||
|
|
db39fbc419 | ||
|
|
3dabe47c78 | ||
|
|
affc194cde | ||
|
|
03fa580a55 | ||
|
|
d0dce654bf | ||
|
|
169323e238 | ||
|
|
71df415b14 | ||
|
|
6bb01bc564 | ||
|
|
07c6fe5975 | ||
|
|
5964181cb2 | ||
|
|
4b8288a2b2 | ||
|
|
88b1c1c6a5 | ||
|
|
1234deabfa | ||
|
|
45306bbb6c | ||
|
|
18e2403b01 | ||
|
|
e578c5f3ad | ||
|
|
61245e3d7e | ||
|
|
7804182d0d | ||
|
|
f2195154f6 |
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
|
||||
|
||||
|
||||
|
||||
@@ -160,6 +160,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
let requestPayload: RequestPayload | DalleRequestPayload;
|
||||
|
||||
const isDalle3 = _isDalle3(options.config.model);
|
||||
const isO1 = options.config.model.startsWith("o1");
|
||||
if (isDalle3) {
|
||||
const prompt = getMessageTextContent(
|
||||
options.messages.slice(-1)?.pop() as any,
|
||||
@@ -181,30 +182,32 @@ export class ChatGPTApi implements LLMApi {
|
||||
const content = visionModel
|
||||
? await preProcessImageContent(v.content)
|
||||
: getMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
if (!(isO1 && v.role === "system"))
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
|
||||
requestPayload = {
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
stream: !isO1 ? options.config.stream : false,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
temperature: !isO1 ? modelConfig.temperature : 1,
|
||||
presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
|
||||
frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0,
|
||||
top_p: !isO1 ? modelConfig.top_p : 1,
|
||||
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
||||
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||
};
|
||||
|
||||
// add max_tokens to vision model
|
||||
if (visionModel && modelConfig.model.includes("preview")) {
|
||||
if (visionModel) {
|
||||
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[Request] openai payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !isDalle3 && !!options.config.stream;
|
||||
const shouldStream = !isDalle3 && !!options.config.stream && !isO1;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
@@ -313,7 +316,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : 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;
|
||||
|
||||
@@ -80,7 +80,7 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
|
||||
}, [props.autoHeight, props.height, iframeHeight]);
|
||||
|
||||
const srcDoc = useMemo(() => {
|
||||
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`;
|
||||
const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
|
||||
if (props.code.includes("<!DOCTYPE html>")) {
|
||||
props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
|
||||
}
|
||||
|
||||
@@ -646,3 +646,51 @@
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-key-container {
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shortcut-key-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.shortcut-key-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.shortcut-key-title {
|
||||
font-size: 14px;
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.shortcut-key-keys {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: var(--border-in-light);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
background-color: var(--gray);
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.shortcut-key span {
|
||||
font-size: 12px;
|
||||
color: var(--black);
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import SizeIcon from "../icons/size.svg";
|
||||
import QualityIcon from "../icons/hd.svg";
|
||||
import StyleIcon from "../icons/palette.svg";
|
||||
import PluginIcon from "../icons/plugin.svg";
|
||||
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
||||
|
||||
import {
|
||||
ChatMessage,
|
||||
@@ -66,7 +67,6 @@ import {
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
isDalle3,
|
||||
removeOutdatedEntries,
|
||||
showPlugins,
|
||||
safeLocalStorage,
|
||||
} from "../utils";
|
||||
@@ -192,7 +192,7 @@ function PromptToast(props: {
|
||||
|
||||
return (
|
||||
<div className={styles["prompt-toast"]} key="prompt-toast">
|
||||
{props.showToast && (
|
||||
{props.showToast && context.length > 0 && (
|
||||
<div
|
||||
className={styles["prompt-toast-inner"] + " clickable"}
|
||||
role="button"
|
||||
@@ -441,6 +441,7 @@ export function ChatActions(props: {
|
||||
showPromptHints: () => void;
|
||||
hitBottom: boolean;
|
||||
uploading: boolean;
|
||||
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const config = useAppConfig();
|
||||
const navigate = useNavigate();
|
||||
@@ -506,6 +507,8 @@ export function ChatActions(props: {
|
||||
const currentStyle =
|
||||
chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
useEffect(() => {
|
||||
const show = isVisionModel(currentModel);
|
||||
setShowUploadImage(show);
|
||||
@@ -759,6 +762,14 @@ export function ChatActions(props: {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isMobileScreen && (
|
||||
<ChatAction
|
||||
onClick={() => props.setShowShortcutKeyModal(true)}
|
||||
text={Locale.Chat.ShortcutKey.Title}
|
||||
icon={<ShortcutkeyIcon />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -833,6 +844,67 @@ export function DeleteImageButton(props: { deleteImage: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ShortcutKeyModal(props: { onClose: () => void }) {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
const shortcuts = [
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.newChat,
|
||||
keys: isMac ? ["⌘", "Shift", "O"] : ["Ctrl", "Shift", "O"],
|
||||
},
|
||||
{ title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] },
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.copyLastCode,
|
||||
keys: isMac ? ["⌘", "Shift", ";"] : ["Ctrl", "Shift", ";"],
|
||||
},
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.copyLastMessage,
|
||||
keys: isMac ? ["⌘", "Shift", "C"] : ["Ctrl", "Shift", "C"],
|
||||
},
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.showShortcutKey,
|
||||
keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title={Locale.Chat.ShortcutKey.Title}
|
||||
onClose={props.onClose}
|
||||
actions={[
|
||||
<IconButton
|
||||
type="primary"
|
||||
text={Locale.UI.Confirm}
|
||||
icon={<ConfirmIcon />}
|
||||
key="ok"
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<div className={styles["shortcut-key-container"]}>
|
||||
<div className={styles["shortcut-key-grid"]}>
|
||||
{shortcuts.map((shortcut, index) => (
|
||||
<div key={index} className={styles["shortcut-key-item"]}>
|
||||
<div className={styles["shortcut-key-title"]}>
|
||||
{shortcut.title}
|
||||
</div>
|
||||
<div className={styles["shortcut-key-keys"]}>
|
||||
{shortcut.keys.map((key, i) => (
|
||||
<div key={i} className={styles["shortcut-key"]}>
|
||||
<span>{key}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function _Chat() {
|
||||
type RenderMessage = ChatMessage & { preview?: boolean };
|
||||
|
||||
@@ -902,7 +974,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: () =>
|
||||
@@ -1033,20 +1104,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) => {
|
||||
@@ -1388,6 +1449,70 @@ function _Chat() {
|
||||
setAttachImages(images);
|
||||
}
|
||||
|
||||
// 快捷键 shortcut keys
|
||||
const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: any) => {
|
||||
// 打开新聊天 command + shift + o
|
||||
if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.key.toLowerCase() === "o"
|
||||
) {
|
||||
event.preventDefault();
|
||||
setTimeout(() => {
|
||||
chatStore.newSession();
|
||||
navigate(Path.Chat);
|
||||
}, 10);
|
||||
}
|
||||
// 聚焦聊天输入 shift + esc
|
||||
else if (event.shiftKey && event.key.toLowerCase() === "escape") {
|
||||
event.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
// 复制最后一个代码块 command + shift + ;
|
||||
else if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.code === "Semicolon"
|
||||
) {
|
||||
event.preventDefault();
|
||||
const copyCodeButton =
|
||||
document.querySelectorAll<HTMLElement>(".copy-code-button");
|
||||
if (copyCodeButton.length > 0) {
|
||||
copyCodeButton[copyCodeButton.length - 1].click();
|
||||
}
|
||||
}
|
||||
// 复制最后一个回复 command + shift + c
|
||||
else if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.key.toLowerCase() === "c"
|
||||
) {
|
||||
event.preventDefault();
|
||||
const lastNonUserMessage = messages
|
||||
.filter((message) => message.role !== "user")
|
||||
.pop();
|
||||
if (lastNonUserMessage) {
|
||||
const lastMessageContent = getMessageTextContent(lastNonUserMessage);
|
||||
copyToClipboard(lastMessageContent);
|
||||
}
|
||||
}
|
||||
// 展示快捷键 command + /
|
||||
else if ((event.metaKey || event.ctrlKey) && event.key === "/") {
|
||||
event.preventDefault();
|
||||
setShowShortcutKeyModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [messages, chatStore, navigate]);
|
||||
|
||||
return (
|
||||
<div className={styles.chat} key={session.id}>
|
||||
<div className="window-header" data-tauri-drag-region>
|
||||
@@ -1704,6 +1829,7 @@ function _Chat() {
|
||||
setUserInput("/");
|
||||
onSearch("");
|
||||
}}
|
||||
setShowShortcutKeyModal={setShowShortcutKeyModal}
|
||||
/>
|
||||
<label
|
||||
className={`${styles["chat-input-panel-inner"]} ${
|
||||
@@ -1775,6 +1901,10 @@ function _Chat() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showShortcutKeyModal && (
|
||||
<ShortcutKeyModal onClose={() => setShowShortcutKeyModal(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,9 +237,26 @@ function escapeBrackets(text: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function tryWrapHtmlCode(text: string) {
|
||||
// try add wrap html code (fixed: html codeblock include 2 newline)
|
||||
return text
|
||||
.replace(
|
||||
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
|
||||
(match, quoteStart, lang, newLine, doctype) => {
|
||||
return !quoteStart ? "\n```html\n" + doctype : match;
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g,
|
||||
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
|
||||
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
const escapedContent = useMemo(() => {
|
||||
return escapeBrackets(escapeDollarNumber(props.content));
|
||||
return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
|
||||
}, [props.content]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -357,21 +357,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}
|
||||
|
||||
@@ -250,6 +250,8 @@ export const KnowledgeCutOffDate: Record<string, string> = {
|
||||
"gpt-4o-mini": "2023-10",
|
||||
"gpt-4o-mini-2024-07-18": "2023-10",
|
||||
"gpt-4-vision-preview": "2023-04",
|
||||
"o1-mini": "2023-10",
|
||||
"o1-preview": "2023-10",
|
||||
// After improvements,
|
||||
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
|
||||
"gemini-pro": "2023-12",
|
||||
@@ -276,6 +278,8 @@ const openaiModels = [
|
||||
"gpt-4-turbo-2024-04-09",
|
||||
"gpt-4-1106-preview",
|
||||
"dall-e-3",
|
||||
"o1-mini",
|
||||
"o1-preview"
|
||||
];
|
||||
|
||||
const googleModels = [
|
||||
|
||||
1
app/icons/shortcutkey.svg
Normal file
1
app/icons/shortcutkey.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M42 7H6C4.89543 7 4 7.89543 4 9V37C4 38.1046 4.89543 39 6 39H42C43.1046 39 44 38.1046 44 37V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"/><path d="M12 19H14" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 19H23" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M29 19H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 28H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 734 B |
@@ -41,11 +41,7 @@ export default function RootLayout({
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link
|
||||
rel="manifest"
|
||||
href="/site.webmanifest"
|
||||
crossOrigin="use-credentials"
|
||||
></link>
|
||||
<link rel="manifest" href="/site.webmanifest" crossOrigin="use-credentials"></link>
|
||||
<script src="/serviceWorkerRegister.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ShortcutKeyModal } from "../components/chat";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
|
||||
@@ -47,7 +48,6 @@ const cn = {
|
||||
Commands: {
|
||||
new: "新建聊天",
|
||||
newm: "从面具新建聊天",
|
||||
copy: "复制当前聊天",
|
||||
next: "下一个聊天",
|
||||
prev: "上一个聊天",
|
||||
clear: "清除上下文",
|
||||
@@ -82,6 +82,14 @@ const cn = {
|
||||
SaveAs: "存为面具",
|
||||
},
|
||||
IsContext: "预设提示词",
|
||||
ShortcutKey: {
|
||||
Title: "键盘快捷方式",
|
||||
newChat: "打开新聊天",
|
||||
focusInput: "聚焦输入框",
|
||||
copyLastMessage: "复制最后一个回复",
|
||||
copyLastCode: "复制最后一个代码块",
|
||||
showShortcutKey: "显示快捷方式",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "分享聊天记录",
|
||||
@@ -207,10 +215,6 @@ const cn = {
|
||||
Title: "同步类型",
|
||||
SubTitle: "选择喜爱的同步服务器",
|
||||
},
|
||||
EnableAutoSync: {
|
||||
Title: "自动同步设置",
|
||||
SubTitle: "在回复完成或删除消息后自动同步数据",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "启用代理",
|
||||
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
||||
@@ -500,8 +504,8 @@ const cn = {
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "已写入剪切板",
|
||||
Failed: "复制失败,请赋予剪切板权限",
|
||||
Success: "已写入剪贴板",
|
||||
Failed: "复制失败,请赋予剪贴板权限",
|
||||
},
|
||||
Download: {
|
||||
Success: "内容已下载到您的目录。",
|
||||
|
||||
@@ -49,7 +49,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",
|
||||
@@ -84,6 +83,14 @@ const en: LocaleType = {
|
||||
SaveAs: "Save as Mask",
|
||||
},
|
||||
IsContext: "Contextual Prompt",
|
||||
ShortcutKey: {
|
||||
Title: "Keyboard Shortcuts",
|
||||
newChat: "Open New Chat",
|
||||
focusInput: "Focus Input Field",
|
||||
copyLastMessage: "Copy Last Reply",
|
||||
copyLastCode: "Copy Last Code Block",
|
||||
showShortcutKey: "Show Shortcuts",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "Export Messages",
|
||||
@@ -210,11 +217,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",
|
||||
|
||||
@@ -81,6 +81,14 @@ const tw = {
|
||||
SaveAs: "另存新檔",
|
||||
},
|
||||
IsContext: "預設提示詞",
|
||||
ShortcutKey: {
|
||||
Title: "鍵盤快捷方式",
|
||||
newChat: "打開新聊天",
|
||||
focusInput: "聚焦輸入框",
|
||||
copyLastMessage: "複製最後一個回覆",
|
||||
copyLastCode: "複製最後一個代碼塊",
|
||||
showShortcutKey: "顯示快捷方式",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "將聊天記錄匯出為 Markdown",
|
||||
|
||||
@@ -210,7 +210,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 {
|
||||
trimTopic,
|
||||
getMessageTextContent,
|
||||
removeOutdatedEntries,
|
||||
} from "../utils";
|
||||
import { trimTopic, getMessageTextContent } from "../utils";
|
||||
|
||||
import Locale, { getLang } from "../locales";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
@@ -31,7 +27,6 @@ import { createPersistStore } from "../utils/store";
|
||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||
import { useAccessStore } from "./access";
|
||||
import { isDalle3, safeLocalStorage } from "../utils";
|
||||
import { useSyncStore } from "./sync";
|
||||
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
@@ -83,7 +78,6 @@ export interface ChatSession {
|
||||
lastUpdate: number;
|
||||
lastSummarizeIndex: number;
|
||||
clearContextIndex?: number;
|
||||
deletedMessageIds?: Record<string, number>;
|
||||
|
||||
mask: Mask;
|
||||
}
|
||||
@@ -107,7 +101,6 @@ function createEmptySession(): ChatSession {
|
||||
},
|
||||
lastUpdate: Date.now(),
|
||||
lastSummarizeIndex: 0,
|
||||
deletedMessageIds: {},
|
||||
|
||||
mask: createEmptyMask(),
|
||||
};
|
||||
@@ -185,19 +178,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: "",
|
||||
};
|
||||
|
||||
@@ -225,28 +208,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;
|
||||
@@ -309,18 +270,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(
|
||||
@@ -337,24 +287,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,
|
||||
@@ -375,24 +320,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();
|
||||
@@ -400,8 +327,6 @@ export const useChatStore = createPersistStore(
|
||||
});
|
||||
get().updateStat(message);
|
||||
get().summarizeSession();
|
||||
get().sortSessions();
|
||||
noticeCloudSync();
|
||||
},
|
||||
|
||||
async onUserInput(content: string, attachImages?: string[]) {
|
||||
|
||||
@@ -26,7 +26,6 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||
|
||||
const DEFAULT_SYNC_STATE = {
|
||||
provider: ProviderType.WebDAV,
|
||||
enableAutoSync: true,
|
||||
useProxy: true,
|
||||
proxyUrl: corsPath(ApiPath.Cors),
|
||||
|
||||
@@ -46,8 +45,6 @@ const DEFAULT_SYNC_STATE = {
|
||||
lastProvider: "",
|
||||
};
|
||||
|
||||
let lastSyncTime = 0;
|
||||
|
||||
export const useSyncStore = createPersistStore(
|
||||
DEFAULT_SYNC_STATE,
|
||||
(set, get) => ({
|
||||
@@ -94,16 +91,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];
|
||||
@@ -113,15 +100,15 @@ export const useSyncStore = createPersistStore(
|
||||
const remoteState = await client.get(config.username);
|
||||
if (!remoteState || remoteState === "") {
|
||||
await client.set(config.username, JSON.stringify(localState));
|
||||
console.log(
|
||||
"[Sync] Remote state is empty, using local state instead.",
|
||||
);
|
||||
return;
|
||||
console.log("[Sync] Remote state is empty, using local state instead.");
|
||||
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);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Sync] failed to get remote state", e);
|
||||
throw e;
|
||||
@@ -136,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,
|
||||
|
||||
13
app/utils.ts
13
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 ||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "NextChat",
|
||||
"version": "2.15.1"
|
||||
"version": "2.15.2"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
||||
Reference in New Issue
Block a user