mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-26 10:39:21 +08:00
Merge branch 'main' into dev
This commit is contained in:
@@ -9,10 +9,17 @@ export async function requestOpenai(req: NextRequest) {
|
||||
const apiKey = req.headers.get("token");
|
||||
const openaiPath = req.headers.get("path");
|
||||
|
||||
let baseUrl = BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `${PROTOCOL}://${baseUrl}`;
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", openaiPath);
|
||||
console.log("[apiKey] ", apiKey);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, {
|
||||
return fetch(`${baseUrl}/${openaiPath}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
@import "../styles/animation.scss";
|
||||
|
||||
.chat-input-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.chat-input-action {
|
||||
display: inline-flex;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
border: var(--border-in-light);
|
||||
padding: 4px 10px;
|
||||
animation: slide-in ease 0.3s;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: all ease 0.3s;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-toast {
|
||||
position: absolute;
|
||||
bottom: -50px;
|
||||
|
||||
@@ -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 MenuIcon from "../icons/menu.svg";
|
||||
import CopyIcon from "../icons/copy.svg";
|
||||
@@ -14,6 +15,12 @@ import DeleteIcon from "../icons/delete.svg";
|
||||
import MaxIcon from "../icons/max.svg";
|
||||
import MinIcon from "../icons/min.svg";
|
||||
|
||||
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,
|
||||
SubmitKey,
|
||||
@@ -22,6 +29,7 @@ import {
|
||||
ROLES,
|
||||
createMessage,
|
||||
useAccessStore,
|
||||
Theme,
|
||||
} from "../store";
|
||||
|
||||
import {
|
||||
@@ -60,7 +68,11 @@ export function Avatar(props: { role: Message["role"] }) {
|
||||
const config = useChatStore((state) => state.config);
|
||||
|
||||
if (props.role !== "user") {
|
||||
return <BotIcon className={styles["user-avtar"]} />;
|
||||
return (
|
||||
<div className="no-dark">
|
||||
<BotIcon className={styles["user-avtar"]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -316,22 +328,90 @@ function useScrollToBottom() {
|
||||
// for auto-scroll
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const scrollToBottom = () => {
|
||||
const dom = scrollRef.current;
|
||||
if (dom) {
|
||||
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
||||
}
|
||||
};
|
||||
|
||||
// auto scroll
|
||||
useLayoutEffect(() => {
|
||||
const dom = scrollRef.current;
|
||||
if (dom && autoScroll) {
|
||||
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
||||
}
|
||||
autoScroll && scrollToBottom();
|
||||
});
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
autoScroll,
|
||||
setAutoScroll,
|
||||
scrollToBottom,
|
||||
};
|
||||
}
|
||||
|
||||
export function ChatActions(props: {
|
||||
showPromptModal: () => void;
|
||||
scrollToBottom: () => void;
|
||||
hitBottom: boolean;
|
||||
}) {
|
||||
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);
|
||||
const nextIndex = (themeIndex + 1) % themes.length;
|
||||
const nextTheme = themes[nextIndex];
|
||||
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`}
|
||||
onClick={props.scrollToBottom}
|
||||
>
|
||||
<BottomIcon />
|
||||
</div>
|
||||
)}
|
||||
{props.hitBottom && (
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
onClick={props.showPromptModal}
|
||||
>
|
||||
<BrainIcon />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
onClick={nextTheme}
|
||||
>
|
||||
{theme === Theme.Auto ? (
|
||||
<AutoIcon />
|
||||
) : theme === Theme.Light ? (
|
||||
<LightIcon />
|
||||
) : theme === Theme.Dark ? (
|
||||
<DarkIcon />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Chat(props: {
|
||||
showSideBar?: () => void;
|
||||
sideBarShowing?: boolean;
|
||||
@@ -357,11 +437,10 @@ export function Chat(props: {
|
||||
const [beforeInput, setBeforeInput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||
const { scrollRef, setAutoScroll } = useScrollToBottom();
|
||||
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
||||
const [hitBottom, setHitBottom] = useState(false);
|
||||
|
||||
const onChatBodyScroll = (e: HTMLElement) => {
|
||||
setAutoScroll(false);
|
||||
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
||||
setHitBottom(isTouchBottom);
|
||||
};
|
||||
@@ -383,16 +462,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(
|
||||
@@ -417,7 +486,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;
|
||||
|
||||
@@ -475,21 +543,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);
|
||||
@@ -541,6 +632,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;
|
||||
@@ -554,14 +652,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>
|
||||
@@ -578,6 +669,7 @@ export function Chat(props: {
|
||||
onClick={props?.showSideBar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isMobileScreen() && (
|
||||
<div className={styles["window-action-button"]}>
|
||||
<IconButton
|
||||
@@ -590,6 +682,15 @@ export function Chat(props: {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles["window-action-button"]}>
|
||||
<IconButton
|
||||
icon={<RenameIcon />}
|
||||
bordered
|
||||
onClick={renameSession}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles["window-action-button"]}>
|
||||
<IconButton
|
||||
icon={<ExportIcon />}
|
||||
@@ -678,12 +779,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
|
||||
@@ -694,22 +803,20 @@ export function Chat(props: {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(message.preview || message.content.length === 0) &&
|
||||
!isUser ? (
|
||||
<LoadingIcon />
|
||||
) : (
|
||||
<div
|
||||
className="markdown-body"
|
||||
style={{ fontSize: `${fontSize}px` }}
|
||||
onContextMenu={(e) => onRightClick(e, message)}
|
||||
onDoubleClickCapture={() => {
|
||||
if (!isMobileScreen()) return;
|
||||
setUserInput(message.content);
|
||||
}}
|
||||
>
|
||||
<Markdown content={message.content} />
|
||||
</div>
|
||||
)}
|
||||
<Markdown
|
||||
content={message.content}
|
||||
loading={
|
||||
(message.preview || message.content.length === 0) &&
|
||||
!isUser
|
||||
}
|
||||
onContextMenu={(e) => onRightClick(e, message)}
|
||||
onDoubleClickCapture={() => {
|
||||
if (!isMobileScreen()) return;
|
||||
setUserInput(message.content);
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
parentRef={scrollRef}
|
||||
/>
|
||||
</div>
|
||||
{!isUser && !message.preview && (
|
||||
<div className={styles["chat-message-actions"]}>
|
||||
@@ -726,6 +833,12 @@ export function Chat(props: {
|
||||
|
||||
<div className={styles["chat-input-panel"]}>
|
||||
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
||||
|
||||
<ChatActions
|
||||
showPromptModal={() => setShowPromptModal(true)}
|
||||
scrollToBottom={scrollToBottom}
|
||||
hitBottom={hitBottom}
|
||||
/>
|
||||
<div className={styles["chat-input-panel-inner"]}>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
max-height: var(--full-height);
|
||||
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +231,7 @@
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
padding-bottom: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -354,11 +356,16 @@
|
||||
}
|
||||
|
||||
.chat-input-panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
padding-top: 5px;
|
||||
padding-top: 10px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border-top: var(--border-in-light);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
@mixin single-line {
|
||||
|
||||
@@ -17,7 +17,7 @@ import LoadingIcon from "../icons/three-dots.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
|
||||
import { useChatStore } from "../store";
|
||||
import { isMobileScreen } from "../utils";
|
||||
import { getCSSVar, isMobileScreen } from "../utils";
|
||||
import Locale from "../locales";
|
||||
import { Chat } from "./chat";
|
||||
|
||||
@@ -66,9 +66,7 @@ function useSwitchTheme() {
|
||||
metaDescriptionDark?.setAttribute("content", "#151515");
|
||||
metaDescriptionLight?.setAttribute("content", "#fafafa");
|
||||
} else {
|
||||
const themeColor = getComputedStyle(document.body)
|
||||
.getPropertyValue("--theme-color")
|
||||
.trim();
|
||||
const themeColor = getCSSVar("--themeColor");
|
||||
metaDescriptionDark?.setAttribute("content", themeColor);
|
||||
metaDescriptionLight?.setAttribute("content", themeColor);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import RehypeHighlight from "rehype-highlight";
|
||||
import { useRef, useState, RefObject, useEffect } from "react";
|
||||
import { copyToClipboard } from "../utils";
|
||||
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
|
||||
export function PreCode(props: { children: any }) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
|
||||
@@ -27,49 +29,78 @@ export function PreCode(props: { children: any }) {
|
||||
);
|
||||
}
|
||||
|
||||
const useLazyLoad = (ref: RefObject<Element>): boolean => {
|
||||
const [isIntersecting, setIntersecting] = useState<boolean>(false);
|
||||
export function Markdown(
|
||||
props: {
|
||||
content: string;
|
||||
loading?: boolean;
|
||||
fontSize?: number;
|
||||
parentRef: RefObject<HTMLDivElement>;
|
||||
} & React.DOMAttributes<HTMLDivElement>,
|
||||
) {
|
||||
const mdRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const parent = props.parentRef.current;
|
||||
const md = mdRef.current;
|
||||
const rendered = useRef(true); // disable lazy loading for bad ux
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIntersecting(true);
|
||||
observer.disconnect();
|
||||
// to triggr rerender
|
||||
setCounter(counter + 1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.loading]);
|
||||
|
||||
const inView =
|
||||
rendered.current ||
|
||||
(() => {
|
||||
if (parent && md) {
|
||||
const parentBounds = parent.getBoundingClientRect();
|
||||
const mdBounds = md.getBoundingClientRect();
|
||||
const isInRange = (x: number) =>
|
||||
x <= parentBounds.bottom && x >= parentBounds.top;
|
||||
const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
|
||||
|
||||
if (inView) {
|
||||
rendered.current = true;
|
||||
}
|
||||
|
||||
return inView;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
const shouldLoading = props.loading || !inView;
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return isIntersecting;
|
||||
};
|
||||
|
||||
export function Markdown(props: { content: string }) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
[
|
||||
RehypeHighlight,
|
||||
{
|
||||
detect: false,
|
||||
ignoreMissing: true,
|
||||
},
|
||||
],
|
||||
]}
|
||||
components={{
|
||||
pre: PreCode,
|
||||
}}
|
||||
linkTarget={"_blank"}
|
||||
<div
|
||||
className="markdown-body"
|
||||
style={{ fontSize: `${props.fontSize ?? 14}px` }}
|
||||
ref={mdRef}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDoubleClickCapture={props.onDoubleClickCapture}
|
||||
>
|
||||
{props.content}
|
||||
</ReactMarkdown>
|
||||
{shouldLoading ? (
|
||||
<LoadingIcon />
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
[
|
||||
RehypeHighlight,
|
||||
{
|
||||
detect: false,
|
||||
ignoreMissing: true,
|
||||
},
|
||||
],
|
||||
]}
|
||||
components={{
|
||||
pre: PreCode,
|
||||
}}
|
||||
linkTarget={"_blank"}
|
||||
>
|
||||
{props.content}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -470,7 +571,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
text={Locale.Settings.Prompt.Edit}
|
||||
onClick={() => showToast(Locale.WIP)}
|
||||
onClick={() => setShowPromptModal(true)}
|
||||
/>
|
||||
</SettingItem>
|
||||
</List>
|
||||
@@ -556,6 +657,10 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
></InputRange>
|
||||
</SettingItem>
|
||||
</List>
|
||||
|
||||
{shouldShowPromptModal && (
|
||||
<UserPromptModal onClose={() => setShowPromptModal(false)} />
|
||||
)}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
11
app/global.d.ts
vendored
Normal file
11
app/global.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
declare module "*.jpg";
|
||||
declare module "*.png";
|
||||
declare module "*.woff2";
|
||||
declare module "*.woff";
|
||||
declare module "*.ttf";
|
||||
declare module "*.scss" {
|
||||
const content: Record<string, string>;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module "*.svg";
|
||||
1
app/icons/auto.svg
Normal file
1
app/icons/auto.svg
Normal 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; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.666666666666666 5.333333333333333) rotate(0 2.333750009536743 2.6666666666666665)" d="M0 5.33667L0.73 3.66667 M4.6675 5.33667L3.9375 3.66667 M0.729167 3.67L2.32917 0L3.93917 3.67 M0.729167 3.66667L3.93917 3.66667 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.533316666666666 2.6666666666666665)" d="M13.07,5.33C12.45,2.29 9.76,0 6.53,0C3.31,0 0.62,2.29 0,5.33L2,4.67 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 9.333333333333332) rotate(0 6.533316666666666 2.6666666666666665)" d="M0,0C0.62,3.04 3.31,5.33 6.53,5.33C9.76,5.33 12.45,3.04 13.07,0L11.33,0.67 " /></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
app/icons/bottom.svg
Normal file
1
app/icons/bottom.svg
Normal 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(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>
|
||||
|
After Width: | Height: | Size: 736 B |
1
app/icons/dark.svg
Normal file
1
app/icons/dark.svg
Normal 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.333333333333485) rotate(0 6.666666666666666 6.666666666666666)" d="M6.67,0L4.91,1.76L1.76,1.76L1.76,4.91L0,6.67L1.76,8.42L1.76,11.58L4.91,11.58L6.67,13.33L8.42,11.58L11.58,11.58L11.58,8.42L13.33,6.67L11.58,4.91L11.58,1.76L8.42,1.76L6.67,0Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.666666666666666 5.44771525016904) rotate(0 2.4732087011352872 2.442809041582063)" d="M4,0.55C2.17,-0.78 0,0.55 0,1.89C1.67,1.89 3.33,2.22 3.33,4.89C4.67,4.89 5.83,1.89 4,0.55Z " /></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
app/icons/light.svg
Normal file
1
app/icons/light.svg
Normal 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(4.340166666666667 4.21550000000002) rotate(0 3.6666666666666665 3.666666666666666)" d="M0,3.67C0,5.69 1.64,7.33 3.67,7.33C5.69,7.33 7.33,5.69 7.33,3.67C7.33,1.64 5.69,0 3.67,0C1.64,0 0,1.64 0,3.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(12.166666666666666 12.1719333333333) rotate(0 0.4100499999999994 0.41240499999999997)" d="M0.82,0.82L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.0068366666666666 3.0654333333332033) rotate(0 0.3411483333333332 0.34309999999999974)" d="M0.68,0.69L0,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 1.2155666666667457) rotate(0 0 0.5)" d="M0,1L0,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(13.333266666666667 8.21550000000002) rotate(0 0.6666666666666666 0)" d="M1.33,0L0,0 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(12.5108 3.065499999999929) rotate(0 0.41123333333333295 0.41123333333333295)" d="M0,0.82L0.82,0 " /><path id="路径 7" fill-rule="evenodd" style="fill:#333333" transform="translate(5.673499999999999 5.5488333333332776) rotate(0 1.1666666666666665 2.333333333333333)" opacity="1" d="M2.33,0C1.04,0 0,1.04 0,2.33C0,3.62 1.04,4.67 2.33,4.67L2.33,0Z " /><path id="路径 8" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333966666666666 7.8821666666667625) rotate(0 0.6666666666666666 0)" d="M0,0L1.33,0 " /><path id="路径 9" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.348133333333333 12.3125) rotate(0 0.3421333333333335 0.3421266666666665)" d="M0,0.68L0.68,0 " /><path id="路径 10" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 13.548763333333454) rotate(0 0 0.6666666666666666)" d="M0,1.33L0,0 " /></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1
app/icons/pause.svg
Normal file
1
app/icons/pause.svg
Normal 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
1
app/icons/rename.svg
Normal 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 |
@@ -18,6 +18,7 @@ const cn = {
|
||||
Copy: "复制",
|
||||
Stop: "停止",
|
||||
Retry: "重试",
|
||||
Delete: "删除",
|
||||
},
|
||||
Rename: "重命名对话",
|
||||
Typing: "正在输入…",
|
||||
@@ -62,10 +63,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: {
|
||||
@@ -78,6 +79,7 @@ const cn = {
|
||||
it: "Italiano",
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
},
|
||||
},
|
||||
Avatar: "头像",
|
||||
@@ -107,6 +109,11 @@ const cn = {
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`内置 ${builtin} 条,用户定义 ${custom} 条`,
|
||||
Edit: "编辑",
|
||||
Modal: {
|
||||
Title: "提示词列表",
|
||||
Add: "增加一条",
|
||||
Search: "搜尋提示詞",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "附带历史消息数",
|
||||
|
||||
189
app/locales/de.ts
Normal file
189
app/locales/de.ts
Normal 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;
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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ı",
|
||||
|
||||
@@ -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:
|
||||
"Summarise the conversation in a short and concise eye-catching title that instantly conveys the main topic. Use as few words as possible. Use the language used in the enquiry, e.g. use English for English enquiry, use zh-hant for traditional chinese enquiry. Don't use quotation marks at the beginning and the end.",
|
||||
Summarize:
|
||||
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 200 字以內",
|
||||
"Summarise the conversation in at most 250 tokens for continuing the conversation in future. Use the language used in the conversation, e.g. use English for English conversation, use zh-hant for traditional chinese conversation.",
|
||||
},
|
||||
ConfirmClearAll: "確認清除所有對話、設定數據?",
|
||||
},
|
||||
|
||||
@@ -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,7 +171,6 @@ 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);
|
||||
@@ -189,8 +192,8 @@ export async function requestChatStream(
|
||||
|
||||
finish();
|
||||
} else if (res.status === 401) {
|
||||
console.error("Anauthorized");
|
||||
options?.onError(new Error("Anauthorized"), res.status);
|
||||
console.error("Unauthorized");
|
||||
options?.onError(new Error("Unauthorized"), res.status);
|
||||
} else {
|
||||
console.error("Stream Error", res.body);
|
||||
options?.onError(new Error("Stream Error"), res.status);
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@mixin light {
|
||||
--theme: light;
|
||||
|
||||
/* color */
|
||||
--white: white;
|
||||
--black: rgb(48, 48, 48);
|
||||
@@ -18,6 +20,8 @@
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
--theme: dark;
|
||||
|
||||
/* color */
|
||||
--white: rgb(30, 30, 30);
|
||||
--black: rgb(187, 187, 187);
|
||||
@@ -31,6 +35,10 @@
|
||||
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
|
||||
|
||||
--theme-color: var(--gray);
|
||||
|
||||
div:not(.no-dark) > svg {
|
||||
filter: invert(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.light {
|
||||
@@ -132,6 +140,7 @@ label {
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
@@ -216,6 +225,7 @@ input[type="password"] {
|
||||
color: var(--black);
|
||||
padding: 0 10px;
|
||||
max-width: 50%;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
div.math {
|
||||
@@ -282,10 +292,6 @@ pre {
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
div:not(.no-dark) > svg {
|
||||
filter: invert(0.5);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
@@ -120,3 +120,7 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function getCSSVar(varName: string) {
|
||||
return getComputedStyle(document.body).getPropertyValue(varName).trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user