Compare commits

...

22 Commits

Author SHA1 Message Date
李超
eae593d660 feat: Add automatic data synchronization settings and implementation, enabling auto-sync after completing replies or deleting conversations 2024-08-15 22:42:23 +08:00
织梦人
621b1480c2 Merge branch 'ChatGPTNextWeb:main' into main 2024-08-15 22:41:31 +08:00
Dogtiti
87325fad74 Merge pull request #5270 from DDMeaqua/main
feat: add model name to conversation page
2024-08-15 20:12:33 +08:00
Lloyd Zhou
122aa94c9f Merge pull request #5273 from ElricLiu/main
Update README.md
2024-08-15 14:35:48 +08:00
ElricLiu
e2e8a45104 Update README.md 2024-08-14 20:38:52 +08:00
GH Action - Upstream Sync
93bfb55822 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-08-14 01:01:30 +00:00
DDMeaqua
fb5fc13f72 feat: add model name 2024-08-13 19:58:44 +08:00
Dogtiti
edb92f7bfb Merge pull request #5245 from MrrDrr/dalle3_add_options
dall-e-3 adds 'quality' and 'style' options
2024-08-13 15:31:18 +08:00
l.tingting
ca865a80dc dall-e-3 adds 'quality' and 'style' options 2024-08-10 11:09:07 +08:00
李超
648e60028d feat: The cloud synchronization feature is enhanced to support the synchronization of deleted conversations and deleted messages 2024-08-08 22:02:15 +08:00
Dogtiti
cf1c8e8f2a Merge pull request #5233 from ConnectAI-E/feature/bangpt
Feature/bangpt
2024-08-08 17:27:50 +08:00
lyf
d948be2372 Merge branch 'feature/bangpt' of ssh://github.com/ConnectAI-E/ChatGPT-Next-Web into feature/bangpt 2024-08-08 16:25:49 +08:00
lyf
cc28aef625 add gpt-4o-2024-08-06 2024-08-08 16:23:40 +08:00
Dogtiti
036358de7c Merge pull request #5219 from ConnectAI-E/feature/access
修复ChatGPTNextWeb页面的无障碍问题
2024-08-08 13:59:16 +08:00
lyf
0958b9ee12 tsxiugai 2024-08-08 13:08:56 +08:00
Dogtiti
aff1d7ecd6 Merge pull request #5223 from 122cygf/main
修复拼写错误
2024-08-07 21:29:43 +08:00
lzz
42fdbd9bb8 Fix spelling errors 2024-08-07 17:22:32 +08:00
lyf
034c82e514 修改设置的无障碍 2024-08-07 13:39:23 +08:00
lyf
14ff46b5cd 解决按钮无障碍 2024-08-07 13:01:08 +08:00
lyf
c9099ca0a5 无障碍按钮和链接 2024-08-07 10:55:02 +08:00
Dogtiti
58b144b345 Merge pull request #5218 from ConnectAI-E/feature/bangpt
解决禁止gpt4时禁止gtp4o-mini问题
2024-08-07 10:45:38 +08:00
lyf
af21c57e77 ban gpt4 2024-08-07 10:15:39 +08:00
27 changed files with 375 additions and 27 deletions

View File

@@ -91,7 +91,8 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [x] artifacts
- [ ] network search, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [ ] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [ ] local knowledge base
## What's New
@@ -128,6 +129,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [x] 插件机制,支持 artifacts联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [x] artifacts
- [ ] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [ ] 本地知识库
## 最新动态

View File

