mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-04-27 05:24:26 +08:00
Compare commits
32 Commits
v2.14.2
...
f1d69cb312
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1d69cb312 | ||
|
|
a6b14c7910 | ||
|
|
31baa10363 | ||
|
|
5c51fd2ed8 | ||
|
|
e275abdb9c | ||
|
|
09a90665d5 | ||
|
|
6649fbdfd0 | ||
|
|
64a0ffee7b | ||
|
|
b529118f31 | ||
|
|
39d7d9f13a | ||
|
|
fcd55df969 | ||
|
|
0745b6498d | ||
|
|
1e59948358 | ||
|
|
1102ef6e6b | ||
|
|
fdb89af355 | ||
|
|
b2336f5ed9 | ||
|
|
7ce2e8f4c4 | ||
|
|
fd1c656bdd | ||
|
|
82298a760a | ||
|
|
b84bb72e07 | ||
|
|
0a6ddda992 | ||
|
|
e3f499be0c | ||
|
|
eae593d660 | ||
|
|
621b1480c2 | ||
|
|
86220573b6 | ||
|
|
65ed6b02a4 | ||
|
|
98093a1f31 | ||
|
|
00990dc195 | ||
|
|
3da5284a07 | ||
|
|
cd920364f8 | ||
|
|
93bfb55822 | ||
|
|
648e60028d |
18
.github/workflows/docker.yml
vendored
18
.github/workflows/docker.yml
vendored
@@ -19,26 +19,26 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Extract metadata (tags, labels) for Docker
|
name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: yidadaa/chatgpt-next-web
|
images: ${{ secrets.DOCKER_USERNAME }}/chatgpt-next-web
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Build and push Docker image
|
name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -49,4 +49,4 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import {
|
|||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
isDalle3,
|
isDalle3,
|
||||||
|
removeOutdatedEntries,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
||||||
@@ -1023,10 +1024,20 @@ function _Chat() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteMessage = (msgId?: string) => {
|
const deleteMessage = (msgId?: string) => {
|
||||||
chatStore.updateCurrentSession(
|
chatStore.updateCurrentSession((session) => {
|
||||||
(session) =>
|
session.deletedMessageIds &&
|
||||||
(session.messages = session.messages.filter((m) => m.id !== msgId)),
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDelete = (msgId: string) => {
|
const onDelete = (msgId: string) => {
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
|
|||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const SearchChat = dynamic(
|
||||||
|
async () => (await import("./search-chat")).SearchChatPage,
|
||||||
|
{
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const Sd = dynamic(async () => (await import("./sd")).Sd, {
|
const Sd = dynamic(async () => (await import("./sd")).Sd, {
|
||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
@@ -174,6 +181,7 @@ function Screen() {
|
|||||||
<Route path={Path.Home} element={<Chat />} />
|
<Route path={Path.Home} element={<Chat />} />
|
||||||
<Route path={Path.NewChat} element={<NewChat />} />
|
<Route path={Path.NewChat} element={<NewChat />} />
|
||||||
<Route path={Path.Masks} element={<MaskPage />} />
|
<Route path={Path.Masks} element={<MaskPage />} />
|
||||||
|
<Route path={Path.SearchChat} element={<SearchChat />} />
|
||||||
<Route path={Path.Chat} element={<Chat />} />
|
<Route path={Path.Chat} element={<Chat />} />
|
||||||
<Route path={Path.Settings} element={<Settings />} />
|
<Route path={Path.Settings} element={<Settings />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
167
app/components/search-chat.tsx
Normal file
167
app/components/search-chat.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import { ErrorBoundary } from "./error";
|
||||||
|
import styles from "./mask.module.scss";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { IconButton } from "./button";
|
||||||
|
import CloseIcon from "../icons/close.svg";
|
||||||
|
import EyeIcon from "../icons/eye.svg";
|
||||||
|
import Locale from "../locales";
|
||||||
|
import { Path } from "../constant";
|
||||||
|
|
||||||
|
import { useChatStore } from "../store";
|
||||||
|
|
||||||
|
type Item = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
export function SearchChatPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
|
const sessions = chatStore.sessions;
|
||||||
|
const selectSession = chatStore.selectSession;
|
||||||
|
|
||||||
|
const [searchResults, setSearchResults] = useState<Item[]>([]);
|
||||||
|
|
||||||
|
const previousValueRef = useRef<string>("");
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const doSearch = useCallback((text: string) => {
|
||||||
|
const lowerCaseText = text.toLowerCase();
|
||||||
|
const results: Item[] = [];
|
||||||
|
|
||||||
|
sessions.forEach((session, index) => {
|
||||||
|
const fullTextContents: string[] = [];
|
||||||
|
|
||||||
|
session.messages.forEach((message) => {
|
||||||
|
const content = message.content as string;
|
||||||
|
if (!content.toLowerCase || content === "") return;
|
||||||
|
const lowerCaseContent = content.toLowerCase();
|
||||||
|
|
||||||
|
// full text search
|
||||||
|
let pos = lowerCaseContent.indexOf(lowerCaseText);
|
||||||
|
while (pos !== -1) {
|
||||||
|
const start = Math.max(0, pos - 35);
|
||||||
|
const end = Math.min(content.length, pos + lowerCaseText.length + 35);
|
||||||
|
fullTextContents.push(content.substring(start, end));
|
||||||
|
pos = lowerCaseContent.indexOf(
|
||||||
|
lowerCaseText,
|
||||||
|
pos + lowerCaseText.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fullTextContents.length > 0) {
|
||||||
|
results.push({
|
||||||
|
id: index,
|
||||||
|
name: session.topic,
|
||||||
|
content: fullTextContents.join("... "), // concat content with...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort by length of matching content
|
||||||
|
results.sort((a, b) => b.content.length - a.content.length);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (searchInputRef.current) {
|
||||||
|
const currentValue = searchInputRef.current.value;
|
||||||
|
if (currentValue !== previousValueRef.current) {
|
||||||
|
if (currentValue.length > 0) {
|
||||||
|
const result = doSearch(currentValue);
|
||||||
|
setSearchResults(result);
|
||||||
|
}
|
||||||
|
previousValueRef.current = currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Cleanup the interval on component unmount
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [doSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className={styles["mask-page"]}>
|
||||||
|
{/* header */}
|
||||||
|
<div className="window-header">
|
||||||
|
<div className="window-header-title">
|
||||||
|
<div className="window-header-main-title">
|
||||||
|
{Locale.SearchChat.Page.Title}
|
||||||
|
</div>
|
||||||
|
<div className="window-header-submai-title">
|
||||||
|
{Locale.SearchChat.Page.SubTitle(searchResults.length)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="window-actions">
|
||||||
|
<div className="window-action-button">
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
bordered
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["mask-page-body"]}>
|
||||||
|
<div className={styles["mask-filter"]}>
|
||||||
|
{/**搜索输入框 */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles["search-bar"]}
|
||||||
|
placeholder={Locale.SearchChat.Page.Search}
|
||||||
|
autoFocus
|
||||||
|
ref={searchInputRef}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const searchText = e.currentTarget.value;
|
||||||
|
if (searchText.length > 0) {
|
||||||
|
const result = doSearch(searchText);
|
||||||
|
setSearchResults(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{searchResults.map((item) => (
|
||||||
|
<div
|
||||||
|
className={styles["mask-item"]}
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(Path.Chat);
|
||||||
|
selectSession(item.id);
|
||||||
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{/** 搜索匹配的文本 */}
|
||||||
|
<div className={styles["mask-header"]}>
|
||||||
|
<div className={styles["mask-title"]}>
|
||||||
|
<div className={styles["mask-name"]}>{item.name}</div>
|
||||||
|
{item.content.slice(0, 70)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/** 操作按钮 */}
|
||||||
|
<div className={styles["mask-actions"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<EyeIcon />}
|
||||||
|
text={Locale.SearchChat.Item.View}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -357,6 +357,21 @@ function SyncConfigModal(props: { onClose?: () => void }) {
|
|||||||
</select>
|
</select>
|
||||||
</ListItem>
|
</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
|
<ListItem
|
||||||
title={Locale.Settings.Sync.Config.Proxy.Title}
|
title={Locale.Settings.Sync.Config.Proxy.Title}
|
||||||
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
|
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import path from "path";
|
||||||
|
|
||||||
export const OWNER = "ChatGPTNextWeb";
|
export const OWNER = "ChatGPTNextWeb";
|
||||||
export const REPO = "ChatGPT-Next-Web";
|
export const REPO = "ChatGPT-Next-Web";
|
||||||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
||||||
@@ -41,6 +43,7 @@ export enum Path {
|
|||||||
Sd = "/sd",
|
Sd = "/sd",
|
||||||
SdNew = "/sd-new",
|
SdNew = "/sd-new",
|
||||||
Artifacts = "/artifacts",
|
Artifacts = "/artifacts",
|
||||||
|
SearchChat = "/search-chat",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ApiPath {
|
export enum ApiPath {
|
||||||
@@ -475,4 +478,7 @@ export const internalAllowedWebDavEndpoints = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
|
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
|
||||||
export const PLUGINS = [{ name: "Stable Diffusion", path: Path.Sd }];
|
export const PLUGINS = [
|
||||||
|
{ name: "Stable Diffusion", path: Path.Sd },
|
||||||
|
{ name: "Search Chat", path: Path.SearchChat },
|
||||||
|
];
|
||||||
|
|||||||
1
app/icons/zoom.svg
Normal file
1
app/icons/zoom.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24"><g fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></g></svg>
|
||||||
|
After Width: | Height: | Size: 285 B |
@@ -459,6 +459,21 @@ const ar: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "أنت مساعد",
|
Sysmessage: "أنت مساعد",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "بحث",
|
||||||
|
Page: {
|
||||||
|
Title: "البحث في سجلات الدردشة",
|
||||||
|
Search: "أدخل كلمات البحث",
|
||||||
|
NoResult: "لم يتم العثور على نتائج",
|
||||||
|
NoData: "لا توجد بيانات",
|
||||||
|
Loading: "جارٍ التحميل",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `تم العثور على ${count} نتائج`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "عرض",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "القناع",
|
Name: "القناع",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -466,6 +466,21 @@ const bn: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "আপনি একজন সহকারী",
|
Sysmessage: "আপনি একজন সহকারী",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "অনুসন্ধান",
|
||||||
|
Page: {
|
||||||
|
Title: "চ্যাট রেকর্ড অনুসন্ধান করুন",
|
||||||
|
Search: "অনুসন্ধান কীওয়ার্ড লিখুন",
|
||||||
|
NoResult: "কোন ফলাফল পাওয়া যায়নি",
|
||||||
|
NoData: "কোন তথ্য নেই",
|
||||||
|
Loading: "লোড হচ্ছে",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count} টি ফলাফল পাওয়া গেছে`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "দেখুন",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "মাস্ক",
|
Name: "মাস্ক",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -206,6 +206,10 @@ const cn = {
|
|||||||
Title: "同步类型",
|
Title: "同步类型",
|
||||||
SubTitle: "选择喜爱的同步服务器",
|
SubTitle: "选择喜爱的同步服务器",
|
||||||
},
|
},
|
||||||
|
EnableAutoSync: {
|
||||||
|
Title: "自动同步设置",
|
||||||
|
SubTitle: "在回复完成或删除消息后自动同步数据",
|
||||||
|
},
|
||||||
Proxy: {
|
Proxy: {
|
||||||
Title: "启用代理",
|
Title: "启用代理",
|
||||||
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
||||||
@@ -519,6 +523,21 @@ const cn = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "你是一个助手",
|
Sysmessage: "你是一个助手",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "搜索",
|
||||||
|
Page: {
|
||||||
|
Title: "搜索聊天记录",
|
||||||
|
Search: "输入搜索关键词",
|
||||||
|
NoResult: "没有找到结果",
|
||||||
|
NoData: "没有数据",
|
||||||
|
Loading: "加载中",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `搜索到 ${count} 条结果`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "查看",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "面具",
|
Name: "面具",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -467,6 +467,21 @@ const cs: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Jste asistent",
|
Sysmessage: "Jste asistent",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Hledat",
|
||||||
|
Page: {
|
||||||
|
Title: "Hledat v historii chatu",
|
||||||
|
Search: "Zadejte hledané klíčové slovo",
|
||||||
|
NoResult: "Nebyly nalezeny žádné výsledky",
|
||||||
|
NoData: "Žádná data",
|
||||||
|
Loading: "Načítání",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Nalezeno ${count} výsledků`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Zobrazit",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Maska",
|
Name: "Maska",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -482,6 +482,21 @@ const de: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Du bist ein Assistent",
|
Sysmessage: "Du bist ein Assistent",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Suche",
|
||||||
|
Page: {
|
||||||
|
Title: "Chatverlauf durchsuchen",
|
||||||
|
Search: "Suchbegriff eingeben",
|
||||||
|
NoResult: "Keine Ergebnisse gefunden",
|
||||||
|
NoData: "Keine Daten",
|
||||||
|
Loading: "Laden",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count} Ergebnisse gefunden`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Ansehen",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Masken",
|
Name: "Masken",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -209,6 +209,11 @@ const en: LocaleType = {
|
|||||||
Title: "Sync Type",
|
Title: "Sync Type",
|
||||||
SubTitle: "Choose your favorite sync service",
|
SubTitle: "Choose your favorite sync service",
|
||||||
},
|
},
|
||||||
|
EnableAutoSync: {
|
||||||
|
Title: "Auto Sync Settings",
|
||||||
|
SubTitle:
|
||||||
|
"Automatically synchronize data after replying or deleting messages",
|
||||||
|
},
|
||||||
Proxy: {
|
Proxy: {
|
||||||
Title: "Enable CORS Proxy",
|
Title: "Enable CORS Proxy",
|
||||||
SubTitle: "Enable a proxy to avoid cross-origin restrictions",
|
SubTitle: "Enable a proxy to avoid cross-origin restrictions",
|
||||||
@@ -527,6 +532,21 @@ const en: LocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "You are an assistant that",
|
Sysmessage: "You are an assistant that",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Search",
|
||||||
|
Page: {
|
||||||
|
Title: "Search Chat History",
|
||||||
|
Search: "Enter search query to search chat history",
|
||||||
|
NoResult: "No results found",
|
||||||
|
NoData: "No data",
|
||||||
|
Loading: "Loading...",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Found ${count} results`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "View",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Mask",
|
Name: "Mask",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -480,6 +480,21 @@ const es: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Eres un asistente",
|
Sysmessage: "Eres un asistente",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Buscar",
|
||||||
|
Page: {
|
||||||
|
Title: "Buscar en el historial de chat",
|
||||||
|
Search: "Ingrese la palabra clave de búsqueda",
|
||||||
|
NoResult: "No se encontraron resultados",
|
||||||
|
NoData: "Sin datos",
|
||||||
|
Loading: "Cargando",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Se encontraron ${count} resultados`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Ver",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Máscara",
|
Name: "Máscara",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -480,6 +480,21 @@ const fr: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Vous êtes un assistant",
|
Sysmessage: "Vous êtes un assistant",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Recherche",
|
||||||
|
Page: {
|
||||||
|
Title: "Rechercher dans l'historique des discussions",
|
||||||
|
Search: "Entrez le mot-clé de recherche",
|
||||||
|
NoResult: "Aucun résultat trouvé",
|
||||||
|
NoData: "Aucune donnée",
|
||||||
|
Loading: "Chargement",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count} résultats trouvés`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Voir",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Masque",
|
Name: "Masque",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -470,6 +470,21 @@ const id: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Anda adalah seorang asisten",
|
Sysmessage: "Anda adalah seorang asisten",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Cari",
|
||||||
|
Page: {
|
||||||
|
Title: "Cari riwayat obrolan",
|
||||||
|
Search: "Masukkan kata kunci pencarian",
|
||||||
|
NoResult: "Tidak ada hasil ditemukan",
|
||||||
|
NoData: "Tidak ada data",
|
||||||
|
Loading: "Memuat",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Ditemukan ${count} hasil`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Lihat",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Masker",
|
Name: "Masker",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -481,6 +481,21 @@ const it: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Sei un assistente",
|
Sysmessage: "Sei un assistente",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Cerca",
|
||||||
|
Page: {
|
||||||
|
Title: "Cerca nei messaggi",
|
||||||
|
Search: "Inserisci parole chiave per la ricerca",
|
||||||
|
NoResult: "Nessun risultato trovato",
|
||||||
|
NoData: "Nessun dato",
|
||||||
|
Loading: "Caricamento in corso",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Trovati ${count} risultati`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Visualizza",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Maschera",
|
Name: "Maschera",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -460,9 +460,27 @@ const jp: PartialLocaleType = {
|
|||||||
Plugin: {
|
Plugin: {
|
||||||
Name: "プラグイン",
|
Name: "プラグイン",
|
||||||
},
|
},
|
||||||
|
Discovery: {
|
||||||
|
Name: "発見",
|
||||||
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "あなたはアシスタントです",
|
Sysmessage: "あなたはアシスタントです",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "検索",
|
||||||
|
Page: {
|
||||||
|
Title: "チャット履歴を検索",
|
||||||
|
Search: "検索キーワードを入力",
|
||||||
|
NoResult: "結果が見つかりませんでした",
|
||||||
|
NoData: "データがありません",
|
||||||
|
Loading: "読み込み中",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count} 件の結果が見つかりました`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "表示",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "マスク",
|
Name: "マスク",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -458,6 +458,21 @@ const ko: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "당신은 보조자입니다.",
|
Sysmessage: "당신은 보조자입니다.",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "검색",
|
||||||
|
Page: {
|
||||||
|
Title: "채팅 기록 검색",
|
||||||
|
Search: "검색어 입력",
|
||||||
|
NoResult: "결과를 찾을 수 없습니다",
|
||||||
|
NoData: "데이터가 없습니다",
|
||||||
|
Loading: "로딩 중",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count}개의 결과를 찾았습니다`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "보기",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "마스크",
|
Name: "마스크",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -474,6 +474,21 @@ const no: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Du er en assistent",
|
Sysmessage: "Du er en assistent",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Søk",
|
||||||
|
Page: {
|
||||||
|
Title: "Søk i chatthistorikk",
|
||||||
|
Search: "Skriv inn søkeord",
|
||||||
|
NoResult: "Ingen resultater funnet",
|
||||||
|
NoData: "Ingen data",
|
||||||
|
Loading: "Laster inn",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Fant ${count} resultater`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Vis",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Maske",
|
Name: "Maske",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -405,6 +405,21 @@ const pt: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Você é um assistente que",
|
Sysmessage: "Você é um assistente que",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Pesquisar",
|
||||||
|
Page: {
|
||||||
|
Title: "Pesquisar histórico de chat",
|
||||||
|
Search: "Digite palavras-chave para pesquisa",
|
||||||
|
NoResult: "Nenhum resultado encontrado",
|
||||||
|
NoData: "Sem dados",
|
||||||
|
Loading: "Carregando",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Encontrado ${count} resultados`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Ver",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Máscara",
|
Name: "Máscara",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -471,6 +471,21 @@ const ru: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Вы - помощник",
|
Sysmessage: "Вы - помощник",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Поиск",
|
||||||
|
Page: {
|
||||||
|
Title: "Поиск в истории чатов",
|
||||||
|
Search: "Введите ключевые слова для поиска",
|
||||||
|
NoResult: "Результатов не найдено",
|
||||||
|
NoData: "Нет данных",
|
||||||
|
Loading: "Загрузка",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Найдено ${count} результатов`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Просмотр",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Маска",
|
Name: "Маска",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -423,6 +423,21 @@ const sk: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Ste asistent, ktorý",
|
Sysmessage: "Ste asistent, ktorý",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Hľadať",
|
||||||
|
Page: {
|
||||||
|
Title: "Hľadať v histórii chatu",
|
||||||
|
Search: "Zadajte kľúčové slová na vyhľadávanie",
|
||||||
|
NoResult: "Nenašli sa žiadne výsledky",
|
||||||
|
NoData: "Žiadne údaje",
|
||||||
|
Loading: "Načítava sa",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Nájdených ${count} výsledkov`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Zobraziť",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Maska",
|
Name: "Maska",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -470,6 +470,21 @@ const tr: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Sen bir asistansın",
|
Sysmessage: "Sen bir asistansın",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Ara",
|
||||||
|
Page: {
|
||||||
|
Title: "Sohbet geçmişini ara",
|
||||||
|
Search: "Arama anahtar kelimelerini girin",
|
||||||
|
NoResult: "Sonuç bulunamadı",
|
||||||
|
NoData: "Veri yok",
|
||||||
|
Loading: "Yükleniyor",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count} sonuç bulundu`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Görüntüle",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Maske",
|
Name: "Maske",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -452,6 +452,21 @@ const tw = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "搜索",
|
||||||
|
Page: {
|
||||||
|
Title: "搜索聊天記錄",
|
||||||
|
Search: "輸入搜索關鍵詞",
|
||||||
|
NoResult: "沒有找到結果",
|
||||||
|
NoData: "沒有數據",
|
||||||
|
Loading: "加載中",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `找到 ${count} 條結果`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "查看",
|
||||||
|
},
|
||||||
|
},
|
||||||
NewChat: {
|
NewChat: {
|
||||||
Return: "返回",
|
Return: "返回",
|
||||||
Skip: "跳過",
|
Skip: "跳過",
|
||||||
|
|||||||
@@ -466,6 +466,21 @@ const vi: PartialLocaleType = {
|
|||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Bạn là một trợ lý",
|
Sysmessage: "Bạn là một trợ lý",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Tìm kiếm",
|
||||||
|
Page: {
|
||||||
|
Title: "Tìm kiếm lịch sử trò chuyện",
|
||||||
|
Search: "Nhập từ khóa tìm kiếm",
|
||||||
|
NoResult: "Không tìm thấy kết quả",
|
||||||
|
NoData: "Không có dữ liệu",
|
||||||
|
Loading: "Đang tải",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Tìm thấy ${count} kết quả`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Xem",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Mặt nạ",
|
Name: "Mặt nạ",
|
||||||
Page: {
|
Page: {
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export const useAccessStore = createPersistStore(
|
|||||||
this.isValidBaidu() ||
|
this.isValidBaidu() ||
|
||||||
this.isValidByteDance() ||
|
this.isValidByteDance() ||
|
||||||
this.isValidAlibaba() ||
|
this.isValidAlibaba() ||
|
||||||
this.isValidTencent ||
|
this.isValidTencent() ||
|
||||||
this.isValidMoonshot() ||
|
this.isValidMoonshot() ||
|
||||||
this.isValidIflytek() ||
|
this.isValidIflytek() ||
|
||||||
!this.enabledAccessControl() ||
|
!this.enabledAccessControl() ||
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { trimTopic, getMessageTextContent } from "../utils";
|
import {
|
||||||
|
trimTopic,
|
||||||
|
getMessageTextContent,
|
||||||
|
removeOutdatedEntries,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
import Locale, { getLang } from "../locales";
|
import Locale, { getLang } from "../locales";
|
||||||
import { showToast } from "../components/ui-lib";
|
import { showToast } from "../components/ui-lib";
|
||||||
@@ -26,6 +30,7 @@ import { nanoid } from "nanoid";
|
|||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||||
import { useAccessStore } from "./access";
|
import { useAccessStore } from "./access";
|
||||||
|
import { useSyncStore } from "./sync";
|
||||||
import { isDalle3 } from "../utils";
|
import { isDalle3 } from "../utils";
|
||||||
|
|
||||||
export type ChatMessage = RequestMessage & {
|
export type ChatMessage = RequestMessage & {
|
||||||
@@ -62,6 +67,7 @@ export interface ChatSession {
|
|||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
lastSummarizeIndex: number;
|
lastSummarizeIndex: number;
|
||||||
clearContextIndex?: number;
|
clearContextIndex?: number;
|
||||||
|
deletedMessageIds?: Record<string, number>;
|
||||||
|
|
||||||
mask: Mask;
|
mask: Mask;
|
||||||
}
|
}
|
||||||
@@ -85,6 +91,7 @@ function createEmptySession(): ChatSession {
|
|||||||
},
|
},
|
||||||
lastUpdate: Date.now(),
|
lastUpdate: Date.now(),
|
||||||
lastSummarizeIndex: 0,
|
lastSummarizeIndex: 0,
|
||||||
|
deletedMessageIds: {},
|
||||||
|
|
||||||
mask: createEmptyMask(),
|
mask: createEmptyMask(),
|
||||||
};
|
};
|
||||||
@@ -162,9 +169,19 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
|||||||
return output;
|
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 = {
|
const DEFAULT_CHAT_STATE = {
|
||||||
sessions: [createEmptySession()],
|
sessions: [createEmptySession()],
|
||||||
currentSessionIndex: 0,
|
currentSessionIndex: 0,
|
||||||
|
deletedSessionIds: {} as Record<string, number>,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useChatStore = createPersistStore(
|
export const useChatStore = createPersistStore(
|
||||||
@@ -253,7 +270,18 @@ export const useChatStore = createPersistStore(
|
|||||||
if (!deletedSession) return;
|
if (!deletedSession) return;
|
||||||
|
|
||||||
const sessions = get().sessions.slice();
|
const sessions = get().sessions.slice();
|
||||||
sessions.splice(index, 1);
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const currentIndex = get().currentSessionIndex;
|
const currentIndex = get().currentSessionIndex;
|
||||||
let nextIndex = Math.min(
|
let nextIndex = Math.min(
|
||||||
@@ -270,19 +298,24 @@ export const useChatStore = createPersistStore(
|
|||||||
const restoreState = {
|
const restoreState = {
|
||||||
currentSessionIndex: get().currentSessionIndex,
|
currentSessionIndex: get().currentSessionIndex,
|
||||||
sessions: get().sessions.slice(),
|
sessions: get().sessions.slice(),
|
||||||
|
deletedSessionIds: get().deletedSessionIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
set(() => ({
|
set(() => ({
|
||||||
currentSessionIndex: nextIndex,
|
currentSessionIndex: nextIndex,
|
||||||
sessions,
|
sessions,
|
||||||
|
deletedSessionIds,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
noticeCloudSync();
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
Locale.Home.DeleteToast,
|
Locale.Home.DeleteToast,
|
||||||
{
|
{
|
||||||
text: Locale.Home.Revert,
|
text: Locale.Home.Revert,
|
||||||
onClick() {
|
onClick() {
|
||||||
set(() => restoreState);
|
set(() => restoreState);
|
||||||
|
noticeCloudSync();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
@@ -303,6 +336,24 @@ export const useChatStore = createPersistStore(
|
|||||||
return session;
|
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) {
|
onNewMessage(message: ChatMessage) {
|
||||||
get().updateCurrentSession((session) => {
|
get().updateCurrentSession((session) => {
|
||||||
session.messages = session.messages.concat();
|
session.messages = session.messages.concat();
|
||||||
@@ -310,6 +361,8 @@ export const useChatStore = createPersistStore(
|
|||||||
});
|
});
|
||||||
get().updateStat(message);
|
get().updateStat(message);
|
||||||
get().summarizeSession();
|
get().summarizeSession();
|
||||||
|
get().sortSessions();
|
||||||
|
noticeCloudSync();
|
||||||
},
|
},
|
||||||
|
|
||||||
async onUserInput(content: string, attachImages?: string[]) {
|
async onUserInput(content: string, attachImages?: string[]) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
|
|||||||
|
|
||||||
const DEFAULT_SYNC_STATE = {
|
const DEFAULT_SYNC_STATE = {
|
||||||
provider: ProviderType.WebDAV,
|
provider: ProviderType.WebDAV,
|
||||||
|
enableAutoSync: true,
|
||||||
useProxy: true,
|
useProxy: true,
|
||||||
proxyUrl: corsPath(ApiPath.Cors),
|
proxyUrl: corsPath(ApiPath.Cors),
|
||||||
|
|
||||||
@@ -45,6 +46,8 @@ const DEFAULT_SYNC_STATE = {
|
|||||||
lastProvider: "",
|
lastProvider: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let lastSyncTime = 0;
|
||||||
|
|
||||||
export const useSyncStore = createPersistStore(
|
export const useSyncStore = createPersistStore(
|
||||||
DEFAULT_SYNC_STATE,
|
DEFAULT_SYNC_STATE,
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
@@ -91,6 +94,16 @@ export const useSyncStore = createPersistStore(
|
|||||||
},
|
},
|
||||||
|
|
||||||
async sync() {
|
async sync() {
|
||||||
|
if (lastSyncTime && lastSyncTime >= Date.now() - 800) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastSyncTime = Date.now();
|
||||||
|
|
||||||
|
const enableAutoSync = get().enableAutoSync;
|
||||||
|
if (!enableAutoSync) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const localState = getLocalAppState();
|
const localState = getLocalAppState();
|
||||||
const provider = get().provider;
|
const provider = get().provider;
|
||||||
const config = get()[provider];
|
const config = get()[provider];
|
||||||
@@ -100,15 +113,15 @@ export const useSyncStore = createPersistStore(
|
|||||||
const remoteState = await client.get(config.username);
|
const remoteState = await client.get(config.username);
|
||||||
if (!remoteState || remoteState === "") {
|
if (!remoteState || remoteState === "") {
|
||||||
await client.set(config.username, JSON.stringify(localState));
|
await client.set(config.username, JSON.stringify(localState));
|
||||||
console.log("[Sync] Remote state is empty, using local state instead.");
|
console.log(
|
||||||
return
|
"[Sync] Remote state is empty, using local state instead.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
const parsedRemoteState = JSON.parse(
|
const parsedRemoteState = JSON.parse(remoteState) as AppState;
|
||||||
await client.get(config.username),
|
|
||||||
) as AppState;
|
|
||||||
mergeAppState(localState, parsedRemoteState);
|
mergeAppState(localState, parsedRemoteState);
|
||||||
setLocalAppState(localState);
|
setLocalAppState(localState);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("[Sync] failed to get remote state", e);
|
console.log("[Sync] failed to get remote state", e);
|
||||||
throw e;
|
throw e;
|
||||||
@@ -123,6 +136,14 @@ export const useSyncStore = createPersistStore(
|
|||||||
const client = this.getClient();
|
const client = this.getClient();
|
||||||
return await client.check();
|
return await client.check();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async autoSync() {
|
||||||
|
const { lastSyncTime, provider } = get();
|
||||||
|
const syncStore = useSyncStore.getState();
|
||||||
|
if (lastSyncTime && syncStore.cloudSync()) {
|
||||||
|
syncStore.sync();
|
||||||
|
}
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: StoreKey.Sync,
|
name: StoreKey.Sync,
|
||||||
|
|||||||
13
app/utils.ts
13
app/utils.ts
@@ -270,3 +270,16 @@ export function isVisionModel(model: string) {
|
|||||||
export function isDalle3(model: string) {
|
export function isDalle3(model: string) {
|
||||||
return "dall-e-3" === model;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useMaskStore } from "../store/mask";
|
|||||||
import { usePromptStore } from "../store/prompt";
|
import { usePromptStore } from "../store/prompt";
|
||||||
import { StoreKey } from "../constant";
|
import { StoreKey } from "../constant";
|
||||||
import { merge } from "./merge";
|
import { merge } from "./merge";
|
||||||
|
import { removeOutdatedEntries } from "@/app/utils";
|
||||||
|
|
||||||
type NonFunctionKeys<T> = {
|
type NonFunctionKeys<T> = {
|
||||||
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
|
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
|
||||||
@@ -65,7 +66,10 @@ type StateMerger = {
|
|||||||
const MergeStates: StateMerger = {
|
const MergeStates: StateMerger = {
|
||||||
[StoreKey.Chat]: (localState, remoteState) => {
|
[StoreKey.Chat]: (localState, remoteState) => {
|
||||||
// merge sessions
|
// merge sessions
|
||||||
|
const currentSession = useChatStore.getState().currentSession();
|
||||||
|
|
||||||
const localSessions: Record<string, ChatSession> = {};
|
const localSessions: Record<string, ChatSession> = {};
|
||||||
|
const localDeletedSessionIds = localState.deletedSessionIds || {};
|
||||||
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
||||||
|
|
||||||
remoteState.sessions.forEach((remoteSession) => {
|
remoteState.sessions.forEach((remoteSession) => {
|
||||||
@@ -75,29 +79,81 @@ const MergeStates: StateMerger = {
|
|||||||
const localSession = localSessions[remoteSession.id];
|
const localSession = localSessions[remoteSession.id];
|
||||||
if (!localSession) {
|
if (!localSession) {
|
||||||
// if remote session is new, just merge it
|
// if remote session is new, just merge it
|
||||||
localState.sessions.push(remoteSession);
|
if (
|
||||||
|
(localDeletedSessionIds[remoteSession.id] || -1) <
|
||||||
|
remoteSession.lastUpdate
|
||||||
|
) {
|
||||||
|
localState.sessions.push(remoteSession);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// if both have the same session id, merge the messages
|
// if both have the same session id, merge the messages
|
||||||
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
|
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
|
||||||
|
const localDeletedMessageIds = localSession.deletedMessageIds || {};
|
||||||
remoteSession.messages.forEach((m) => {
|
remoteSession.messages.forEach((m) => {
|
||||||
if (!localMessageIds.has(m.id)) {
|
if (!localMessageIds.has(m.id)) {
|
||||||
localSession.messages.push(m);
|
if (
|
||||||
|
!localDeletedMessageIds[m.id] ||
|
||||||
|
new Date(localDeletedMessageIds[m.id]).toLocaleString() < m.date
|
||||||
|
) {
|
||||||
|
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
|
// sort local messages with date field in asc order
|
||||||
localSession.messages.sort(
|
localSession.messages.sort(
|
||||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
(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 || {};
|
||||||
|
localState.sessions = localState.sessions.filter((localSession) => {
|
||||||
|
return (
|
||||||
|
(remoteDeletedSessionIds[localSession.id] || -1) <=
|
||||||
|
localSession.lastUpdate
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// sort local sessions with date field in desc order
|
// sort local sessions with date field in desc order
|
||||||
localState.sessions.sort(
|
localState.sessions.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
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;
|
return localState;
|
||||||
},
|
},
|
||||||
[StoreKey.Prompt]: (localState, remoteState) => {
|
[StoreKey.Prompt]: (localState, remoteState) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user