mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-31 03:09:04 +08:00
Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
175b4e7f92 | ||
|
b050417ab1 | ||
|
37b49400db | ||
|
ebcb2e7837 | ||
|
f1e7db6a88 | ||
|
2ba0929458 | ||
|
83fed42997 | ||
|
2c4626709c | ||
|
59fbadd9eb | ||
|
adb860b464 | ||
|
8d8790586d | ||
|
61ca60c550 | ||
|
d713d01600 | ||
|
372ea0f845 | ||
|
61888708f5 | ||
|
c900459f73 | ||
|
2c92f75c86 | ||
|
3e1514239c | ||
|
0707a1d49a | ||
|
9521f19507 | ||
|
bd69116df2 | ||
|
6535986484 | ||
|
e03db9c2d5 | ||
|
038790370c | ||
|
261bf0b298 | ||
|
5a7bdcfe59 | ||
|
4f3261b262 | ||
|
48e6087b1b | ||
|
28103c901d | ||
|
368701610f |
@@ -15,7 +15,6 @@ BASE_URL=
|
||||
|
||||
# Specify OpenAI organization ID.(optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to input their own API key, set this value to 1.
|
||||
OPENAI_ORG_ID=
|
||||
|
||||
# (optional)
|
||||
@@ -31,4 +30,4 @@ DISABLE_GPT4=
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to query balance, set this value to 1.
|
||||
HIDE_BALANCE_QUERY=
|
||||
HIDE_BALANCE_QUERY=
|
||||
|
@@ -114,7 +114,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
OPENAI_API_KEY=<your api key here>
|
||||
|
||||
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
||||
BASE_URL=https://chatgpt2.nextweb.fun/api/proxy
|
||||
BASE_URL=https://nb.nextweb.fun/api/proxy
|
||||
```
|
||||
|
||||
### 本地开发
|
||||
|
@@ -4,7 +4,7 @@ import { getServerSideConfig } from "../../config/server";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
// Danger! Don not write any secret value here!
|
||||
// Danger! Do not hard code any secret value here!
|
||||
// 警告!不要在这里写入任何敏感信息!
|
||||
const DANGER_CONFIG = {
|
||||
needCode: serverConfig.needCode,
|
||||
|
@@ -26,13 +26,18 @@ async function handle(
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
background-color: var(--white);
|
||||
min-width: 600px;
|
||||
min-height: 480px;
|
||||
min-height: 370px;
|
||||
max-width: 1200px;
|
||||
|
||||
display: flex;
|
||||
|
@@ -115,7 +115,7 @@ const loadAsyncGoogleFont = () => {
|
||||
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
|
||||
linkEl.rel = "stylesheet";
|
||||
linkEl.href =
|
||||
googleFontUrl + "/css2?family=Noto+Sans:wght@300;400;700;900&display=swap";
|
||||
googleFontUrl + "/css2?family=" + encodeURIComponent("Noto Sans:wght@300;400;700;900") + "&display=swap";
|
||||
document.head.appendChild(linkEl);
|
||||
};
|
||||
|
||||
|
@@ -151,6 +151,7 @@ export function Markdown(
|
||||
ref={mdRef}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDoubleClickCapture={props.onDoubleClickCapture}
|
||||
dir="auto"
|
||||
>
|
||||
{props.loading ? (
|
||||
<LoadingIcon />
|
||||
|
@@ -50,7 +50,7 @@ import Locale, {
|
||||
} from "../locales";
|
||||
import { copyToClipboard } from "../utils";
|
||||
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 { ErrorBoundary } from "./error";
|
||||
import { InputRange } from "./input-range";
|
||||
@@ -275,7 +275,7 @@ function CheckButton() {
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
text="检查可用性"
|
||||
text={Locale.Settings.Sync.Config.Modal.Check}
|
||||
bordered
|
||||
onClick={check}
|
||||
icon={
|
||||
@@ -413,7 +413,42 @@ function SyncConfigModal(props: { onClose?: () => void }) {
|
||||
|
||||
{syncStore.provider === ProviderType.UpStash && (
|
||||
<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>
|
||||
)}
|
||||
</Modal>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
|
||||
import styles from "./home.module.scss";
|
||||
|
||||
@@ -17,6 +17,7 @@ import Locale from "../locales";
|
||||
import { useAppConfig, useChatStore } from "../store";
|
||||
|
||||
import {
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
NARROW_SIDEBAR_WIDTH,
|
||||
@@ -57,31 +58,57 @@ function useDragSideBar() {
|
||||
|
||||
const config = useAppConfig();
|
||||
const startX = useRef(0);
|
||||
const startDragWidth = useRef(config.sidebarWidth ?? 300);
|
||||
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
const lastUpdateTime = useRef(Date.now());
|
||||
|
||||
const handleMouseMove = useRef((e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 50) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
config.update((config) => (config.sidebarWidth = nextWidth));
|
||||
});
|
||||
|
||||
const handleMouseUp = useRef(() => {
|
||||
startDragWidth.current = config.sidebarWidth ?? 300;
|
||||
window.removeEventListener("mousemove", handleMouseMove.current);
|
||||
window.removeEventListener("mouseup", handleMouseUp.current);
|
||||
});
|
||||
|
||||
const onDragMouseDown = (e: MouseEvent) => {
|
||||
startX.current = e.clientX;
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove.current);
|
||||
window.addEventListener("mouseup", handleMouseUp.current);
|
||||
const toggleSideBar = () => {
|
||||
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;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
config.update((config) => {
|
||||
if (nextWidth < MIN_SIDEBAR_WIDTH) {
|
||||
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
||||
} else {
|
||||
config.sidebarWidth = nextWidth;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 shouldNarrow =
|
||||
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
||||
@@ -89,13 +116,13 @@ function useDragSideBar() {
|
||||
useEffect(() => {
|
||||
const barWidth = shouldNarrow
|
||||
? NARROW_SIDEBAR_WIDTH
|
||||
: limit(config.sidebarWidth ?? 300);
|
||||
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
||||
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
||||
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
||||
|
||||
return {
|
||||
onDragMouseDown,
|
||||
onDragStart,
|
||||
shouldNarrow,
|
||||
};
|
||||
}
|
||||
@@ -104,7 +131,7 @@ export function SideBar(props: { className?: string }) {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
// drag side bar
|
||||
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
|
||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||
const navigate = useNavigate();
|
||||
const config = useAppConfig();
|
||||
|
||||
@@ -198,7 +225,7 @@ export function SideBar(props: { className?: string }) {
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
||||
onPointerDown={(e) => onDragStart(e as any)}
|
||||
>
|
||||
<DragIcon />
|
||||
</div>
|
||||
|
@@ -8,7 +8,7 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c
|
||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||
|
||||
export const DEFAULT_CORS_HOST = "https://chatgpt2.nextweb.fun";
|
||||
export const DEFAULT_CORS_HOST = "https://nb.nextweb.fun";
|
||||
export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
|
||||
|
||||
export enum Path {
|
||||
@@ -43,6 +43,7 @@ export enum StoreKey {
|
||||
Sync = "sync",
|
||||
}
|
||||
|
||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
||||
export const MAX_SIDEBAR_WIDTH = 500;
|
||||
export const MIN_SIDEBAR_WIDTH = 230;
|
||||
export const NARROW_SIDEBAR_WIDTH = 100;
|
||||
|
@@ -187,6 +187,7 @@ const cn = {
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "配置云同步",
|
||||
Check: "检查可用性",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "同步类型",
|
||||
@@ -206,6 +207,12 @@ const cn = {
|
||||
UserName: "用户名",
|
||||
Password: "密码",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "UpStash Redis REST Url",
|
||||
UserName: "备份名称",
|
||||
Password: "UpStash Redis REST Token",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "本地数据",
|
||||
|
@@ -189,6 +189,7 @@ const en: LocaleType = {
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Config Sync",
|
||||
Check: "Check Connection",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Sync Type",
|
||||
@@ -209,6 +210,12 @@ const en: LocaleType = {
|
||||
UserName: "User Name",
|
||||
Password: "Password",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "UpStash Redis REST Url",
|
||||
UserName: "Backup Name",
|
||||
Password: "UpStash Redis REST Token",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "Local Data",
|
||||
|
@@ -60,7 +60,9 @@ const id: PartialLocaleType = {
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter untuk membalut";
|
||||
}
|
||||
return inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah";
|
||||
return (
|
||||
inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah"
|
||||
);
|
||||
},
|
||||
Send: "Kirim",
|
||||
Config: {
|
||||
@@ -117,33 +119,35 @@ const id: PartialLocaleType = {
|
||||
Title: "Setel Ulang Semua Pengaturan",
|
||||
SubTitle: "Mengembalikan semua pengaturan ke nilai default",
|
||||
Action: "Setel Ulang",
|
||||
Confirm: "Anda yakin ingin mengembalikan semua pengaturan ke nilai default?",
|
||||
Confirm:
|
||||
"Anda yakin ingin mengembalikan semua pengaturan ke nilai default?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "Hapus Semua Data",
|
||||
SubTitle: "Menghapus semua pesan dan pengaturan",
|
||||
SubTitle: "Semua data yang tersimpan secara lokal akan dihapus",
|
||||
Action: "Hapus",
|
||||
Confirm: "Anda yakin ingin menghapus semua pesan dan pengaturan?",
|
||||
Confirm:
|
||||
"Apakah Anda yakin ingin menghapus semua data yang tersimpan secara lokal?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "Semua Bahasa",
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
FontSize: {
|
||||
Title: "Ukuran Font",
|
||||
SubTitle: "Ubah ukuran font konten chat",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "Suntikkan Petunjuk Sistem",
|
||||
SubTitle:
|
||||
"Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "Template Input",
|
||||
SubTitle: "Pesan baru akan diisi menggunakan template ini",
|
||||
},
|
||||
Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "Semua Bahasa",
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
FontSize: {
|
||||
Title: "Ukuran Font",
|
||||
SubTitle: "Ubah ukuran font konten chat",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "Suntikkan Petunjuk Sistem",
|
||||
SubTitle:
|
||||
"Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "Template Input",
|
||||
SubTitle: "Pesan baru akan diisi menggunakan template ini",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Version: ${x}`,
|
||||
@@ -154,9 +158,40 @@ const id: PartialLocaleType = {
|
||||
GoToUpdate: "Perbarui Sekarang",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "Hasilkan Judul Otomatis",
|
||||
SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan",
|
||||
Title: "Hasilkan Judul Otomatis",
|
||||
SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "Pembaruan Terakhir",
|
||||
NotSyncYet: "Belum disinkronkan",
|
||||
Success: "Sinkronisasi Berhasil",
|
||||
Fail: "Sinkronisasi Gagal",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Konfigurasi Sinkronisasi",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Tipe Sinkronisasi",
|
||||
SubTitle: "Pilih layanan sinkronisasi favorit Anda",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Aktifkan Proxy CORS",
|
||||
SubTitle:
|
||||
"Aktifkan Proxy untuk menghindari pembatasan atau pemblokiran lintas sumber",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "Lokasi Titik Akhir Proxy CORS",
|
||||
SubTitle: "Hanya berlaku untuk Proxy CORS bawaan untuk proyek ini",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "Lokasi Titik Akhir WebDAV",
|
||||
UserName: "User Pengguna",
|
||||
Password: "Kata Sandi",
|
||||
},
|
||||
},
|
||||
},
|
||||
SendKey: "Kirim",
|
||||
Theme: "Tema",
|
||||
TightBorder: "Batas Ketat",
|
||||
@@ -176,76 +211,77 @@ const id: PartialLocaleType = {
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Nonaktifkan Otomatisasi",
|
||||
SubTitle: "Aktifkan/Matikan otomatisasi",
|
||||
},
|
||||
List: "Daftar Prompt",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} bawaan, ${custom} penggunaan khusus`,
|
||||
Edit: "Edit",
|
||||
Modal: {
|
||||
Title: "Daftar Prompt",
|
||||
Add: "Tambahkan",
|
||||
Search: "Cari Prompt",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Edit Prompt",
|
||||
},
|
||||
Disable: {
|
||||
Title: "Nonaktifkan Otomatisasi",
|
||||
SubTitle: "Aktifkan/Matikan otomatisasi",
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Jumlah Pesan Riwayat",
|
||||
SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan",
|
||||
List: "Daftar Prompt",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} bawaan, ${custom} penggunaan khusus`,
|
||||
Edit: "Edit",
|
||||
Modal: {
|
||||
Title: "Daftar Prompt",
|
||||
Add: "Tambahkan",
|
||||
Search: "Cari Prompt",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Batas Kompresi Riwayat",
|
||||
SubTitle:
|
||||
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
|
||||
},
|
||||
Token: {
|
||||
Title: "Kunci API",
|
||||
SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses",
|
||||
Placeholder: "Kunci API OpenAI",
|
||||
EditModal: {
|
||||
Title: "Edit Prompt",
|
||||
},
|
||||
Usage: {
|
||||
Title: "Saldo Akun",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Digunakan bulan ini: ${used}, total langganan: ${total}`;
|
||||
},
|
||||
IsChecking: "Memeriksa...",
|
||||
Check: "Periksa",
|
||||
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Jumlah Pesan Riwayat",
|
||||
SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Batas Kompresi Riwayat",
|
||||
SubTitle:
|
||||
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
|
||||
},
|
||||
Token: {
|
||||
Title: "Kunci API",
|
||||
SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses",
|
||||
Placeholder: "Kunci API OpenAI",
|
||||
},
|
||||
Usage: {
|
||||
Title: "Saldo Akun",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Digunakan bulan ini: ${used}, total langganan: ${total}`;
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Kode Akses",
|
||||
SubTitle: "Kontrol akses diaktifkan",
|
||||
Placeholder: "Diperlukan kode akses",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint",
|
||||
SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom",
|
||||
},
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Suhu",
|
||||
SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya",
|
||||
},
|
||||
TopP: {
|
||||
Title: "Top P",
|
||||
SubTitle: "Tidak mengubah nilai dengan suhu",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Token Maksimum",
|
||||
SubTitle: "Panjang maksimum token input dan output",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "Penalti Kehadiran",
|
||||
SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "Penalti Frekuensi",
|
||||
SubTitle: "Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
|
||||
},
|
||||
IsChecking: "Memeriksa...",
|
||||
Check: "Periksa",
|
||||
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Kode Akses",
|
||||
SubTitle: "Kontrol akses diaktifkan",
|
||||
Placeholder: "Diperlukan kode akses",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint",
|
||||
SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom",
|
||||
},
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Suhu",
|
||||
SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya",
|
||||
},
|
||||
TopP: {
|
||||
Title: "Top P",
|
||||
SubTitle: "Tidak mengubah nilai dengan suhu",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Token Maksimum",
|
||||
SubTitle: "Panjang maksimum token input dan output",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "Penalti Kehadiran",
|
||||
SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "Penalti Frekuensi",
|
||||
SubTitle:
|
||||
"Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Percakapan Baru",
|
||||
@@ -261,8 +297,9 @@ const id: PartialLocaleType = {
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Berhasil disalin ke clipboard",
|
||||
Failed: "Gagal menyalin, berikan izin untuk memberikan izin",
|
||||
Success: "Tersalin ke clipboard",
|
||||
Failed:
|
||||
"Gagal menyalin, mohon berikan izin untuk mengakses clipboard atau Clipboard API tidak didukung (Tauri)",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `Dengan ${x} promp kontekstual`,
|
||||
@@ -341,7 +378,7 @@ const id: PartialLocaleType = {
|
||||
Model: "Model",
|
||||
Messages: "Pesan",
|
||||
Topic: "Topik",
|
||||
Time: "Waktu",
|
||||
Time: "Tanggal & Waktu",
|
||||
},
|
||||
URLCommand: {
|
||||
Code: "Kode akses terdeteksi dari url, konfirmasi untuk mendaftar ? ",
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { LLMModel } from "../client/api";
|
||||
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";
|
||||
|
||||
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
||||
@@ -29,7 +34,7 @@ export const DEFAULT_CONFIG = {
|
||||
tightBorder: !!getClientConfig()?.isApp,
|
||||
sendPreviewBubble: true,
|
||||
enableAutoGenerateTitle: true,
|
||||
sidebarWidth: 300,
|
||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Updater } from "../typing";
|
||||
import { ApiPath, StoreKey } from "../constant";
|
||||
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import {
|
||||
AppState,
|
||||
@@ -22,27 +22,29 @@ export interface WebDavConfig {
|
||||
|
||||
export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||
|
||||
export const useSyncStore = createPersistStore(
|
||||
{
|
||||
provider: ProviderType.WebDAV,
|
||||
useProxy: true,
|
||||
proxyUrl: corsPath(ApiPath.Cors),
|
||||
const DEFAULT_SYNC_STATE = {
|
||||
provider: ProviderType.WebDAV,
|
||||
useProxy: true,
|
||||
proxyUrl: corsPath(ApiPath.Cors),
|
||||
|
||||
webdav: {
|
||||
endpoint: "",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
|
||||
upstash: {
|
||||
endpoint: "",
|
||||
username: "",
|
||||
apiKey: "",
|
||||
},
|
||||
|
||||
lastSyncTime: 0,
|
||||
lastProvider: "",
|
||||
webdav: {
|
||||
endpoint: "",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
|
||||
upstash: {
|
||||
endpoint: "",
|
||||
username: STORAGE_KEY,
|
||||
apiKey: "",
|
||||
},
|
||||
|
||||
lastSyncTime: 0,
|
||||
lastProvider: "",
|
||||
};
|
||||
|
||||
export const useSyncStore = createPersistStore(
|
||||
DEFAULT_SYNC_STATE,
|
||||
(set, get) => ({
|
||||
coundSync() {
|
||||
const config = get()[get().provider];
|
||||
@@ -108,6 +110,16 @@ export const useSyncStore = createPersistStore(
|
||||
}),
|
||||
{
|
||||
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 { corsFetch } from "../cors";
|
||||
import { chunks } from "../format";
|
||||
|
||||
export type UpstashConfig = SyncStore["upstash"];
|
||||
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 {
|
||||
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() {
|
||||
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() {
|
||||
throw Error("[Sync] not implemented");
|
||||
async set(_: string, value: string) {
|
||||
// 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() {
|
||||
return {
|
||||
Authorization: `Basic ${config.apiKey}`,
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
};
|
||||
},
|
||||
path(path: string) {
|
||||
|
@@ -20,10 +20,8 @@ export function createWebDavClient(store: SyncStore) {
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[WebDav] check", res.status, res.statusText);
|
||||
|
||||
return [201, 200, 404].includes(res.status);
|
||||
return [201, 200, 404, 401].includes(res.status);
|
||||
} catch (e) {
|
||||
console.error("[WebDav] failed to check", e);
|
||||
}
|
||||
|
@@ -11,3 +11,18 @@ export function prettyObject(msg: any) {
|
||||
}
|
||||
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));
|
||||
|
||||
remoteState.sessions.forEach((remoteSession) => {
|
||||
// skip empty chats
|
||||
if (remoteSession.messages.length === 0) return;
|
||||
|
||||
const localSession = localSessions[remoteSession.id];
|
||||
if (!localSession) {
|
||||
// if remote session is new, just merge it
|
||||
|
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "ChatGPT Next Web",
|
||||
"version": "2.9.6"
|
||||
"version": "2.9.7"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
Reference in New Issue
Block a user