Merge branch 'Yidadaa:main' into main

This commit is contained in:
EricYangXD
2023-04-19 15:31:58 +08:00
committed by GitHub
32 changed files with 822 additions and 279 deletions

View File

@@ -9,9 +9,16 @@ export async function requestOpenai(req: NextRequest) {
const apiKey = req.headers.get("token");
const openaiPath = req.headers.get("path");
console.log("[Proxy] ", openaiPath);
let baseUrl = BASE_URL;
return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, {
if (!baseUrl.startsWith("http")) {
baseUrl = `${PROTOCOL}://${baseUrl}`;
}
console.log("[Proxy] ", openaiPath);
console.log("[Base Url]", baseUrl);
return fetch(`${baseUrl}/${openaiPath}`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,

View File

@@ -17,7 +17,7 @@ async function makeRequest(req: NextRequest) {
},
{
status: 500,
},
}
);
}
}
@@ -29,3 +29,7 @@ export async function POST(req: NextRequest) {
export async function GET(req: NextRequest) {
return makeRequest(req);
}
export const config = {
runtime: "edge",
};

View File

@@ -3,6 +3,7 @@ import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg";
import RenameIcon from "../icons/rename.svg";
import ExportIcon from "../icons/share.svg";
import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg";
@@ -18,6 +19,7 @@ import LightIcon from "../icons/light.svg";
import DarkIcon from "../icons/dark.svg";
import AutoIcon from "../icons/auto.svg";
import BottomIcon from "../icons/bottom.svg";
import StopIcon from "../icons/pause.svg";
import {
Message,
@@ -37,7 +39,6 @@ import {
isMobileScreen,
selectOrCopy,
autoGrowTextArea,
getCSSVar,
} from "../utils";
import dynamic from "next/dynamic";
@@ -354,8 +355,8 @@ export function ChatActions(props: {
}) {
const chatStore = useChatStore();
// switch themes
const theme = chatStore.config.theme;
function nextTheme() {
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
const themeIndex = themes.indexOf(theme);
@@ -364,8 +365,20 @@ export function ChatActions(props: {
chatStore.updateConfig((config) => (config.theme = nextTheme));
}
// stop all responses
const couldStop = ControllerPool.hasPending();
const stopAll = () => ControllerPool.stopAll();
return (
<div className={chatStyle["chat-input-actions"]}>
{couldStop && (
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={stopAll}
>
<StopIcon />
</div>
)}
{!props.hitBottom && (
<div
className={`${chatStyle["chat-input-action"]} clickable`}
@@ -442,16 +455,6 @@ export function Chat(props: {
inputRef.current?.focus();
};
const scrollInput = () => {
const dom = inputRef.current;
if (!dom) return;
const paddingBottomNum: number = parseInt(
window.getComputedStyle(dom).paddingBottom,
10,
);
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
};
// auto grow input
const [inputRows, setInputRows] = useState(2);
const measure = useDebouncedCallback(
@@ -476,7 +479,6 @@ export function Chat(props: {
// only search prompts when user input is short
const SEARCH_TEXT_LIMIT = 30;
const onInput = (text: string) => {
scrollInput();
setUserInput(text);
const n = text.trim().length;
@@ -534,21 +536,44 @@ export function Chat(props: {
}
};
const onResend = (botIndex: number) => {
const findLastUesrIndex = (messageId: number) => {
// find last user input message and resend
for (let i = botIndex; i >= 0; i -= 1) {
if (messages[i].role === "user") {
setIsLoading(true);
chatStore
.onUserInput(messages[i].content)
.then(() => setIsLoading(false));
chatStore.updateCurrentSession((session) =>
session.messages.splice(i, 2),
);
inputRef.current?.focus();
return;
let lastUserMessageIndex: number | null = null;
for (let i = 0; i < session.messages.length; i += 1) {
const message = session.messages[i];
if (message.id === messageId) {
break;
}
if (message.role === "user") {
lastUserMessageIndex = i;
}
}
return lastUserMessageIndex;
};
const deleteMessage = (userIndex: number) => {
chatStore.updateCurrentSession((session) =>
session.messages.splice(userIndex, 2),
);
};
const onDelete = (botMessageId: number) => {
const userIndex = findLastUesrIndex(botMessageId);
if (userIndex === null) return;
deleteMessage(userIndex);
};
const onResend = (botMessageId: number) => {
// find last user input message and resend
const userIndex = findLastUesrIndex(botMessageId);
if (userIndex === null) return;
setIsLoading(true);
const content = session.messages[userIndex].content;
deleteMessage(userIndex);
chatStore.onUserInput(content).then(() => setIsLoading(false));
inputRef.current?.focus();
};
const config = useChatStore((state) => state.config);
@@ -600,6 +625,13 @@ export function Chat(props: {
const [showPromptModal, setShowPromptModal] = useState(false);
const renameSession = () => {
const newTopic = prompt(Locale.Chat.Rename, session.topic);
if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession((session) => (session.topic = newTopic!));
}
};
// Auto focus
useEffect(() => {
if (props.sideBarShowing && isMobileScreen()) return;
@@ -613,14 +645,7 @@ export function Chat(props: {
<div className={styles["window-header-title"]}>
<div
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
onClickCapture={() => {
const newTopic = prompt(Locale.Chat.Rename, session.topic);
if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession(
(session) => (session.topic = newTopic!),
);
}
}}
onClickCapture={renameSession}
>
{session.topic}
</div>
@@ -639,12 +664,9 @@ export function Chat(props: {
</div>
<div className={styles["window-action-button"]}>
<IconButton
icon={<BrainIcon />}
icon={<RenameIcon />}
bordered
title={Locale.Chat.Actions.CompressedHistory}
onClick={() => {
setShowPromptModal(true);
}}
onClick={renameSession}
/>
</div>
<div className={styles["window-action-button"]}>
@@ -686,6 +708,7 @@ export function Chat(props: {
className={styles["chat-body"]}
ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
onMouseDown={() => inputRef.current?.blur()}
onWheel={(e) => setAutoScroll(hitBottom && e.deltaY > 0)}
onTouchStart={() => {
inputRef.current?.blur();
@@ -723,12 +746,20 @@ export function Chat(props: {
{Locale.Chat.Actions.Stop}
</div>
) : (
<div
className={styles["chat-message-top-action"]}
onClick={() => onResend(i)}
>
{Locale.Chat.Actions.Retry}
</div>
<>
<div
className={styles["chat-message-top-action"]}
onClick={() => onDelete(message.id ?? i)}
>
{Locale.Chat.Actions.Delete}
</div>
<div
className={styles["chat-message-top-action"]}
onClick={() => onResend(message.id ?? i)}
>
{Locale.Chat.Actions.Retry}
</div>
</>
)}
<div

View File

@@ -32,3 +32,63 @@
min-width: 80%;
}
}
.user-prompt-modal {
min-height: 40vh;
.user-prompt-search {
width: 100%;
max-width: 100%;
margin-bottom: 10px;
background-color: var(--gray);
}
.user-prompt-list {
padding: 10px 0;
.user-prompt-item {
margin-bottom: 10px;
widows: 100%;
.user-prompt-header {
display: flex;
widows: 100%;
margin-bottom: 5px;
.user-prompt-title {
flex-grow: 1;
max-width: 100%;
margin-right: 5px;
padding: 5px;
font-size: 12px;
text-align: left;
}
.user-prompt-buttons {
display: flex;
align-items: center;
.user-prompt-button {
height: 100%;
&:not(:last-child) {
margin-right: 5px;
}
}
}
}
.user-prompt-content {
width: 100%;
box-sizing: border-box;
padding: 5px;
margin-right: 10px;
font-size: 12px;
flex-grow: 1;
}
}
}
.user-prompt-actions {
}
}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, HTMLProps } from "react";
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
@@ -6,12 +6,13 @@ import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg";
import CloseIcon from "../icons/close.svg";
import CopyIcon from "../icons/copy.svg";
import ClearIcon from "../icons/clear.svg";
import EditIcon from "../icons/edit.svg";
import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg";
import { List, ListItem, Popover, showToast } from "./ui-lib";
import { Input, List, ListItem, Modal, Popover } from "./ui-lib";
import { IconButton } from "./button";
import {
@@ -26,14 +27,114 @@ import {
import { Avatar } from "./chat";
import Locale, { AllLangs, changeLang, getLang } from "../locales";
import { getEmojiUrl } from "../utils";
import { copyToClipboard, getEmojiUrl } from "../utils";
import Link from "next/link";
import { UPDATE_URL } from "../constant";
import { SearchService, usePromptStore } from "../store/prompt";
import { requestUsage } from "../requests";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
function UserPromptModal(props: { onClose?: () => void }) {
const promptStore = usePromptStore();
const userPrompts = promptStore.getUserPrompts();
const builtinPrompts = SearchService.builtinPrompts;
const allPrompts = userPrompts.concat(builtinPrompts);
const [searchInput, setSearchInput] = useState("");
const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
useEffect(() => {
if (searchInput.length > 0) {
const searchResult = SearchService.search(searchInput);
setSearchPrompts(searchResult);
} else {
setSearchPrompts([]);
}
}, [searchInput]);
return (
<div className="modal-mask">
<Modal
title={Locale.Settings.Prompt.Modal.Title}
onClose={() => props.onClose?.()}
actions={[
<IconButton
key="add"
onClick={() => promptStore.add({ title: "", content: "" })}
icon={<ClearIcon />}
bordered
text={Locale.Settings.Prompt.Modal.Add}
/>,
]}
>
<div className={styles["user-prompt-modal"]}>
<input
type="text"
className={styles["user-prompt-search"]}
placeholder={Locale.Settings.Prompt.Modal.Search}
value={searchInput}
onInput={(e) => setSearchInput(e.currentTarget.value)}
></input>
<div className={styles["user-prompt-list"]}>
{prompts.map((v, _) => (
<div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
<div className={styles["user-prompt-header"]}>
<input
type="text"
className={styles["user-prompt-title"]}
value={v.title}
readOnly={!v.isUser}
onChange={(e) => {
if (v.isUser) {
promptStore.updateUserPrompts(
v.id!,
(prompt) => (prompt.title = e.currentTarget.value),
);
}
}}
></input>
<div className={styles["user-prompt-buttons"]}>
{v.isUser && (
<IconButton
icon={<ClearIcon />}
bordered
className={styles["user-prompt-button"]}
onClick={() => promptStore.remove(v.id!)}
/>
)}
<IconButton
icon={<CopyIcon />}
bordered
className={styles["user-prompt-button"]}
onClick={() => copyToClipboard(v.content)}
/>
</div>
</div>
<Input
rows={2}
value={v.content}
className={styles["user-prompt-content"]}
readOnly={!v.isUser}
onChange={(e) => {
if (v.isUser) {
promptStore.updateUserPrompts(
v.id!,
(prompt) => (prompt.content = e.currentTarget.value),
);
}
}}
/>
</div>
))}
</div>
</div>
</Modal>
</div>
);
}
function SettingItem(props: {
title: string;
subTitle?: string;
@@ -99,18 +200,16 @@ export function Settings(props: { closeSettings: () => void }) {
});
}
const [usage, setUsage] = useState<{
used?: number;
subscription?: number;
}>();
const usage = {
used: updateStore.used,
subscription: updateStore.subscription,
};
const [loadingUsage, setLoadingUsage] = useState(false);
function checkUsage() {
setLoadingUsage(true);
requestUsage()
.then((res) => setUsage(res))
.finally(() => {
setLoadingUsage(false);
});
updateStore.updateUsage().finally(() => {
setLoadingUsage(false);
});
}
const accessStore = useAccessStore();
@@ -122,10 +221,12 @@ export function Settings(props: { closeSettings: () => void }) {
const promptStore = usePromptStore();
const builtinCount = SearchService.count.builtin;
const customCount = promptStore.prompts.size ?? 0;
const customCount = promptStore.getUserPrompts().length ?? 0;
const [shouldShowPromptModal, setShowPromptModal] = useState(false);
const showUsage = accessStore.isAuthorized();
useEffect(() => {
// checks per minutes
checkUpdate();
showUsage && checkUsage();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -469,7 +570,7 @@ export function Settings(props: { closeSettings: () => void }) {
<IconButton
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => showToast(Locale.WIP)}
onClick={() => setShowPromptModal(true)}
/>
</SettingItem>
</List>
@@ -555,6 +656,10 @@ export function Settings(props: { closeSettings: () => void }) {
></InputRange>
</SettingItem>
</List>
{shouldShowPromptModal && (
<UserPromptModal onClose={() => setShowPromptModal(false)} />
)}
</div>
</ErrorBoundary>
);

View File

@@ -53,7 +53,7 @@
box-shadow: var(--card-shadow);
background-color: var(--white);
border-radius: 12px;
width: 50vw;
width: 60vw;
animation: slide-in ease 0.3s;
--modal-padding: 20px;

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8.002766666666666 2) rotate(0 0 4.649916666666667)" d="M0,9.3L0,0 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 7.333333333333333) rotate(0 4 2)" d="M8,0L4,4L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 14) rotate(0 4 0)" d="M8,0L0,0 " /></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 4) rotate(0 4 2)" d="M8,0L4,4L0,0 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 8) rotate(0 4 2)" d="M8,0L4,4L0,0 " /></g></g></svg>

Before

Width:  |  Height:  |  Size: 958 B

After

Width:  |  Height:  |  Size: 736 B

1
app/icons/pause.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.666666666666666 6.666666666666666)" d="M13.33,6.67C13.33,2.98 10.35,0 6.67,0C2.98,0 0,2.98 0,6.67C0,10.35 2.98,13.33 6.67,13.33C10.35,13.33 13.33,10.35 13.33,6.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.333333333333333 6) rotate(0 0 2)" d="M0,0L0,4 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.666666666666666 6) rotate(0 0 2)" d="M0,0L0,4 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
app/icons/rename.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.774903333333333 1.3006199999999999) rotate(0 6.599664999999999 6.599656666666666)" d="M2.83,13.2L13.2,2.83L10.37,0L0,10.37L0,13.2L2.83,13.2Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.317366666666667 4.129066666666667) rotate(0 1.4142166666666658 1.4142166666666665)" d="M0,0L2.83,2.83 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 911 B

View File

@@ -1,34 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="38"
height="38" viewBox="0 0 38 38" fill="none">
<defs>
<filter id="filter_0" x="-4" y="-4" width="38" height="38" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="0" dy="2" />
<feGaussianBlur stdDeviation="2" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_Shadow" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_Shadow" result="shape" />
</filter>
<rect id="path_0" x="0" y="0" width="30" height="30" />
</defs>
<g opacity="1" transform="translate(4 2) rotate(0 15 15)">
<g id="undefined" filter="url(#filter_0)">
<rect stroke="#000000" stroke-width="1" stroke-opacity="0.05" />
<rect x="0" y="0" width="30" height="30" rx="10" />
</g>
<mask id="bg-mask-0" fill="white">
<use xlink:href="#path_0"></use>
</mask>
<g mask="url(#bg-mask-0)">
<g opacity="1" transform="translate(6 4.5) rotate(0 9 11)">
<text>
<tspan x="0" y="16.240000000000002" font-size="14" line-height="0" fill="#000000"
opacity="1" font-family="SourceHanSansCN-Regular" letter-spacing="0">🤣</tspan>
</text>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -17,6 +17,7 @@ const cn = {
Copy: "复制",
Stop: "停止",
Retry: "重试",
Delete: "删除",
},
Rename: "重命名对话",
Typing: "正在输入…",
@@ -37,12 +38,12 @@ const cn = {
MessageFromChatGPT: "来自 ChatGPT 的消息",
},
Memory: {
Title: "历史记忆",
EmptyContent: "尚未记忆",
Send: "发送记忆",
Copy: "复制记忆",
Title: "历史摘要",
EmptyContent: "尚未总结",
Send: "启用总结并发送摘要",
Copy: "复制摘要",
Reset: "重置对话",
ResetConfirm: "重置后将清空当前对话记录以及历史记忆,确认重置?",
ResetConfirm: "重置后将清空当前对话记录以及历史摘要,确认重置?",
},
Home: {
NewChat: "新的聊天",
@@ -58,10 +59,10 @@ const cn = {
ResetAll: "重置所有选项",
Close: "关闭",
ConfirmResetAll: {
Confirm: "Are you sure you want to reset all configurations?",
Confirm: "确认清除所有配置?",
},
ConfirmClearAll: {
Confirm: "Are you sure you want to reset all chat?",
Confirm: "确认清除所有聊天记录?",
},
},
Lang: {
@@ -74,6 +75,7 @@ const cn = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "头像",
@@ -103,6 +105,11 @@ const cn = {
ListCount: (builtin: number, custom: number) =>
`内置 ${builtin} 条,用户定义 ${custom}`,
Edit: "编辑",
Modal: {
Title: "提示词列表",
Add: "增加一条",
Search: "搜索提示词",
},
},
HistoryCount: {
Title: "附带历史消息数",

189
app/locales/de.ts Normal file
View File

@@ -0,0 +1,189 @@
import { SubmitKey } from "../store/app";
import type { LocaleType } from "./index";
const de: LocaleType = {
WIP: "In Bearbeitung...",
Error: {
Unauthorized:
"Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der Einstellungsseite ein.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} Nachrichten`,
},
Chat: {
SubTitle: (count: number) => `${count} Nachrichten mit ChatGPT`,
Actions: {
ChatList: "Zur Chat-Liste gehen",
CompressedHistory: "Komprimierter Gedächtnis-Prompt",
Export: "Alle Nachrichten als Markdown exportieren",
Copy: "Kopieren",
Stop: "Stop",
Retry: "Wiederholen",
Delete: "Delete",
},
Rename: "Chat umbenennen",
Typing: "Tippen...",
Input: (submitKey: string) => {
var inputHints = `${submitKey} um zu Senden`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Umschalt + Eingabe für Zeilenumbruch";
}
return inputHints + ", / zum Durchsuchen von Prompts";
},
Send: "Senden",
},
Export: {
Title: "Alle Nachrichten",
Copy: "Alles kopieren",
Download: "Herunterladen",
MessageFromYou: "Deine Nachricht",
MessageFromChatGPT: "Nachricht von ChatGPT",
},
Memory: {
Title: "Gedächtnis-Prompt",
EmptyContent: "Noch nichts.",
Send: "Gedächtnis senden",
Copy: "Gedächtnis kopieren",
Reset: "Sitzung zurücksetzen",
ResetConfirm:
"Das Zurücksetzen löscht den aktuellen Gesprächsverlauf und das Langzeit-Gedächtnis. Möchten Sie wirklich zurücksetzen?",
},
Home: {
NewChat: "Neuer Chat",
DeleteChat: "Bestätigen Sie, um das ausgewählte Gespräch zu löschen?",
DeleteToast: "Chat gelöscht",
Revert: "Zurücksetzen",
},
Settings: {
Title: "Einstellungen",
SubTitle: "Alle Einstellungen",
Actions: {
ClearAll: "Alle Daten löschen",
ResetAll: "Alle Einstellungen zurücksetzen",
Close: "Schließen",
ConfirmResetAll: {
Confirm: "Möchten Sie wirklich alle Konfigurationen zurücksetzen?",
},
ConfirmClearAll: {
Confirm: "Möchten Sie wirklich alle Chats zurücksetzen?",
},
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Schriftgröße",
SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
},
Update: {
Version: (x: string) => `Version: ${x}`,
IsLatest: "Neueste Version",
CheckUpdate: "Update prüfen",
IsChecking: "Update wird geprüft...",
FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`,
GoToUpdate: "Aktualisieren",
},
SendKey: "Senden-Taste",
Theme: "Erscheinungsbild",
TightBorder: "Enger Rahmen",
SendPreviewBubble: "Vorschau-Bubble senden",
Prompt: {
Disable: {
Title: "Autovervollständigung deaktivieren",
SubTitle: "Autovervollständigung mit / starten",
},
List: "Prompt-Liste",
ListCount: (builtin: number, custom: number) =>
`${builtin} integriert, ${custom} benutzerdefiniert`,
Edit: "Bearbeiten",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
},
HistoryCount: {
Title: "Anzahl der angehängten Nachrichten",
SubTitle: "Anzahl der pro Anfrage angehängten gesendeten Nachrichten",
},
CompressThreshold: {
Title: "Schwellenwert für Verlaufskomprimierung",
SubTitle:
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
},
Token: {
Title: "API-Schlüssel",
SubTitle:
"Verwenden Sie Ihren Schlüssel, um das Zugangscode-Limit zu ignorieren",
Placeholder: "OpenAI API-Schlüssel",
},
Usage: {
Title: "Kontostand",
SubTitle(used: any, total: any) {
return `Diesen Monat ausgegeben $${used}, Abonnement $${total}`;
},
IsChecking: "Wird überprüft...",
Check: "Erneut prüfen",
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
},
AccessCode: {
Title: "Zugangscode",
SubTitle: "Zugangskontrolle aktiviert",
Placeholder: "Zugangscode erforderlich",
},
Model: "Modell",
Temperature: {
Title: "Temperature", //Temperatur
SubTitle: "Ein größerer Wert führt zu zufälligeren Antworten",
},
MaxTokens: {
Title: "Max Tokens", //Maximale Token
SubTitle: "Maximale Anzahl der Anfrage- plus Antwort-Token",
},
PresencePenlty: {
Title: "Presence Penalty", //Anwesenheitsstrafe
SubTitle:
"Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird",
},
},
Store: {
DefaultTopic: "Neues Gespräch",
BotHello: "Hallo! Wie kann ich Ihnen heute helfen?",
Error:
"Etwas ist schief gelaufen, bitte versuchen Sie es später noch einmal.",
Prompt: {
History: (content: string) =>
"Dies ist eine Zusammenfassung des Chatverlaufs zwischen dem KI und dem Benutzer als Rückblick: " +
content,
Topic:
"Bitte erstellen Sie einen vier- bis fünfwörtigen Titel, der unser Gespräch zusammenfasst, ohne Einleitung, Zeichensetzung, Anführungszeichen, Punkte, Symbole oder zusätzlichen Text. Entfernen Sie Anführungszeichen.",
Summarize:
"Fassen Sie unsere Diskussion kurz in 200 Wörtern oder weniger zusammen, um sie als Pronpt für zukünftige Gespräche zu verwenden.",
},
ConfirmClearAll:
"Bestätigen Sie, um alle Chat- und Einstellungsdaten zu löschen?",
},
Copy: {
Success: "In die Zwischenablage kopiert",
Failed:
"Kopieren fehlgeschlagen, bitte geben Sie die Berechtigung zum Zugriff auf die Zwischenablage frei",
},
Context: {
Toast: (x: any) => `Mit ${x} Kontext-Prompts`,
Edit: "Kontext- und Gedächtnis-Prompts",
Add: "Hinzufügen",
},
};
export default de;

View File

@@ -19,6 +19,7 @@ const en: LocaleType = {
Copy: "Copy",
Stop: "Stop",
Retry: "Retry",
Delete: "Delete",
},
Rename: "Rename Chat",
Typing: "Typing…",
@@ -77,6 +78,7 @@ const en: LocaleType = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -105,6 +107,11 @@ const en: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Edit",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
},
HistoryCount: {
Title: "Attached Messages Count",
@@ -126,7 +133,7 @@ const en: LocaleType = {
return `Used this month $${used}, subscription $${total}`;
},
IsChecking: "Checking...",
Check: "Check Again",
Check: "Check",
NoAccess: "Enter API Key to check balance",
},
AccessCode: {

View File

@@ -19,6 +19,7 @@ const es: LocaleType = {
Copy: "Copiar",
Stop: "Detener",
Retry: "Reintentar",
Delete: "Delete",
},
Rename: "Renombrar chat",
Typing: "Escribiendo...",
@@ -77,6 +78,7 @@ const es: LocaleType = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -105,6 +107,11 @@ const es: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} incorporado, ${custom} definido por el usuario`,
Edit: "Editar",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
},
HistoryCount: {
Title: "Cantidad de mensajes adjuntos",

View File

@@ -5,10 +5,20 @@ import ES from "./es";
import IT from "./it";
import TR from "./tr";
import JP from "./jp";
import DE from "./de";
export type { LocaleType } from "./cn";
export const AllLangs = ["en", "cn", "tw", "es", "it", "tr", "jp"] as const;
export const AllLangs = [
"en",
"cn",
"tw",
"es",
"it",
"tr",
"jp",
"de",
] as const;
type Lang = (typeof AllLangs)[number];
const LANG_KEY = "lang";
@@ -44,21 +54,13 @@ export function getLang(): Lang {
const lang = getLanguage();
if (lang.includes("zh") || lang.includes("cn")) {
return "cn";
} else if (lang.includes("tw")) {
return "tw";
} else if (lang.includes("es")) {
return "es";
} else if (lang.includes("it")) {
return "it";
} else if (lang.includes("tr")) {
return "tr";
} else if (lang.includes("jp")) {
return "jp";
} else {
return "en";
for (const option of AllLangs) {
if (lang.includes(option)) {
return option;
}
}
return "en";
}
export function changeLang(lang: Lang) {
@@ -66,6 +68,13 @@ export function changeLang(lang: Lang) {
location.reload();
}
export default { en: EN, cn: CN, tw: TW, es: ES, it: IT, tr: TR, jp: JP }[
getLang()
];
export default {
en: EN,
cn: CN,
tw: TW,
es: ES,
it: IT,
tr: TR,
jp: JP,
de: DE,
}[getLang()] as typeof CN;

View File

@@ -19,6 +19,7 @@ const it: LocaleType = {
Copy: "Copia",
Stop: "Stop",
Retry: "Riprova",
Delete: "Delete",
},
Rename: "Rinomina Chat",
Typing: "Typing…",
@@ -45,12 +46,12 @@ const it: LocaleType = {
Send: "Send Memory",
Reset: "Reset Session",
ResetConfirm:
"Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
"Ripristinare cancellerà la conversazione corrente e la cronologia di memoria. Sei sicuro che vuoi riavviare?",
},
Home: {
NewChat: "Nuova Chat",
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
DeleteToast: "Chat Deleted",
DeleteToast: "Chat Cancellata",
Revert: "Revert",
},
Settings: {
@@ -77,6 +78,7 @@ const it: LocaleType = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -93,9 +95,9 @@ const it: LocaleType = {
GoToUpdate: "Aggiorna",
},
SendKey: "Tasto invia",
Theme: "tema",
TightBorder: "Bordi stretti",
SendPreviewBubble: "Invia l'anteprima della bolla",
Theme: "Tema",
TightBorder: "Schermo intero",
SendPreviewBubble: "Anteprima di digitazione",
Prompt: {
Disable: {
Title: "Disabilita l'auto completamento",
@@ -105,6 +107,11 @@ const it: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Modifica",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
},
HistoryCount: {
Title: "Conteggio dei messaggi allegati",
@@ -116,7 +123,7 @@ const it: LocaleType = {
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
},
Token: {
Title: "Chiave API",
Title: "API Key",
SubTitle:
"Utilizzare la chiave per ignorare il limite del codice di accesso",
Placeholder: "OpenAI API Key",
@@ -124,7 +131,7 @@ const it: LocaleType = {
Usage: {
Title: "Bilancio Account",
SubTitle(used: any, total: any) {
return `Usato in questo mese $${used}, subscription $${total}`;
return `Attualmente usato in questo mese $${used}, soglia massima $${total}`;
},
IsChecking: "Controllando...",
Check: "Controlla ancora",

View File

@@ -18,6 +18,7 @@ const jp = {
Copy: "コピー",
Stop: "停止",
Retry: "リトライ",
Delete: "Delete",
},
Rename: "チャットの名前を変更",
Typing: "入力中…",
@@ -76,6 +77,7 @@ const jp = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "アバター",
@@ -106,6 +108,11 @@ const jp = {
ListCount: (builtin: number, custom: number) =>
`組み込み ${builtin} 件、ユーザー定義 ${custom}`,
Edit: "編集",
Modal: {
Title: "プロンプトリスト",
Add: "新規追加",
Search: "プロンプトワード検索",
},
},
HistoryCount: {
Title: "履歴メッセージ数を添付",
@@ -177,6 +184,4 @@ const jp = {
},
};
export type LocaleType = typeof jp;
export default jp;

View File

@@ -19,6 +19,7 @@ const tr: LocaleType = {
Copy: "Kopyala",
Stop: "Durdur",
Retry: "Tekrar Dene",
Delete: "Delete",
},
Rename: "Sohbeti Yeniden Adlandır",
Typing: "Yazıyor…",
@@ -77,6 +78,7 @@ const tr: LocaleType = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -105,6 +107,11 @@ const tr: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} yerleşik, ${custom} kullanıcı tanımlı`,
Edit: "Düzenle",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
},
HistoryCount: {
Title: "Ekli Mesaj Sayısı",

View File

@@ -18,6 +18,7 @@ const tw: LocaleType = {
Copy: "複製",
Stop: "停止",
Retry: "重試",
Delete: "刪除",
},
Rename: "重命名對話",
Typing: "正在輸入…",
@@ -75,6 +76,7 @@ const tw: LocaleType = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "大頭貼",
@@ -103,6 +105,11 @@ const tw: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`內置 ${builtin} 條,用戶定義 ${custom}`,
Edit: "編輯",
Modal: {
Title: "提示詞列表",
Add: "增加一條",
Search: "搜尋提示詞",
},
},
HistoryCount: {
Title: "附帶歷史訊息數",
@@ -152,9 +159,10 @@ const tw: LocaleType = {
Prompt: {
History: (content: string) =>
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
Topic:
"Use the language used by the user (e.g. en for english conversation, zh-hant for chinese conversation, etc.) to generate a title (at most 6 words) summarizing our conversation without any lead-in, quotation marks, preamble like 'Title:', direct text copies, single-word replies, quotation marks, translations, or brackets. Remove enclosing quotation marks. The title should make third-party grasp the essence of the conversation in first sight.",
Summarize:
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt且字數控制在 200 字以內",
"Use the language used by the user (e.g. en-us for english conversation, zh-hant for chinese conversation, etc.) to summarise the conversation in at most 200 words. The summary will be used as prompt for you to continue the conversation in the future.",
},
ConfirmClearAll: "確認清除所有對話、設定數據?",
},

View File

@@ -2,7 +2,7 @@ import type { ChatRequest, ChatResponse } from "./api/openai/typing";
import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
import { showToast } from "./components/ui-lib";
const TIME_OUT_MS = 30000;
const TIME_OUT_MS = 60000;
const makeRequestParam = (
messages: Message[],
@@ -114,6 +114,10 @@ export async function requestUsage() {
response.total_usage = Math.round(response.total_usage) / 100;
}
if (total.hard_limit_usd) {
total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
}
return {
used: response.total_usage,
subscription: total.hard_limit_usd,
@@ -167,15 +171,14 @@ export async function requestChatStream(
options?.onController?.(controller);
while (true) {
// handle time out, will stop if no response in 10 secs
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
const content = await reader?.read();
clearTimeout(resTimeoutId);
if (!content || !content.value) {
break;
}
const text = decoder.decode(content.value, { stream: true });
responseText += text;
@@ -235,6 +238,14 @@ export const ControllerPool = {
controller?.abort();
},
stopAll() {
Object.values(this.controllers).forEach((v) => v.abort());
},
hasPending() {
return Object.values(this.controllers).length > 0;
},
remove(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
delete this.controllers[key];

View File

@@ -386,6 +386,7 @@ export const useChatStore = create<ChatStore>()(
const botMessage: Message = createMessage({
role: "assistant",
streaming: true,
id: userMessage.id! + 1,
});
// get recent messages
@@ -421,7 +422,7 @@ export const useChatStore = create<ChatStore>()(
onError(error, statusCode) {
if (statusCode === 401) {
botMessage.content = Locale.Error.Unauthorized;
} else {
} else if (!error.message.includes("aborted")) {
botMessage.content += "\n\n" + Locale.Store.Error;
}
botMessage.streaming = false;
@@ -461,6 +462,7 @@ export const useChatStore = create<ChatStore>()(
const context = session.context.slice();
// long term memory
if (
session.sendMemory &&
session.memoryPrompt &&
@@ -470,9 +472,33 @@ export const useChatStore = create<ChatStore>()(
context.push(memoryPrompt);
}
const recentMessages = context.concat(
messages.slice(Math.max(0, n - config.historyMessageCount)),
// get short term and unmemoried long term memory
const shortTermMemoryMessageIndex = Math.max(
0,
n - config.historyMessageCount,
);
const longTermMemoryMessageIndex = session.lastSummarizeIndex;
const oldestIndex = Math.max(
shortTermMemoryMessageIndex,
longTermMemoryMessageIndex,
);
const threshold = config.compressMessageLengthThreshold;
// get recent messages as many as possible
const reversedRecentMessages = [];
for (
let i = n - 1, count = 0;
i >= oldestIndex && count < threshold;
i -= 1
) {
const msg = messages[i];
if (!msg || msg.isError) continue;
count += msg.content.length;
reversedRecentMessages.push(msg);
}
// concat
const recentMessages = context.concat(reversedRecentMessages.reverse());
return recentMessages;
},
@@ -541,7 +567,10 @@ export const useChatStore = create<ChatStore>()(
config.compressMessageLengthThreshold,
);
if (historyMsgLength > config.compressMessageLengthThreshold) {
if (
historyMsgLength > config.compressMessageLengthThreshold &&
session.sendMemory
) {
requestChatStream(
toBeSummarizedMsgs.concat({
role: "system",

View File

@@ -5,62 +5,74 @@ import { getLang } from "../locales";
export interface Prompt {
id?: number;
isUser?: boolean;
title: string;
content: string;
}
export interface PromptStore {
counter: number;
latestId: number;
prompts: Map<number, Prompt>;
prompts: Record<number, Prompt>;
add: (prompt: Prompt) => number;
remove: (id: number) => void;
search: (text: string) => Prompt[];
getUserPrompts: () => Prompt[];
updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void;
}
export const PROMPT_KEY = "prompt-store";
export const SearchService = {
ready: false,
engine: new Fuse<Prompt>([], { keys: ["title"] }),
builtinEngine: new Fuse<Prompt>([], { keys: ["title"] }),
userEngine: new Fuse<Prompt>([], { keys: ["title"] }),
count: {
builtin: 0,
},
allBuiltInPrompts: [] as Prompt[],
allPrompts: [] as Prompt[],
builtinPrompts: [] as Prompt[],
init(prompts: Prompt[]) {
init(builtinPrompts: Prompt[], userPrompts: Prompt[]) {
if (this.ready) {
return;
}
this.allBuiltInPrompts = prompts;
this.engine.setCollection(prompts);
this.allPrompts = userPrompts.concat(builtinPrompts);
this.builtinPrompts = builtinPrompts.slice();
this.builtinEngine.setCollection(builtinPrompts);
this.userEngine.setCollection(userPrompts);
this.ready = true;
},
remove(id: number) {
this.engine.remove((doc) => doc.id === id);
this.userEngine.remove((doc) => doc.id === id);
},
add(prompt: Prompt) {
this.engine.add(prompt);
this.userEngine.add(prompt);
},
search(text: string) {
const results = this.engine.search(text);
return results.map((v) => v.item);
const userResults = this.userEngine.search(text);
const builtinResults = this.builtinEngine.search(text);
return userResults.concat(builtinResults).map((v) => v.item);
},
};
export const usePromptStore = create<PromptStore>()(
persist(
(set, get) => ({
counter: 0,
latestId: 0,
prompts: new Map(),
prompts: {},
add(prompt) {
const prompts = get().prompts;
prompt.id = get().latestId + 1;
prompts.set(prompt.id, prompt);
prompt.isUser = true;
prompts[prompt.id] = prompt;
set(() => ({
latestId: prompt.id!,
@@ -72,19 +84,40 @@ export const usePromptStore = create<PromptStore>()(
remove(id) {
const prompts = get().prompts;
prompts.delete(id);
delete prompts[id];
SearchService.remove(id);
set(() => ({
prompts,
counter: get().counter + 1,
}));
},
getUserPrompts() {
const userPrompts = Object.values(get().prompts ?? {});
userPrompts.sort((a, b) => (b.id && a.id ? b.id - a.id : 0));
return userPrompts;
},
updateUserPrompts(id: number, updater) {
const prompt = get().prompts[id] ?? {
title: "",
content: "",
id,
};
SearchService.remove(id);
updater(prompt);
const prompts = get().prompts;
prompts[id] = prompt;
set(() => ({ prompts }));
SearchService.add(prompt);
},
search(text) {
if (text.length === 0) {
// return all prompts
const userPrompts = get().prompts?.values?.() ?? [];
return SearchService.allBuiltInPrompts.concat([...userPrompts]);
// return all rompts
return SearchService.allPrompts.concat([...get().getUserPrompts()]);
}
return SearchService.search(text) as Prompt[];
},
@@ -104,24 +137,27 @@ export const usePromptStore = create<PromptStore>()(
if (getLang() === "cn") {
fetchPrompts = fetchPrompts.reverse();
}
const builtinPrompts = fetchPrompts
.map((promptList: PromptList) => {
const builtinPrompts = fetchPrompts.map(
(promptList: PromptList) => {
return promptList.map(
([title, content]) =>
({
id: Math.random(),
title,
content,
} as Prompt),
);
})
.concat([...(state?.prompts?.values() ?? [])]);
const allPromptsForSearch = builtinPrompts.reduce(
(pre, cur) => pre.concat(cur),
[],
},
);
const userPrompts =
usePromptStore.getState().getUserPrompts() ?? [];
const allPromptsForSearch = builtinPrompts
.reduce((pre, cur) => pre.concat(cur), [])
.filter((v) => !!v.title && !!v.content);
SearchService.count.builtin = res.en.length + res.cn.length;
SearchService.init(allPromptsForSearch);
SearchService.init(allPromptsForSearch, userPrompts);
});
},
},

View File

@@ -1,13 +1,19 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
import { requestUsage } from "../requests";
export interface UpdateStore {
lastUpdate: number;
remoteVersion: string;
used?: number;
subscription?: number;
lastUpdateUsage: number;
version: string;
getLatestVersion: (force: boolean) => Promise<string>;
getLatestVersion: (force?: boolean) => Promise<void>;
updateUsage: (force?: boolean) => Promise<void>;
}
export const UPDATE_KEY = "chat-update";
@@ -26,22 +32,27 @@ function queryMeta(key: string, defaultValue?: string): string {
return ret;
}
const ONE_MINUTE = 60 * 1000;
export const useUpdateStore = create<UpdateStore>()(
persist(
(set, get) => ({
lastUpdate: 0,
remoteVersion: "",
lastUpdateUsage: 0,
version: "unknown",
async getLatestVersion(force = false) {
set(() => ({ version: queryMeta("version") }));
set(() => ({ version: queryMeta("version") ?? "unknown" }));
const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
const shouldFetch = force || overTenMins;
if (!shouldFetch) {
return get().version ?? "unknown";
}
const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
if (!force && !overTenMins) return;
set(() => ({
lastUpdate: Date.now(),
}));
try {
// const data = await (await fetch(FETCH_TAG_URL)).json();
@@ -49,14 +60,26 @@ export const useUpdateStore = create<UpdateStore>()(
const data = await (await fetch(FETCH_COMMIT_URL)).json();
const remoteId = (data[0].sha as string).substring(0, 7);
set(() => ({
lastUpdate: Date.now(),
remoteVersion: remoteId,
}));
console.log("[Got Upstream] ", remoteId);
return remoteId;
} catch (error) {
console.error("[Fetch Upstream Commit Id]", error);
return get().version ?? "";
}
},
async updateUsage(force = false) {
const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
if (!overOneMinute && !force) return;
set(() => ({
lastUpdateUsage: Date.now(),
}));
const usage = await requestUsage();
if (usage) {
set(() => usage);
}
},
}),

View File

@@ -140,6 +140,7 @@ label {
input {
text-align: center;
font-family: inherit;
}
input[type="checkbox"] {
@@ -224,6 +225,7 @@ input[type="password"] {
color: var(--black);
padding: 0 10px;
max-width: 50%;
font-family: inherit;
}
div.math {