@@ -13,7 +13,7 @@ function getModels(remoteModelRes: OpenAIListModelResponse) {
if (config.disableGPT4) {
remoteModelRes.data = remoteModelRes.data.filter(
(m) => !m.id.startsWith("gpt-4"),
(m) => !m.id.startsWith("gpt-4") || m.id.startsWith("gpt-4o-mini"),
);
}

View File

@@ -44,6 +44,8 @@ export interface LLMConfig {
presence_penalty?: number;
frequency_penalty?: number;
size?: DalleRequestPayload["size"];
quality?: DalleRequestPayload["quality"];
style?: DalleRequestPayload["style"];
}
export interface ChatOptions {

View File

@@ -17,7 +17,7 @@ import {
base64Image2Blob,
} from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { DalleSize } from "@/app/typing";
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
import {
ChatOptions,
@@ -70,6 +70,8 @@ export interface DalleRequestPayload {
response_format: "url" | "b64_json";
n: number;
size: DalleSize;
quality: DalleQuality;
style: DalleStyle;
}
export class ChatGPTApi implements LLMApi {
@@ -162,6 +164,8 @@ export class ChatGPTApi implements LLMApi {
response_format: "b64_json", // using b64_json, and save image in CacheStorage
n: 1,
size: options.config?.size ?? "1024x1024",
quality: options.config?.quality ?? "standard",
style: options.config?.style ?? "vivid",
};
} else {
const visionModel = isVisionModel(options.config.model);

View File

@@ -18,6 +18,7 @@ export function IconButton(props: {
tabIndex?: number;
autoFocus?: boolean;
style?: CSSProperties;
aria?: string;
}) {
return (
<button
@@ -34,9 +35,11 @@ export function IconButton(props: {
tabIndex={props.tabIndex}
autoFocus={props.autoFocus}
style={props.style}
aria-label={props.aria}
>
{props.icon && (
<div
aria-label={props.text || props.title}
className={
styles["icon-button-icon"] +
` ${props.type === "primary" && "no-dark"}`
@@ -47,7 +50,12 @@ export function IconButton(props: {
)}
{props.text && (
<div className={styles["icon-button-text"]}>{props.text}</div>
<div
aria-label={props.text || props.title}
className={styles["icon-button-text"]}
>
{props.text}
</div>
)}
</button>
);

View File

@@ -346,6 +346,12 @@
flex-wrap: nowrap;
}
}
.chat-model-name {
font-size: 12px;
color: var(--black);
margin-left: 6px;
}
}
.chat-message-container {

View File

@@ -38,6 +38,8 @@ import BottomIcon from "../icons/bottom.svg";
import StopIcon from "../icons/pause.svg";
import RobotIcon from "../icons/robot.svg";
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 {
@@ -62,6 +64,7 @@ import {
getMessageImages,
isVisionModel,
isDalle3,
removeOutdatedEntries,
} from "../utils";
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
@@ -69,7 +72,7 @@ import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
import dynamic from "next/dynamic";
import { ChatControllerPool } from "../client/controller";
import { DalleSize } from "../typing";
import { DalleSize, DalleQuality, DalleStyle } from "../typing";
import { Prompt, usePromptStore } from "../store/prompt";
import Locale from "../locales";
@@ -485,9 +488,17 @@ export function ChatActions(props: {
const [showUploadImage, setShowUploadImage] = useState(false);
const [showSizeSelector, setShowSizeSelector] = useState(false);
const [showQualitySelector, setShowQualitySelector] = useState(false);
const [showStyleSelector, setShowStyleSelector] = useState(false);
const dalle3Sizes: DalleSize[] = ["1024x1024", "1792x1024", "1024x1792"];
const dalle3Qualitys: DalleQuality[] = ["standard", "hd"];
const dalle3Styles: DalleStyle[] = ["vivid", "natural"];
const currentSize =
chatStore.currentSession().mask.modelConfig?.size ?? "1024x1024";
const currentQuality =
chatStore.currentSession().mask.modelConfig?.quality ?? "standard";
const currentStyle =
chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
useEffect(() => {
const show = isVisionModel(currentModel);
@@ -659,6 +670,60 @@ export function ChatActions(props: {
/>
)}
{isDalle3(currentModel) && (
<ChatAction
onClick={() => setShowQualitySelector(true)}
text={currentQuality}
icon={<QualityIcon />}
/>
)}
{showQualitySelector && (
<Selector
defaultSelectedValue={currentQuality}
items={dalle3Qualitys.map((m) => ({
title: m,
value: m,
}))}
onClose={() => setShowQualitySelector(false)}
onSelection={(q) => {
if (q.length === 0) return;
const quality = q[0];
chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.quality = quality;
});
showToast(quality);
}}
/>
)}
{isDalle3(currentModel) && (
<ChatAction
onClick={() => setShowStyleSelector(true)}
text={currentStyle}
icon={<StyleIcon />}
/>
)}
{showStyleSelector && (
<Selector
defaultSelectedValue={currentStyle}
items={dalle3Styles.map((m) => ({
title: m,
value: m,
}))}
onClose={() => setShowStyleSelector(false)}
onSelection={(s) => {
if (s.length === 0) return;
const style = s[0];
chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.style = style;
});
showToast(style);
}}
/>
)}
<ChatAction
onClick={() => setShowPluginSelector(true)}
text={Locale.Plugin.Name}
@@ -959,10 +1024,20 @@ function _Chat() {
};
const deleteMessage = (msgId?: string) => {
chatStore.updateCurrentSession(
(session) =>
(session.messages = session.messages.filter((m) => m.id !== msgId)),
);
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;
});
});
};
const onDelete = (msgId: string) => {
@@ -1337,6 +1412,8 @@ function _Chat() {
<IconButton
icon={<RenameIcon />}
bordered
title={Locale.Chat.EditMessage.Title}
aria={Locale.Chat.EditMessage.Title}
onClick={() => setIsEditingMessage(true)}
/>
</div>
@@ -1356,6 +1433,8 @@ function _Chat() {
<IconButton
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
bordered
title={Locale.Chat.Actions.FullScreen}
aria={Locale.Chat.Actions.FullScreen}
onClick={() => {
config.update(
(config) => (config.tightBorder = !config.tightBorder),
@@ -1407,6 +1486,7 @@ function _Chat() {
<div className={styles["chat-message-edit"]}>
<IconButton
icon={<EditIcon />}
aria={Locale.Chat.Actions.Edit}
onClick={async () => {
const newMessage = await showPrompt(
Locale.Chat.Actions.Edit,
@@ -1455,6 +1535,11 @@ function _Chat() {
</>
)}
</div>
{!isUser && (
<div className={styles["chat-model-name"]}>
{message.model}
</div>
)}
{showActions && (
<div className={styles["chat-message-actions"]}>

View File

@@ -9,6 +9,7 @@ interface InputRangeProps {
min: string;
max: string;
step: string;
aria: string;
}
export function InputRange({
@@ -19,11 +20,13 @@ export function InputRange({
min,
max,
step,
aria,
}: InputRangeProps) {
return (
<div className={styles["input-range"] + ` ${className ?? ""}`}>
{title || value}
<input
aria-label={aria}
type="range"
title={title}
value={value}

View File

@@ -127,6 +127,8 @@ export function MaskConfig(props: {
onClose={() => setShowPicker(false)}
>
<div
tabIndex={0}
aria-label={Locale.Mask.Config.Avatar}
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
>
@@ -139,6 +141,7 @@ export function MaskConfig(props: {
</ListItem>
<ListItem title={Locale.Mask.Config.Name}>
<input
aria-label={Locale.Mask.Config.Name}
type="text"
value={props.mask.name}
onInput={(e) =>
@@ -153,6 +156,7 @@ export function MaskConfig(props: {
subTitle={Locale.Mask.Config.HideContext.SubTitle}
>
<input
aria-label={Locale.Mask.Config.HideContext.Title}
type="checkbox"
checked={props.mask.hideContext}
onChange={(e) => {
@@ -169,6 +173,7 @@ export function MaskConfig(props: {
subTitle={Locale.Mask.Config.Share.SubTitle}
>
<IconButton
aria={Locale.Mask.Config.Share.Title}
icon={<CopyIcon />}
text={Locale.Mask.Config.Share.Action}
onClick={copyMaskLink}
@@ -182,6 +187,7 @@ export function MaskConfig(props: {
subTitle={Locale.Mask.Config.Sync.SubTitle}
>
<input
aria-label={Locale.Mask.Config.Sync.Title}
type="checkbox"
checked={props.mask.syncGlobalConfig}
onChange={async (e) => {

View File

@@ -17,6 +17,7 @@ export function ModelConfigList(props: {
<>
<ListItem title={Locale.Settings.Model}>
<Select
aria-label={Locale.Settings.Model}
value={value}
onChange={(e) => {
const [model, providerName] = e.currentTarget.value.split("@");
@@ -40,6 +41,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.Temperature.SubTitle}
>
<InputRange
aria={Locale.Settings.Temperature.Title}
value={props.modelConfig.temperature?.toFixed(1)}
min="0"
max="1" // lets limit it to 0-1
@@ -59,6 +61,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.TopP.SubTitle}
>
<InputRange
aria={Locale.Settings.TopP.Title}
value={(props.modelConfig.top_p ?? 1).toFixed(1)}
min="0"
max="1"
@@ -78,6 +81,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.MaxTokens.SubTitle}
>
<input
aria-label={Locale.Settings.MaxTokens.Title}
type="number"
min={1024}
max={512000}
@@ -100,6 +104,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.PresencePenalty.SubTitle}
>
<InputRange
aria={Locale.Settings.PresencePenalty.Title}
value={props.modelConfig.presence_penalty?.toFixed(1)}
min="-2"
max="2"
@@ -121,6 +126,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
>
<InputRange
aria={Locale.Settings.FrequencyPenalty.Title}
value={props.modelConfig.frequency_penalty?.toFixed(1)}
min="-2"
max="2"
@@ -142,6 +148,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
>
<input
aria-label={Locale.Settings.InjectSystemPrompts.Title}
type="checkbox"
checked={props.modelConfig.enableInjectSystemPrompts}
onChange={(e) =>
@@ -159,6 +166,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.InputTemplate.SubTitle}
>
<input
aria-label={Locale.Settings.InputTemplate.Title}
type="text"
value={props.modelConfig.template}
onChange={(e) =>
@@ -175,6 +183,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.HistoryCount.SubTitle}
>
<InputRange
aria={Locale.Settings.HistoryCount.Title}
title={props.modelConfig.historyMessageCount.toString()}
value={props.modelConfig.historyMessageCount}
min="0"
@@ -193,6 +202,7 @@ export function ModelConfigList(props: {
subTitle={Locale.Settings.CompressThreshold.SubTitle}
>
<input
aria-label={Locale.Settings.CompressThreshold.Title}
type="number"
min={500}
max={4000}
@@ -208,6 +218,7 @@ export function ModelConfigList(props: {
</ListItem>
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
<input
aria-label={Locale.Memory.Title}
type="checkbox"
checked={props.modelConfig.sendMemory}
onChange={(e) =>

View File

@@ -192,6 +192,7 @@ export function ControlParam(props: {
required={item.required}
>
<Select
aria-label={item.name}
value={props.data[item.value]}
onChange={(e) => {
props.onChange(item.value, e.currentTarget.value);
@@ -216,6 +217,7 @@ export function ControlParam(props: {
required={item.required}
>
<input
aria-label={item.name}
type="number"
min={item.min}
max={item.max}
@@ -235,6 +237,7 @@ export function ControlParam(props: {
required={item.required}
>
<input
aria-label={item.name}
type="text"
value={props.data[item.value]}
style={{ maxWidth: "100%", width: "100%" }}

View File

@@ -133,6 +133,7 @@ export function Sd() {
{showMaxIcon && (
<div className="window-action-button">
<IconButton
aria={Locale.Chat.Actions.FullScreen}
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
bordered
onClick={() => {

View File

@@ -246,6 +246,7 @@ function DangerItems() {
subTitle={Locale.Settings.Danger.Reset.SubTitle}
>
<IconButton
aria={Locale.Settings.Danger.Reset.Title}
text={Locale.Settings.Danger.Reset.Action}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
@@ -260,6 +261,7 @@ function DangerItems() {
subTitle={Locale.Settings.Danger.Clear.SubTitle}
>
<IconButton
aria={Locale.Settings.Danger.Clear.Title}
text={Locale.Settings.Danger.Clear.Action}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
@@ -355,6 +357,21 @@ 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}
@@ -513,6 +530,7 @@ function SyncItems() {
>
<div style={{ display: "flex" }}>
<IconButton
aria={Locale.Settings.Sync.CloudState + Locale.UI.Config}
icon={<ConfigIcon />}
text={Locale.UI.Config}
onClick={() => {
@@ -543,6 +561,7 @@ function SyncItems() {
>
<div style={{ display: "flex" }}>
<IconButton
aria={Locale.Settings.Sync.LocalState + Locale.UI.Export}
icon={<UploadIcon />}
text={Locale.UI.Export}
onClick={() => {
@@ -550,6 +569,7 @@ function SyncItems() {
}}
/>
<IconButton
aria={Locale.Settings.Sync.LocalState + Locale.UI.Import}
icon={<DownloadIcon />}
text={Locale.UI.Import}
onClick={() => {
@@ -687,6 +707,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
>
<input
aria-label={Locale.Settings.Access.CustomEndpoint.Title}
type="checkbox"
checked={accessStore.useCustomConfig}
onChange={(e) =>
@@ -706,6 +727,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.OpenAI.Endpoint.SubTitle}
>
<input
aria-label={Locale.Settings.Access.OpenAI.Endpoint.Title}
type="text"
value={accessStore.openaiUrl}
placeholder={OPENAI_BASE_URL}
@@ -721,6 +743,8 @@ export function Settings() {
subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
>
<PasswordInput
aria={Locale.Settings.ShowPassword}
aria-label={Locale.Settings.Access.OpenAI.ApiKey.Title}
value={accessStore.openaiApiKey}
type="text"
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
@@ -744,6 +768,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Azure.Endpoint.Title}
type="text"
value={accessStore.azureUrl}
placeholder={Azure.ExampleEndpoint}
@@ -759,6 +784,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Azure.ApiKey.Title}
value={accessStore.azureApiKey}
type="text"
placeholder={Locale.Settings.Access.Azure.ApiKey.Placeholder}
@@ -774,6 +800,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Azure.ApiVerion.SubTitle}
>
<input
aria-label={Locale.Settings.Access.Azure.ApiVerion.Title}
type="text"
value={accessStore.azureApiVersion}
placeholder="2023-08-01-preview"
@@ -798,6 +825,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Google.Endpoint.Title}
type="text"
value={accessStore.googleUrl}
placeholder={Google.ExampleEndpoint}
@@ -813,6 +841,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Google.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Google.ApiKey.Title}
value={accessStore.googleApiKey}
type="text"
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
@@ -828,6 +857,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Google.ApiVersion.SubTitle}
>
<input
aria-label={Locale.Settings.Access.Google.ApiVersion.Title}
type="text"
value={accessStore.googleApiVersion}
placeholder="2023-08-01-preview"
@@ -843,6 +873,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Google.GoogleSafetySettings.SubTitle}
>
<Select
aria-label={Locale.Settings.Access.Google.GoogleSafetySettings.Title}
value={accessStore.googleSafetySettings}
onChange={(e) => {
accessStore.update(
@@ -873,6 +904,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Anthropic.Endpoint.Title}
type="text"
value={accessStore.anthropicUrl}
placeholder={Anthropic.ExampleEndpoint}
@@ -888,6 +920,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Anthropic.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Anthropic.ApiKey.Title}
value={accessStore.anthropicApiKey}
type="text"
placeholder={Locale.Settings.Access.Anthropic.ApiKey.Placeholder}
@@ -903,6 +936,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Anthropic.ApiVerion.SubTitle}
>
<input
aria-label={Locale.Settings.Access.Anthropic.ApiVerion.Title}
type="text"
value={accessStore.anthropicApiVersion}
placeholder={Anthropic.Vision}
@@ -924,6 +958,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Baidu.Endpoint.SubTitle}
>
<input
aria-label={Locale.Settings.Access.Baidu.Endpoint.Title}
type="text"
value={accessStore.baiduUrl}
placeholder={Baidu.ExampleEndpoint}
@@ -939,6 +974,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Baidu.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Baidu.ApiKey.Title}
value={accessStore.baiduApiKey}
type="text"
placeholder={Locale.Settings.Access.Baidu.ApiKey.Placeholder}
@@ -954,6 +990,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Baidu.SecretKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Baidu.SecretKey.Title}
value={accessStore.baiduSecretKey}
type="text"
placeholder={Locale.Settings.Access.Baidu.SecretKey.Placeholder}
@@ -975,6 +1012,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Tencent.Endpoint.SubTitle}
>
<input
aria-label={Locale.Settings.Access.Tencent.Endpoint.Title}
type="text"
value={accessStore.tencentUrl}
placeholder={Tencent.ExampleEndpoint}
@@ -990,6 +1028,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Tencent.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Tencent.ApiKey.Title}
value={accessStore.tencentSecretId}
type="text"
placeholder={Locale.Settings.Access.Tencent.ApiKey.Placeholder}
@@ -1005,6 +1044,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Tencent.SecretKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Tencent.SecretKey.Title}
value={accessStore.tencentSecretKey}
type="text"
placeholder={Locale.Settings.Access.Tencent.SecretKey.Placeholder}
@@ -1029,6 +1069,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.ByteDance.Endpoint.Title}
type="text"
value={accessStore.bytedanceUrl}
placeholder={ByteDance.ExampleEndpoint}
@@ -1044,6 +1085,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.ByteDance.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.ByteDance.ApiKey.Title}
value={accessStore.bytedanceApiKey}
type="text"
placeholder={Locale.Settings.Access.ByteDance.ApiKey.Placeholder}
@@ -1068,6 +1110,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Alibaba.Endpoint.Title}
type="text"
value={accessStore.alibabaUrl}
placeholder={Alibaba.ExampleEndpoint}
@@ -1083,6 +1126,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Alibaba.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Alibaba.ApiKey.Title}
value={accessStore.alibabaApiKey}
type="text"
placeholder={Locale.Settings.Access.Alibaba.ApiKey.Placeholder}
@@ -1107,6 +1151,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Moonshot.Endpoint.Title}
type="text"
value={accessStore.moonshotUrl}
placeholder={Moonshot.ExampleEndpoint}
@@ -1122,6 +1167,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Moonshot.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Moonshot.ApiKey.Title}
value={accessStore.moonshotApiKey}
type="text"
placeholder={Locale.Settings.Access.Moonshot.ApiKey.Placeholder}
@@ -1146,6 +1192,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Stability.Endpoint.Title}
type="text"
value={accessStore.stabilityUrl}
placeholder={Stability.ExampleEndpoint}
@@ -1161,6 +1208,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Stability.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Stability.ApiKey.Title}
value={accessStore.stabilityApiKey}
type="text"
placeholder={Locale.Settings.Access.Stability.ApiKey.Placeholder}
@@ -1184,6 +1232,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Iflytek.Endpoint.Title}
type="text"
value={accessStore.iflytekUrl}
placeholder={Iflytek.ExampleEndpoint}
@@ -1199,6 +1248,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Iflytek.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Iflytek.ApiKey.Title}
value={accessStore.iflytekApiKey}
type="text"
placeholder={Locale.Settings.Access.Iflytek.ApiKey.Placeholder}
@@ -1215,6 +1265,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Iflytek.ApiSecret.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Iflytek.ApiSecret.Title}
value={accessStore.iflytekApiSecret}
type="text"
placeholder={Locale.Settings.Access.Iflytek.ApiSecret.Placeholder}
@@ -1244,6 +1295,7 @@ export function Settings() {
<div className="window-action-button"></div>
<div className="window-action-button">
<IconButton
aria={Locale.UI.Close}
icon={<CloseIcon />}
onClick={() => navigate(Path.Home)}
bordered
@@ -1267,6 +1319,8 @@ export function Settings() {
open={showEmojiPicker}
>
<div
aria-label={Locale.Settings.Avatar}
tabIndex={0}
className={styles.avatar}
onClick={() => {
setShowEmojiPicker(!showEmojiPicker);
@@ -1304,6 +1358,7 @@ export function Settings() {
<ListItem title={Locale.Settings.SendKey}>
<Select
aria-label={Locale.Settings.SendKey}
value={config.submitKey}
onChange={(e) => {
updateConfig(
@@ -1322,6 +1377,7 @@ export function Settings() {
<ListItem title={Locale.Settings.Theme}>
<Select
aria-label={Locale.Settings.Theme}
value={config.theme}
onChange={(e) => {
updateConfig(
@@ -1339,6 +1395,7 @@ export function Settings() {
<ListItem title={Locale.Settings.Lang.Name}>
<Select
aria-label={Locale.Settings.Lang.Name}
value={getLang()}
onChange={(e) => {
changeLang(e.target.value as any);
@@ -1357,6 +1414,7 @@ export function Settings() {
subTitle={Locale.Settings.FontSize.SubTitle}
>
<InputRange
aria={Locale.Settings.FontSize.Title}
title={`${config.fontSize ?? 14}px`}
value={config.fontSize}
min="12"
@@ -1376,6 +1434,7 @@ export function Settings() {
subTitle={Locale.Settings.FontFamily.SubTitle}
>
<input
aria-label={Locale.Settings.FontFamily.Title}
type="text"
value={config.fontFamily}
placeholder={Locale.Settings.FontFamily.Placeholder}
@@ -1392,6 +1451,7 @@ export function Settings() {
subTitle={Locale.Settings.AutoGenerateTitle.SubTitle}
>
<input
aria-label={Locale.Settings.AutoGenerateTitle.Title}
type="checkbox"
checked={config.enableAutoGenerateTitle}
onChange={(e) =>
@@ -1408,6 +1468,7 @@ export function Settings() {
subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
>
<input
aria-label={Locale.Settings.SendPreviewBubble.Title}
type="checkbox"
checked={config.sendPreviewBubble}
onChange={(e) =>
@@ -1428,6 +1489,7 @@ export function Settings() {
subTitle={Locale.Settings.Mask.Splash.SubTitle}
>
<input
aria-label={Locale.Settings.Mask.Splash.Title}
type="checkbox"
checked={!config.dontShowMaskSplashScreen}
onChange={(e) =>
@@ -1445,6 +1507,7 @@ export function Settings() {
subTitle={Locale.Settings.Mask.Builtin.SubTitle}
>
<input
aria-label={Locale.Settings.Mask.Builtin.Title}
type="checkbox"
checked={config.hideBuiltinMasks}
onChange={(e) =>
@@ -1463,6 +1526,7 @@ export function Settings() {
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
<input
aria-label={Locale.Settings.Prompt.Disable.Title}
type="checkbox"
checked={config.disablePromptHint}
onChange={(e) =>
@@ -1482,6 +1546,7 @@ export function Settings() {
)}
>
<IconButton
aria={Locale.Settings.Prompt.List + Locale.Settings.Prompt.Edit}
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => setShowPromptModal(true)}
@@ -1503,6 +1568,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Provider.SubTitle}
>
<Select
aria-label={Locale.Settings.Access.Provider.Title}
value={accessStore.provider}
onChange={(e) => {
accessStore.update(
@@ -1567,6 +1633,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.CustomModel.SubTitle}
>
<input
aria-label={Locale.Settings.Access.CustomModel.Title}
type="text"
value={config.customModels}
placeholder="model1,model2,model3"

View File

@@ -297,12 +297,20 @@ export function SideBar(props: { className?: string }) {
</div>
<div className={styles["sidebar-action"]}>
<Link to={Path.Settings}>
<IconButton icon={<SettingsIcon />} shadow />
<IconButton
aria={Locale.Settings.Title}
icon={<SettingsIcon />}
shadow
/>
</Link>
</div>
<div className={styles["sidebar-action"]}>
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
<IconButton icon={<GithubIcon />} shadow />
<IconButton
aria={Locale.Export.MessageFromChatGPT}
icon={<GithubIcon />}
shadow
/>
</a>
</div>
</>

View File

@@ -265,9 +265,10 @@ export function Input(props: InputProps) {
);
}
export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
export function PasswordInput(
props: HTMLProps<HTMLInputElement> & { aria?: string },
) {
const [visible, setVisible] = useState(false);
function changeVisibility() {
setVisible(!visible);
}
@@ -275,6 +276,7 @@ export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
return (
<div className={"password-input-container"}>
<IconButton
aria={props.aria}
icon={visible ? <EyeIcon /> : <EyeOffIcon />}
onClick={changeVisibility}
className={"password-eye"}

View File

@@ -119,10 +119,16 @@ export const getServerSideConfig = () => {
if (disableGPT4) {
if (customModels) customModels += ",";
customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4"))
customModels += DEFAULT_MODELS.filter(
(m) => m.name.startsWith("gpt-4") && !m.name.startsWith("gpt-4o-mini"),
)
.map((m) => "-" + m.name)
.join(",");
if (defaultModel.startsWith("gpt-4")) defaultModel = "";
if (
defaultModel.startsWith("gpt-4") &&
!defaultModel.startsWith("gpt-4o-mini")
)
defaultModel = "";
}
const isStability = !!process.env.STABILITY_API_KEY;

View File

@@ -243,6 +243,7 @@ export const KnowledgeCutOffDate: Record<string, string> = {
"gpt-4-turbo-preview": "2023-12",
"gpt-4o": "2023-10",
"gpt-4o-2024-05-13": "2023-10",
"gpt-4o-2024-08-06": "2023-10",
"gpt-4o-mini": "2023-10",
"gpt-4o-mini-2024-07-18": "2023-10",
"gpt-4-vision-preview": "2023-04",
@@ -264,6 +265,7 @@ const openaiModels = [
"gpt-4-turbo-preview",
"gpt-4o",
"gpt-4o-2024-05-13",
"gpt-4o-2024-08-06",
"gpt-4o-mini",
"gpt-4o-mini-2024-07-18",
"gpt-4-vision-preview",

4
app/icons/hd.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#333" class="bi bi-badge-hd" viewBox="0 0 16 16">
<path d="M7.396 11V5.001H6.209v2.44H3.687V5H2.5v6h1.187V8.43h2.522V11zM8.5 5.001V11h2.188c1.811 0 2.685-1.107 2.685-3.015 0-1.894-.86-2.984-2.684-2.984zm1.187.967h.843c1.112 0 1.622.686 1.622 2.04 0 1.353-.505 2.02-1.622 2.02h-.843z"/>
<path d="M14 3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

4
app/icons/palette.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#333" class="bi bi-palette" viewBox="0 0 16 16">
<path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3M5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/>
<path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8m-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7"/>
</svg>

After

Width:  |  Height:  |  Size: 781 B

View File

@@ -42,6 +42,7 @@ const cn = {
PinToastAction: "查看",
Delete: "删除",
Edit: "编辑",
FullScreen: "全屏",
},
Commands: {
new: "新建聊天",
@@ -132,6 +133,7 @@ const cn = {
Settings: {
Title: "设置",
SubTitle: "所有设置选项",
ShowPassword: "显示密码",
Danger: {
Reset: {
@@ -204,6 +206,10 @@ const cn = {
Title: "同步类型",
SubTitle: "选择喜爱的同步服务器",
},
EnableAutoSync: {
Title: "自动同步设置",
SubTitle: "在回复完成或删除消息后自动同步数据",
},
Proxy: {
Title: "启用代理",
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",

View File

@@ -44,6 +44,7 @@ const en: LocaleType = {
PinToastAction: "View",
Delete: "Delete",
Edit: "Edit",
FullScreen: "FullScreen",
},
Commands: {
new: "Start a new chat",
@@ -135,6 +136,7 @@ const en: LocaleType = {
Settings: {
Title: "Settings",
SubTitle: "All Settings",
ShowPassword: "ShowPassword",
Danger: {
Reset: {
Title: "Reset All Settings",
@@ -207,6 +209,11 @@ 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",

View File

@@ -1,4 +1,8 @@
import { trimTopic, getMessageTextContent } from "../utils";
import {
trimTopic,
getMessageTextContent,
removeOutdatedEntries,
} from "../utils";
import Locale, { getLang } from "../locales";
import { showToast } from "../components/ui-lib";
@@ -26,6 +30,7 @@ import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
import { collectModelsWithDefaultModel } from "../utils/model";
import { useAccessStore } from "./access";
import { useSyncStore } from "./sync";
import { isDalle3 } from "../utils";
export type ChatMessage = RequestMessage & {
@@ -62,6 +67,7 @@ export interface ChatSession {
lastUpdate: number;
lastSummarizeIndex: number;
clearContextIndex?: number;
deletedMessageIds?: Record<string, number>;
mask: Mask;
}
@@ -85,6 +91,7 @@ function createEmptySession(): ChatSession {
},
lastUpdate: Date.now(),
lastSummarizeIndex: 0,
deletedMessageIds: {},
mask: createEmptyMask(),
};
@@ -162,9 +169,19 @@ 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>,
};
export const useChatStore = createPersistStore(
@@ -253,7 +270,18 @@ export const useChatStore = createPersistStore(
if (!deletedSession) return;
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;
let nextIndex = Math.min(
@@ -270,19 +298,24 @@ 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,
@@ -310,6 +343,7 @@ export const useChatStore = createPersistStore(
});
get().updateStat(message);
get().summarizeSession();
noticeCloudSync();
},
async onUserInput(content: string, attachImages?: string[]) {

View File

@@ -1,5 +1,5 @@
import { LLMModel } from "../client/api";
import { DalleSize } from "../typing";
import { DalleSize, DalleQuality, DalleStyle } from "../typing";
import { getClientConfig } from "../config/client";
import {
DEFAULT_INPUT_TEMPLATE,
@@ -63,6 +63,8 @@ export const DEFAULT_CONFIG = {
enableInjectSystemPrompts: true,
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
size: "1024x1024" as DalleSize,
quality: "standard" as DalleQuality,
style: "vivid" as DalleStyle,
},
};

View File

@@ -26,6 +26,7 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
const DEFAULT_SYNC_STATE = {
provider: ProviderType.WebDAV,
enableAutoSync: true,
useProxy: true,
proxyUrl: corsPath(ApiPath.Cors),
@@ -91,6 +92,11 @@ export const useSyncStore = createPersistStore(
},
async sync() {
const enableAutoSync = get().enableAutoSync;
if (!enableAutoSync) {
return;
}
const localState = getLocalAppState();
const provider = get().provider;
const config = get()[provider];
@@ -100,15 +106,17 @@ 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(
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;
@@ -123,6 +131,14 @@ 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

@@ -9,3 +9,5 @@ export interface RequestMessage {
}
export type DalleSize = "1024x1024" | "1792x1024" | "1024x1792";
export type DalleQuality = "standard" | "hd";
export type DalleStyle = "vivid" | "natural";

View File

@@ -270,3 +270,16 @@ export function isVisionModel(model: string) {
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;
}

View File

@@ -8,6 +8,7 @@ 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,6 +67,7 @@ const MergeStates: StateMerger = {
[StoreKey.Chat]: (localState, remoteState) => {
// merge sessions
const localSessions: Record<string, ChatSession> = {};
const localDeletedSessionIds = localState.deletedSessionIds || {};
localState.sessions.forEach((s) => (localSessions[s.id] = s));
remoteState.sessions.forEach((remoteSession) => {
@@ -75,28 +77,70 @@ const MergeStates: StateMerger = {
const localSession = localSessions[remoteSession.id];
if (!localSession) {
// if remote session is new, just merge it
localState.sessions.push(remoteSession);
if (
(localDeletedSessionIds[remoteSession.id] || -1) <
remoteSession.lastUpdate
) {
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)) {
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
localSession.messages.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
);
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
localState.sessions.sort(
(a, b) =>
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
);
// localState.sessions.sort(
// (a, b) =>
// new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
// );
const deletedSessionIds = {
...remoteDeletedSessionIds,
...localDeletedSessionIds,
};
removeOutdatedEntries(deletedSessionIds);
localState.deletedSessionIds = deletedSessionIds;
return localState;
},