mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-31 18:00:27 +09:00
Merge branch 'Yidadaa:main' into main
This commit is contained in:
commit
90ce3f1e19
@ -26,13 +26,18 @@ async function handle(
|
|||||||
duplex: "half",
|
duplex: "half",
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[Any Proxy]", targetUrl);
|
const fetchResult = await fetch(targetUrl, fetchOptions);
|
||||||
|
|
||||||
const fetchResult = fetch(targetUrl, fetchOptions);
|
console.log("[Any Proxy]", targetUrl, {
|
||||||
|
status: fetchResult.status,
|
||||||
|
statusText: fetchResult.statusText,
|
||||||
|
});
|
||||||
|
|
||||||
return fetchResult;
|
return fetchResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const POST = handle;
|
export const POST = handle;
|
||||||
|
export const GET = handle;
|
||||||
|
export const OPTIONS = handle;
|
||||||
|
|
||||||
export const runtime = "edge";
|
export const runtime = "nodejs";
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
color: var(--black);
|
color: var(--black);
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
min-height: 480px;
|
min-height: 370px;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -50,7 +50,7 @@ import Locale, {
|
|||||||
} from "../locales";
|
} from "../locales";
|
||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard } from "../utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Path, RELEASE_URL, UPDATE_URL } from "../constant";
|
import { Path, RELEASE_URL, STORAGE_KEY, UPDATE_URL } from "../constant";
|
||||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||||
import { ErrorBoundary } from "./error";
|
import { ErrorBoundary } from "./error";
|
||||||
import { InputRange } from "./input-range";
|
import { InputRange } from "./input-range";
|
||||||
@ -276,7 +276,7 @@ function CheckButton() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
text="检查可用性"
|
text={Locale.Settings.Sync.Config.Modal.Check}
|
||||||
bordered
|
bordered
|
||||||
onClick={check}
|
onClick={check}
|
||||||
icon={
|
icon={
|
||||||
@ -414,7 +414,42 @@ function SyncConfigModal(props: { onClose?: () => void }) {
|
|||||||
|
|
||||||
{syncStore.provider === ProviderType.UpStash && (
|
{syncStore.provider === ProviderType.UpStash && (
|
||||||
<List>
|
<List>
|
||||||
<ListItem title={Locale.WIP}></ListItem>
|
<ListItem title={Locale.Settings.Sync.Config.UpStash.Endpoint}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={syncStore.upstash.endpoint}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) =>
|
||||||
|
(config.upstash.endpoint = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem title={Locale.Settings.Sync.Config.UpStash.UserName}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={syncStore.upstash.username}
|
||||||
|
placeholder={STORAGE_KEY}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) =>
|
||||||
|
(config.upstash.username = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem title={Locale.Settings.Sync.Config.UpStash.Password}>
|
||||||
|
<PasswordInput
|
||||||
|
value={syncStore.upstash.apiKey}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) => (config.upstash.apiKey = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></PasswordInput>
|
||||||
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -17,6 +17,7 @@ import Locale from "../locales";
|
|||||||
import { useAppConfig, useChatStore } from "../store";
|
import { useAppConfig, useChatStore } from "../store";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_SIDEBAR_WIDTH,
|
||||||
MAX_SIDEBAR_WIDTH,
|
MAX_SIDEBAR_WIDTH,
|
||||||
MIN_SIDEBAR_WIDTH,
|
MIN_SIDEBAR_WIDTH,
|
||||||
NARROW_SIDEBAR_WIDTH,
|
NARROW_SIDEBAR_WIDTH,
|
||||||
@ -57,11 +58,27 @@ function useDragSideBar() {
|
|||||||
|
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const startX = useRef(0);
|
const startX = useRef(0);
|
||||||
const startDragWidth = useRef(config.sidebarWidth ?? 300);
|
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||||
const lastUpdateTime = useRef(Date.now());
|
const lastUpdateTime = useRef(Date.now());
|
||||||
|
|
||||||
const handleMouseMove = useRef((e: MouseEvent) => {
|
const toggleSideBar = () => {
|
||||||
if (Date.now() < lastUpdateTime.current + 50) {
|
config.update((config) => {
|
||||||
|
if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) {
|
||||||
|
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
||||||
|
} else {
|
||||||
|
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragStart = (e: MouseEvent) => {
|
||||||
|
// Remembers the initial width each time the mouse is pressed
|
||||||
|
startX.current = e.clientX;
|
||||||
|
startDragWidth.current = config.sidebarWidth;
|
||||||
|
const dragStartTime = Date.now();
|
||||||
|
|
||||||
|
const handleDragMove = (e: MouseEvent) => {
|
||||||
|
if (Date.now() < lastUpdateTime.current + 20) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastUpdateTime.current = Date.now();
|
lastUpdateTime.current = Date.now();
|
||||||
@ -74,22 +91,24 @@ function useDragSideBar() {
|
|||||||
config.sidebarWidth = nextWidth;
|
config.sidebarWidth = nextWidth;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const handleMouseUp = useRef(() => {
|
|
||||||
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
|
|
||||||
// startDragWidth.current = config.sidebarWidth ?? 300;
|
|
||||||
window.removeEventListener("mousemove", handleMouseMove.current);
|
|
||||||
window.removeEventListener("mouseup", handleMouseUp.current);
|
|
||||||
});
|
|
||||||
|
|
||||||
const onDragMouseDown = (e: MouseEvent) => {
|
|
||||||
startX.current = e.clientX;
|
|
||||||
// Remembers the initial width each time the mouse is pressed
|
|
||||||
startDragWidth.current = config.sidebarWidth;
|
|
||||||
window.addEventListener("mousemove", handleMouseMove.current);
|
|
||||||
window.addEventListener("mouseup", handleMouseUp.current);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
|
||||||
|
window.removeEventListener("pointermove", handleDragMove);
|
||||||
|
window.removeEventListener("pointerup", handleDragEnd);
|
||||||
|
|
||||||
|
// if user click the drag icon, should toggle the sidebar
|
||||||
|
const shouldFireClick = Date.now() - dragStartTime < 300;
|
||||||
|
if (shouldFireClick) {
|
||||||
|
toggleSideBar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pointermove", handleDragMove);
|
||||||
|
window.addEventListener("pointerup", handleDragEnd);
|
||||||
|
};
|
||||||
|
|
||||||
const isMobileScreen = useMobileScreen();
|
const isMobileScreen = useMobileScreen();
|
||||||
const shouldNarrow =
|
const shouldNarrow =
|
||||||
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
||||||
@ -97,13 +116,13 @@ function useDragSideBar() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const barWidth = shouldNarrow
|
const barWidth = shouldNarrow
|
||||||
? NARROW_SIDEBAR_WIDTH
|
? NARROW_SIDEBAR_WIDTH
|
||||||
: limit(config.sidebarWidth ?? 300);
|
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||||
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
||||||
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
||||||
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDragMouseDown,
|
onDragStart,
|
||||||
shouldNarrow,
|
shouldNarrow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -112,7 +131,7 @@ export function SideBar(props: { className?: string }) {
|
|||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
// drag side bar
|
// drag side bar
|
||||||
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
|
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
|
||||||
@ -206,7 +225,7 @@ export function SideBar(props: { className?: string }) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles["sidebar-drag"]}
|
className={styles["sidebar-drag"]}
|
||||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
onPointerDown={(e) => onDragStart(e as any)}
|
||||||
>
|
>
|
||||||
<DragIcon />
|
<DragIcon />
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,6 +46,7 @@ export enum StoreKey {
|
|||||||
Sync = "sync",
|
Sync = "sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
||||||
export const MAX_SIDEBAR_WIDTH = 500;
|
export const MAX_SIDEBAR_WIDTH = 500;
|
||||||
export const MIN_SIDEBAR_WIDTH = 230;
|
export const MIN_SIDEBAR_WIDTH = 230;
|
||||||
export const NARROW_SIDEBAR_WIDTH = 100;
|
export const NARROW_SIDEBAR_WIDTH = 100;
|
||||||
|
@ -189,6 +189,7 @@ const cn = {
|
|||||||
Config: {
|
Config: {
|
||||||
Modal: {
|
Modal: {
|
||||||
Title: "配置云同步",
|
Title: "配置云同步",
|
||||||
|
Check: "检查可用性",
|
||||||
},
|
},
|
||||||
SyncType: {
|
SyncType: {
|
||||||
Title: "同步类型",
|
Title: "同步类型",
|
||||||
@ -208,6 +209,12 @@ const cn = {
|
|||||||
UserName: "用户名",
|
UserName: "用户名",
|
||||||
Password: "密码",
|
Password: "密码",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
UpStash: {
|
||||||
|
Endpoint: "UpStash Redis REST Url",
|
||||||
|
UserName: "备份名称",
|
||||||
|
Password: "UpStash Redis REST Token",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
LocalState: "本地数据",
|
LocalState: "本地数据",
|
||||||
|
@ -191,6 +191,7 @@ const en: LocaleType = {
|
|||||||
Config: {
|
Config: {
|
||||||
Modal: {
|
Modal: {
|
||||||
Title: "Config Sync",
|
Title: "Config Sync",
|
||||||
|
Check: "Check Connection",
|
||||||
},
|
},
|
||||||
SyncType: {
|
SyncType: {
|
||||||
Title: "Sync Type",
|
Title: "Sync Type",
|
||||||
@ -211,6 +212,12 @@ const en: LocaleType = {
|
|||||||
UserName: "User Name",
|
UserName: "User Name",
|
||||||
Password: "Password",
|
Password: "Password",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
UpStash: {
|
||||||
|
Endpoint: "UpStash Redis REST Url",
|
||||||
|
UserName: "Backup Name",
|
||||||
|
Password: "UpStash Redis REST Token",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
LocalState: "Local Data",
|
LocalState: "Local Data",
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { LLMModel } from "../client/api";
|
import { LLMModel } from "../client/api";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant";
|
import {
|
||||||
|
DEFAULT_INPUT_TEMPLATE,
|
||||||
|
DEFAULT_MODELS,
|
||||||
|
DEFAULT_SIDEBAR_WIDTH,
|
||||||
|
StoreKey,
|
||||||
|
} from "../constant";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
|
|
||||||
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
||||||
@ -29,7 +34,7 @@ export const DEFAULT_CONFIG = {
|
|||||||
tightBorder: !!getClientConfig()?.isApp,
|
tightBorder: !!getClientConfig()?.isApp,
|
||||||
sendPreviewBubble: true,
|
sendPreviewBubble: true,
|
||||||
enableAutoGenerateTitle: true,
|
enableAutoGenerateTitle: true,
|
||||||
sidebarWidth: 300,
|
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||||
|
|
||||||
disablePromptHint: false,
|
disablePromptHint: false,
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Updater } from "../typing";
|
import { Updater } from "../typing";
|
||||||
import { ApiPath, StoreKey } from "../constant";
|
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
@ -22,8 +22,7 @@ export interface WebDavConfig {
|
|||||||
|
|
||||||
export type SyncStore = GetStoreState<typeof useSyncStore>;
|
export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||||
|
|
||||||
export const useSyncStore = createPersistStore(
|
const DEFAULT_SYNC_STATE = {
|
||||||
{
|
|
||||||
provider: ProviderType.WebDAV,
|
provider: ProviderType.WebDAV,
|
||||||
useProxy: true,
|
useProxy: true,
|
||||||
proxyUrl: corsPath(ApiPath.Cors),
|
proxyUrl: corsPath(ApiPath.Cors),
|
||||||
@ -36,13 +35,16 @@ export const useSyncStore = createPersistStore(
|
|||||||
|
|
||||||
upstash: {
|
upstash: {
|
||||||
endpoint: "",
|
endpoint: "",
|
||||||
username: "",
|
username: STORAGE_KEY,
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
lastSyncTime: 0,
|
lastSyncTime: 0,
|
||||||
lastProvider: "",
|
lastProvider: "",
|
||||||
},
|
};
|
||||||
|
|
||||||
|
export const useSyncStore = createPersistStore(
|
||||||
|
DEFAULT_SYNC_STATE,
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
coundSync() {
|
coundSync() {
|
||||||
const config = get()[get().provider];
|
const config = get()[get().provider];
|
||||||
@ -108,6 +110,16 @@ export const useSyncStore = createPersistStore(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: StoreKey.Sync,
|
name: StoreKey.Sync,
|
||||||
version: 1,
|
version: 1.1,
|
||||||
|
|
||||||
|
migrate(persistedState, version) {
|
||||||
|
const newState = persistedState as typeof DEFAULT_SYNC_STATE;
|
||||||
|
|
||||||
|
if (version < 1.1) {
|
||||||
|
newState.upstash.username = STORAGE_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState as any;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,25 +1,87 @@
|
|||||||
|
import { STORAGE_KEY } from "@/app/constant";
|
||||||
import { SyncStore } from "@/app/store/sync";
|
import { SyncStore } from "@/app/store/sync";
|
||||||
|
import { corsFetch } from "../cors";
|
||||||
|
import { chunks } from "../format";
|
||||||
|
|
||||||
export type UpstashConfig = SyncStore["upstash"];
|
export type UpstashConfig = SyncStore["upstash"];
|
||||||
export type UpStashClient = ReturnType<typeof createUpstashClient>;
|
export type UpStashClient = ReturnType<typeof createUpstashClient>;
|
||||||
|
|
||||||
export function createUpstashClient(config: UpstashConfig) {
|
export function createUpstashClient(store: SyncStore) {
|
||||||
|
const config = store.upstash;
|
||||||
|
const storeKey = config.username.length === 0 ? STORAGE_KEY : config.username;
|
||||||
|
const chunkCountKey = `${storeKey}-chunk-count`;
|
||||||
|
const chunkIndexKey = (i: number) => `${storeKey}-chunk-${i}`;
|
||||||
|
|
||||||
|
const proxyUrl =
|
||||||
|
store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async check() {
|
async check() {
|
||||||
return true;
|
try {
|
||||||
|
const res = await corsFetch(this.path(`get/${storeKey}`), {
|
||||||
|
method: "GET",
|
||||||
|
headers: this.headers(),
|
||||||
|
proxyUrl,
|
||||||
|
});
|
||||||
|
console.log("[Upstash] check", res.status, res.statusText);
|
||||||
|
return [200].includes(res.status);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Upstash] failed to check", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async redisGet(key: string) {
|
||||||
|
const res = await corsFetch(this.path(`get/${key}`), {
|
||||||
|
method: "GET",
|
||||||
|
headers: this.headers(),
|
||||||
|
proxyUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[Upstash] get key = ", key, res.status, res.statusText);
|
||||||
|
const resJson = (await res.json()) as { result: string };
|
||||||
|
|
||||||
|
return resJson.result;
|
||||||
|
},
|
||||||
|
|
||||||
|
async redisSet(key: string, value: string) {
|
||||||
|
const res = await corsFetch(this.path(`set/${key}`), {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.headers(),
|
||||||
|
body: value,
|
||||||
|
proxyUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[Upstash] set key = ", key, res.status, res.statusText);
|
||||||
},
|
},
|
||||||
|
|
||||||
async get() {
|
async get() {
|
||||||
throw Error("[Sync] not implemented");
|
const chunkCount = Number(await this.redisGet(chunkCountKey));
|
||||||
|
if (!Number.isInteger(chunkCount)) return;
|
||||||
|
|
||||||
|
const chunks = await Promise.all(
|
||||||
|
new Array(chunkCount)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => this.redisGet(chunkIndexKey(i))),
|
||||||
|
);
|
||||||
|
console.log("[Upstash] get full chunks", chunks);
|
||||||
|
return chunks.join("");
|
||||||
},
|
},
|
||||||
|
|
||||||
async set() {
|
async set(_: string, value: string) {
|
||||||
throw Error("[Sync] not implemented");
|
// upstash limit the max request size which is 1Mb for “Free” and “Pay as you go”
|
||||||
|
// so we need to split the data to chunks
|
||||||
|
let index = 0;
|
||||||
|
for await (const chunk of chunks(value)) {
|
||||||
|
await this.redisSet(chunkIndexKey(index), chunk);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
await this.redisSet(chunkCountKey, index.toString());
|
||||||
},
|
},
|
||||||
|
|
||||||
headers() {
|
headers() {
|
||||||
return {
|
return {
|
||||||
Authorization: `Basic ${config.apiKey}`,
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
path(path: string) {
|
path(path: string) {
|
||||||
|
@ -20,10 +20,8 @@ export function createWebDavClient(store: SyncStore) {
|
|||||||
headers: this.headers(),
|
headers: this.headers(),
|
||||||
proxyUrl,
|
proxyUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[WebDav] check", res.status, res.statusText);
|
console.log("[WebDav] check", res.status, res.statusText);
|
||||||
|
return [201, 200, 404, 401].includes(res.status);
|
||||||
return [201, 200, 404].includes(res.status);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[WebDav] failed to check", e);
|
console.error("[WebDav] failed to check", e);
|
||||||
}
|
}
|
||||||
|
@ -11,3 +11,18 @@ export function prettyObject(msg: any) {
|
|||||||
}
|
}
|
||||||
return ["```json", msg, "```"].join("\n");
|
return ["```json", msg, "```"].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* chunks(s: string, maxBytes = 1000 * 1000) {
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
let buf = new TextEncoder().encode(s);
|
||||||
|
while (buf.length) {
|
||||||
|
let i = buf.lastIndexOf(32, maxBytes + 1);
|
||||||
|
// If no space found, try forward search
|
||||||
|
if (i < 0) i = buf.indexOf(32, maxBytes);
|
||||||
|
// If there's no space at all, take all
|
||||||
|
if (i < 0) i = buf.length;
|
||||||
|
// This is a safe cut-off point; never half-way a multi-byte
|
||||||
|
yield decoder.decode(buf.slice(0, i));
|
||||||
|
buf = buf.slice(i + 1); // Skip space (if any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -69,6 +69,9 @@ const MergeStates: StateMerger = {
|
|||||||
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
||||||
|
|
||||||
remoteState.sessions.forEach((remoteSession) => {
|
remoteState.sessions.forEach((remoteSession) => {
|
||||||
|
// skip empty chats
|
||||||
|
if (remoteSession.messages.length === 0) return;
|
||||||
|
|
||||||
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
|
||||||
|
Loading…
Reference in New Issue
Block a user