Merge branch 'main' into bugfix-0503

This commit is contained in:
Yidadaa 2023-05-03 15:56:02 +08:00
commit f250594e97
25 changed files with 433 additions and 274 deletions

View File

@ -26,8 +26,11 @@ export async function requestOpenai(req: NextRequest) {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`, Authorization: `Bearer ${apiKey}`,
...(process.env.OPENAI_ORG_ID && { "OpenAI-Organization": process.env.OPENAI_ORG_ID }), ...(process.env.OPENAI_ORG_ID && {
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
}),
}, },
cache: "no-store",
method: req.method, method: req.method,
body: req.body, body: req.body,
}); });

View File

@ -67,7 +67,10 @@ export function ChatItem(props: {
</> </>
)} )}
<div className={styles["chat-item-delete"]} onClick={props.onDelete}> <div
className={styles["chat-item-delete"]}
onClickCapture={props.onDelete}
>
<DeleteIcon /> <DeleteIcon />
</div> </div>
</div> </div>
@ -77,14 +80,14 @@ export function ChatItem(props: {
} }
export function ChatList(props: { narrow?: boolean }) { export function ChatList(props: { narrow?: boolean }) {
const [sessions, selectedIndex, selectSession, removeSession, moveSession] = const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
useChatStore((state) => [ (state) => [
state.sessions, state.sessions,
state.currentSessionIndex, state.currentSessionIndex,
state.selectSession, state.selectSession,
state.removeSession,
state.moveSession, state.moveSession,
]); ],
);
const chatStore = useChatStore(); const chatStore = useChatStore();
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -1,5 +1,5 @@
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react"; import { useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg"; import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg"; import BrainIcon from "../icons/brain.svg";
@ -64,12 +64,9 @@ import {
useMaskStore, useMaskStore,
} from "../store/mask"; } from "../store/mask";
const Markdown = dynamic( const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
async () => memo((await import("./markdown")).Markdown), loading: () => <LoadingIcon />,
{ });
loading: () => <LoadingIcon />,
},
);
function exportMessages(messages: Message[], topic: string) { function exportMessages(messages: Message[], topic: string) {
const mdText = const mdText =
@ -394,7 +391,7 @@ export function Chat() {
const onPromptSelect = (prompt: Prompt) => { const onPromptSelect = (prompt: Prompt) => {
setPromptHints([]); setPromptHints([]);
inputRef.current?.focus(); inputRef.current?.focus();
setUserInput(prompt.content); setTimeout(() => setUserInput(prompt.content), 60);
}; };
// auto grow input // auto grow input
@ -728,6 +725,7 @@ export function Chat() {
}} }}
fontSize={fontSize} fontSize={fontSize}
parentRef={scrollRef} parentRef={scrollRef}
defaultShow={i >= messages.length - 10}
/> />
</div> </div>
{!isUser && !message.preview && ( {!isUser && !message.preview && (

View File

@ -9,6 +9,7 @@ import { useRef, useState, RefObject, useEffect } from "react";
import { copyToClipboard } from "../utils"; import { copyToClipboard } from "../utils";
import LoadingIcon from "../icons/three-dots.svg"; import LoadingIcon from "../icons/three-dots.svg";
import React from "react";
export function PreCode(props: { children: any }) { export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null); const ref = useRef<HTMLPreElement>(null);
@ -29,78 +30,94 @@ export function PreCode(props: { children: any }) {
); );
} }
function _MarkDownContent(props: { content: string }) {
return (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[
RehypeKatex,
[
RehypeHighlight,
{
detect: false,
ignoreMissing: true,
},
],
]}
components={{
pre: PreCode,
a: (aProps) => {
const href = aProps.href || "";
const isInternal = /^\/#/i.test(href);
const target = isInternal ? "_self" : aProps.target ?? "_blank";
return <a {...aProps} target={target} />;
},
}}
>
{props.content}
</ReactMarkdown>
);
}
export const MarkdownContent = React.memo(_MarkDownContent);
export function Markdown( export function Markdown(
props: { props: {
content: string; content: string;
loading?: boolean; loading?: boolean;
fontSize?: number; fontSize?: number;
parentRef: RefObject<HTMLDivElement>; parentRef: RefObject<HTMLDivElement>;
defaultShow?: boolean;
} & React.DOMAttributes<HTMLDivElement>, } & React.DOMAttributes<HTMLDivElement>,
) { ) {
const mdRef = useRef<HTMLDivElement>(null); const mdRef = useRef<HTMLDivElement>(null);
const renderedHeight = useRef(0);
const inView = useRef(!!props.defaultShow);
const parent = props.parentRef.current; const parent = props.parentRef.current;
const md = mdRef.current; const md = mdRef.current;
const rendered = useRef(true); // disable lazy loading for bad ux
const [counter, setCounter] = useState(0);
useEffect(() => { const checkInView = () => {
// to triggr rerender if (parent && md) {
setCounter(counter + 1); const parentBounds = parent.getBoundingClientRect();
// eslint-disable-next-line react-hooks/exhaustive-deps const twoScreenHeight = Math.max(500, parentBounds.height * 2);
}, [props.loading]); const mdBounds = md.getBoundingClientRect();
const isInRange = (x: number) =>
x <= parentBounds.bottom + twoScreenHeight &&
x >= parentBounds.top - twoScreenHeight;
inView.current = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
}
const inView = if (inView.current && md) {
rendered.current || renderedHeight.current = Math.max(
(() => { renderedHeight.current,
if (parent && md) { md.getBoundingClientRect().height,
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) { checkInView();
rendered.current = true;
}
return inView;
}
})();
const shouldLoading = props.loading || !inView;
return ( return (
<div <div
className="markdown-body" className="markdown-body"
style={{ fontSize: `${props.fontSize ?? 14}px` }} style={{
fontSize: `${props.fontSize ?? 14}px`,
height:
!inView.current && renderedHeight.current > 0
? renderedHeight.current
: "auto",
}}
ref={mdRef} ref={mdRef}
onContextMenu={props.onContextMenu} onContextMenu={props.onContextMenu}
onDoubleClickCapture={props.onDoubleClickCapture} onDoubleClickCapture={props.onDoubleClickCapture}
> >
{shouldLoading ? ( {inView.current &&
<LoadingIcon /> (props.loading ? (
) : ( <LoadingIcon />
<ReactMarkdown ) : (
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]} <MarkdownContent content={props.content} />
rehypePlugins={[ ))}
RehypeKatex,
[
RehypeHighlight,
{
detect: false,
ignoreMissing: true,
},
],
]}
components={{
pre: PreCode,
}}
linkTarget={"_blank"}
>
{props.content}
</ReactMarkdown>
)}
</div> </div>
); );
} }

View File

@ -1,16 +1,4 @@
@import "../styles/animation.scss"; @import "../styles/animation.scss";
@keyframes search-in {
from {
opacity: 0;
transform: translateY(5vh) scaleX(0.5);
}
to {
opacity: 1;
transform: translateY(0) scaleX(1);
}
}
.mask-page { .mask-page {
height: 100%; height: 100%;
display: flex; display: flex;
@ -23,8 +11,9 @@
.mask-filter { .mask-filter {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
margin-bottom: 10px; margin-bottom: 20px;
animation: search-in ease 0.3s; animation: slide-in ease 0.3s;
height: 40px;
display: flex; display: flex;
@ -32,8 +21,6 @@
flex-grow: 1; flex-grow: 1;
max-width: 100%; max-width: 100%;
min-width: 0; min-width: 0;
margin-bottom: 20px;
animation: search-in ease 0.3s;
} }
.mask-filter-lang { .mask-filter-lang {
@ -45,10 +32,7 @@
height: 100%; height: 100%;
margin-left: 10px; margin-left: 10px;
box-sizing: border-box; box-sizing: border-box;
min-width: 80px;
button {
padding: 10px;
}
} }
} }

View File

@ -291,14 +291,16 @@ export function MaskPage() {
))} ))}
</select> </select>
<div className={styles["mask-create"]}> <IconButton
<IconButton className={styles["mask-create"]}
icon={<AddIcon />} icon={<AddIcon />}
text={Locale.Mask.Page.Create} text={Locale.Mask.Page.Create}
bordered bordered
onClick={() => maskStore.create()} onClick={() => {
/> const createdMask = maskStore.create();
</div> setEditingMaskId(createdMask.id);
}}
/>
</div> </div>
<div> <div>

View File

@ -59,10 +59,9 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
.search-bar { .more {
font-size: 12px; font-size: 12px;
margin-right: 10px; margin-left: 10px;
width: 40vw;
} }
} }

View File

@ -5,10 +5,11 @@ import { EmojiAvatar } from "./emoji";
import styles from "./new-chat.module.scss"; import styles from "./new-chat.module.scss";
import LeftIcon from "../icons/left.svg"; import LeftIcon from "../icons/left.svg";
import AddIcon from "../icons/lightning.svg"; import LightningIcon from "../icons/lightning.svg";
import EyeIcon from "../icons/eye.svg";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { createEmptyMask, Mask, useMaskStore } from "../store/mask"; import { Mask, useMaskStore } from "../store/mask";
import Locale from "../locales"; import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store"; import { useAppConfig, useChatStore } from "../store";
import { MaskAvatar } from "./mask"; import { MaskAvatar } from "./mask";
@ -148,20 +149,22 @@ export function NewChat() {
<div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div> <div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
<div className={styles["actions"]}> <div className={styles["actions"]}>
<input
className={styles["search-bar"]}
placeholder={Locale.NewChat.More}
type="text"
onClick={() => navigate(Path.Masks)}
/>
<IconButton <IconButton
text={Locale.NewChat.Skip} text={Locale.NewChat.Skip}
onClick={() => startChat()} onClick={() => startChat()}
icon={<AddIcon />} icon={<LightningIcon />}
type="primary" type="primary"
shadow shadow
/> />
<IconButton
className={styles["more"]}
text={Locale.NewChat.More}
onClick={() => navigate(Path.Masks)}
icon={<EyeIcon />}
bordered
shadow
/>
</div> </div>
<div className={styles["masks"]}> <div className={styles["masks"]}>

View File

@ -7,6 +7,20 @@
cursor: pointer; cursor: pointer;
} }
.edit-prompt-modal {
display: flex;
flex-direction: column;
.edit-prompt-title {
max-width: unset;
margin-bottom: 20px;
text-align: left;
}
.edit-prompt-content {
max-width: unset;
}
}
.user-prompt-modal { .user-prompt-modal {
min-height: 40vh; min-height: 40vh;
@ -18,47 +32,42 @@
} }
.user-prompt-list { .user-prompt-list {
padding: 10px 0; border: var(--border-in-light);
border-radius: 10px;
.user-prompt-item { .user-prompt-item {
margin-bottom: 10px; display: flex;
widows: 100%; justify-content: space-between;
padding: 10px;
&:not(:last-child) {
border-bottom: var(--border-in-light);
}
.user-prompt-header { .user-prompt-header {
display: flex; max-width: calc(100% - 100px);
widows: 100%;
margin-bottom: 5px;
.user-prompt-title { .user-prompt-title {
flex-grow: 1; font-size: 14px;
max-width: 100%; line-height: 2;
margin-right: 5px; font-weight: bold;
padding: 5px;
font-size: 12px;
text-align: left;
} }
.user-prompt-content {
.user-prompt-buttons { font-size: 12px;
display: flex;
align-items: center;
.user-prompt-button {
height: 100%;
&:not(:last-child) {
margin-right: 5px;
}
}
} }
} }
.user-prompt-content { .user-prompt-buttons {
width: 100%; display: flex;
box-sizing: border-box; align-items: center;
padding: 5px;
margin-right: 10px; .user-prompt-button {
font-size: 12px; height: 100%;
flex-grow: 1;
&:not(:last-child) {
margin-right: 5px;
}
}
} }
} }
} }

View File

@ -3,10 +3,12 @@ import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
import styles from "./settings.module.scss"; import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg"; import ResetIcon from "../icons/reload.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg"; import CloseIcon from "../icons/close.svg";
import CopyIcon from "../icons/copy.svg"; import CopyIcon from "../icons/copy.svg";
import ClearIcon from "../icons/clear.svg"; import ClearIcon from "../icons/clear.svg";
import EditIcon from "../icons/edit.svg"; import EditIcon from "../icons/edit.svg";
import EyeIcon from "../icons/eye.svg";
import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib"; import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib";
import { ModelConfigList } from "./model-config"; import { ModelConfigList } from "./model-config";
@ -30,6 +32,55 @@ import { InputRange } from "./input-range";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Avatar, AvatarPicker } from "./emoji"; import { Avatar, AvatarPicker } from "./emoji";
function EditPromptModal(props: { id: number; onClose: () => void }) {
const promptStore = usePromptStore();
const prompt = promptStore.get(props.id);
return prompt ? (
<div className="modal-mask">
<Modal
title={Locale.Settings.Prompt.EditModal.Title}
onClose={props.onClose}
actions={[
<IconButton
key=""
onClick={props.onClose}
text={Locale.UI.Confirm}
bordered
/>,
]}
>
<div className={styles["edit-prompt-modal"]}>
<input
type="text"
value={prompt.title}
readOnly={!prompt.isUser}
className={styles["edit-prompt-title"]}
onInput={(e) =>
promptStore.update(
props.id,
(prompt) => (prompt.title = e.currentTarget.value),
)
}
></input>
<Input
value={prompt.content}
readOnly={!prompt.isUser}
className={styles["edit-prompt-content"]}
rows={10}
onInput={(e) =>
promptStore.update(
props.id,
(prompt) => (prompt.content = e.currentTarget.value),
)
}
></Input>
</div>
</Modal>
</div>
) : null;
}
function UserPromptModal(props: { onClose?: () => void }) { function UserPromptModal(props: { onClose?: () => void }) {
const promptStore = usePromptStore(); const promptStore = usePromptStore();
const userPrompts = promptStore.getUserPrompts(); const userPrompts = promptStore.getUserPrompts();
@ -39,6 +90,8 @@ function UserPromptModal(props: { onClose?: () => void }) {
const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]); const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
const prompts = searchInput.length > 0 ? searchPrompts : allPrompts; const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
const [editingPromptId, setEditingPromptId] = useState<number>();
useEffect(() => { useEffect(() => {
if (searchInput.length > 0) { if (searchInput.length > 0) {
const searchResult = SearchService.search(searchInput); const searchResult = SearchService.search(searchInput);
@ -56,8 +109,13 @@ function UserPromptModal(props: { onClose?: () => void }) {
actions={[ actions={[
<IconButton <IconButton
key="add" key="add"
onClick={() => promptStore.add({ title: "", content: "" })} onClick={() =>
icon={<ClearIcon />} promptStore.add({
title: "Empty Prompt",
content: "Empty Prompt Content",
})
}
icon={<AddIcon />}
bordered bordered
text={Locale.Settings.Prompt.Modal.Add} text={Locale.Settings.Prompt.Modal.Add}
/>, />,
@ -76,57 +134,51 @@ function UserPromptModal(props: { onClose?: () => void }) {
{prompts.map((v, _) => ( {prompts.map((v, _) => (
<div className={styles["user-prompt-item"]} key={v.id ?? v.title}> <div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
<div className={styles["user-prompt-header"]}> <div className={styles["user-prompt-header"]}>
<input <div className={styles["user-prompt-title"]}>{v.title}</div>
type="text" <div className={styles["user-prompt-content"] + " one-line"}>
className={styles["user-prompt-title"]} {v.content}
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>
</div> </div>
<Input
rows={2} <div className={styles["user-prompt-buttons"]}>
value={v.content} {v.isUser && (
className={styles["user-prompt-content"]} <IconButton
readOnly={!v.isUser} icon={<ClearIcon />}
onChange={(e) => { className={styles["user-prompt-button"]}
if (v.isUser) { onClick={() => promptStore.remove(v.id!)}
promptStore.updateUserPrompts( />
v.id!, )}
(prompt) => (prompt.content = e.currentTarget.value), {v.isUser ? (
); <IconButton
} icon={<EditIcon />}
}} className={styles["user-prompt-button"]}
/> onClick={() => setEditingPromptId(v.id)}
/>
) : (
<IconButton
icon={<EyeIcon />}
className={styles["user-prompt-button"]}
onClick={() => setEditingPromptId(v.id)}
/>
)}
<IconButton
icon={<CopyIcon />}
className={styles["user-prompt-button"]}
onClick={() => copyToClipboard(v.content)}
/>
</div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
</Modal> </Modal>
{editingPromptId !== undefined && (
<EditPromptModal
id={editingPromptId!}
onClose={() => setEditingPromptId(undefined)}
/>
)}
</div> </div>
); );
} }

View File

@ -138,7 +138,11 @@ export function SideBar(props: { className?: string }) {
<div className={styles["sidebar-action"] + " " + styles.mobile}> <div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton <IconButton
icon={<CloseIcon />} icon={<CloseIcon />}
onClick={chatStore.deleteSession} onClick={() => {
if (confirm(Locale.Home.DeleteChat)) {
chatStore.deleteSession(chatStore.currentSessionIndex);
}
}}
/> />
</div> </div>
<div className={styles["sidebar-action"]}> <div className={styles["sidebar-action"]}>

View File

@ -158,6 +158,7 @@ export type ToastProps = {
text: string; text: string;
onClick: () => void; onClick: () => void;
}; };
onClose?: () => void;
}; };
export function Toast(props: ToastProps) { export function Toast(props: ToastProps) {
@ -167,7 +168,10 @@ export function Toast(props: ToastProps) {
<span>{props.content}</span> <span>{props.content}</span>
{props.action && ( {props.action && (
<button <button
onClick={props.action.onClick} onClick={() => {
props.action?.onClick?.();
props.onClose?.();
}}
className={styles["toast-action"]} className={styles["toast-action"]}
> >
{props.action.text} {props.action.text}
@ -201,7 +205,7 @@ export function showToast(
close(); close();
}, delay); }, delay);
root.render(<Toast content={content} action={action} />); root.render(<Toast content={content} action={action} onClose={close} />);
} }
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & { export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {

View File

@ -116,9 +116,12 @@ const cn = {
Edit: "编辑", Edit: "编辑",
Modal: { Modal: {
Title: "提示词列表", Title: "提示词列表",
Add: "增加一条", Add: "新建",
Search: "搜索提示词", Search: "搜索提示词",
}, },
EditModal: {
Title: "编辑提示词",
},
}, },
HistoryCount: { HistoryCount: {
Title: "附带历史消息数", Title: "附带历史消息数",
@ -221,7 +224,15 @@ const cn = {
ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。", ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。",
Title: "挑选一个面具", Title: "挑选一个面具",
SubTitle: "现在开始,与面具背后的灵魂思维碰撞", SubTitle: "现在开始,与面具背后的灵魂思维碰撞",
More: "搜索更多", More: "查看全部",
},
UI: {
Confirm: "确认",
Cancel: "取消",
Close: "关闭",
Create: "新建",
Edit: "编辑",
}, },
}; };

View File

@ -121,6 +121,9 @@ const de: LocaleType = {
Add: "Add One", Add: "Add One",
Search: "Search Prompts", Search: "Search Prompts",
}, },
EditModal: {
Title: "Edit Prompt",
},
}, },
HistoryCount: { HistoryCount: {
Title: "Anzahl der angehängten Nachrichten", Title: "Anzahl der angehängten Nachrichten",
@ -230,6 +233,14 @@ const de: LocaleType = {
NotShow: "Not Show Again", NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.", ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
}, },
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
},
}; };
export default de; export default de;

View File

@ -120,6 +120,9 @@ const en: LocaleType = {
Add: "Add One", Add: "Add One",
Search: "Search Prompts", Search: "Search Prompts",
}, },
EditModal: {
Title: "Edit Prompt",
},
}, },
HistoryCount: { HistoryCount: {
Title: "Attached Messages Count", Title: "Attached Messages Count",
@ -226,6 +229,14 @@ const en: LocaleType = {
NotShow: "Not Show Again", NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.", ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
}, },
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
},
}; };
export default en; export default en;

View File

@ -120,6 +120,9 @@ const es: LocaleType = {
Add: "Add One", Add: "Add One",
Search: "Search Prompts", Search: "Search Prompts",
}, },
EditModal: {
Title: "Edit Prompt",
},
}, },
HistoryCount: { HistoryCount: {
Title: "Cantidad de mensajes adjuntos", Title: "Cantidad de mensajes adjuntos",
@ -227,6 +230,14 @@ const es: LocaleType = {
NotShow: "Not Show Again", NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.", ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
}, },
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
},
}; };
export default es; export default es;

View File

@ -120,6 +120,9 @@ const it: LocaleType = {
Add: "Add One", Add: "Add One",
Search: "Search Prompts", Search: "Search Prompts",
}, },
EditModal: {
Title: "Edit Prompt",
},
}, },
HistoryCount: { HistoryCount: {
Title: "Conteggio dei messaggi allegati", Title: "Conteggio dei messaggi allegati",
@ -228,6 +231,14 @@ const it: LocaleType = {
NotShow: "Not Show Again", NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.", ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
}, },
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
},
}; };
export default it; export default it;

View File

@ -122,6 +122,9 @@ const jp: LocaleType = {
Add: "新規追加", Add: "新規追加",
Search: "プロンプトワード検索", Search: "プロンプトワード検索",
}, },
EditModal: {
Title: "编辑提示词",
},
}, },
HistoryCount: { HistoryCount: {
Title: "履歴メッセージ数を添付", Title: "履歴メッセージ数を添付",
@ -226,6 +229,14 @@ const jp: LocaleType = {
NotShow: "不再展示", NotShow: "不再展示",
ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。", ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。",
}, },
UI: {
Confirm: "确认",
Cancel: "取消",
Close: "关闭",
Create: "新建",
Edit: "编辑",
},
}; };
export default jp; export default jp;

View File

@ -120,6 +120,9 @@ const tr: LocaleType = {
Add: "Add One", Add: "Add One",
Search: "Search Prompts", Search: "Search Prompts",
}, },
EditModal: {
Title: "Edit Prompt",
},
}, },
HistoryCount: { HistoryCount: {
Title: "Ekli Mesaj Sayısı", Title: "Ekli Mesaj Sayısı",
@ -228,6 +231,14 @@ const tr: LocaleType = {
NotShow: "Not Show Again", NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.", ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
}, },
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
},
}; };
export default tr; export default tr;

View File

@ -118,6 +118,9 @@ const tw: LocaleType = {
Add: "新增一條", Add: "新增一條",
Search: "搜尋提示詞", Search: "搜尋提示詞",
}, },
EditModal: {
Title: "编辑提示词",
},
}, },
HistoryCount: { HistoryCount: {
Title: "附帶歷史訊息數", Title: "附帶歷史訊息數",
@ -219,6 +222,13 @@ const tw: LocaleType = {
NotShow: "不再展示", NotShow: "不再展示",
ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。", ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。",
}, },
UI: {
Confirm: "确认",
Cancel: "取消",
Close: "关闭",
Create: "新建",
Edit: "编辑",
},
}; };
export default tw; export default tw;

View File

@ -14,9 +14,8 @@ const TIME_OUT_MS = 60000;
const makeRequestParam = ( const makeRequestParam = (
messages: Message[], messages: Message[],
options?: { options?: {
filterBot?: boolean;
stream?: boolean; stream?: boolean;
model?: ModelType; overrideModel?: ModelType;
}, },
): ChatRequest => { ): ChatRequest => {
let sendMessages = messages.map((v) => ({ let sendMessages = messages.map((v) => ({
@ -24,18 +23,14 @@ const makeRequestParam = (
content: v.content, content: v.content,
})); }));
if (options?.filterBot) {
sendMessages = sendMessages.filter((m) => m.role !== "assistant");
}
const modelConfig = { const modelConfig = {
...useAppConfig.getState().modelConfig, ...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig, ...useChatStore.getState().currentSession().mask.modelConfig,
}; };
// override model config // override model config
if (options?.model) { if (options?.overrideModel) {
modelConfig.model = options.model; modelConfig.model = options.overrideModel;
} }
return { return {
@ -82,8 +77,7 @@ export async function requestChat(
}, },
) { ) {
const req: ChatRequest = makeRequestParam(messages, { const req: ChatRequest = makeRequestParam(messages, {
filterBot: true, overrideModel: options?.model,
model: options?.model,
}); });
const res = await requestOpenaiClient("v1/chat/completions")(req); const res = await requestOpenaiClient("v1/chat/completions")(req);
@ -102,11 +96,11 @@ export async function requestUsage() {
.getDate() .getDate()
.toString() .toString()
.padStart(2, "0")}`; .padStart(2, "0")}`;
const ONE_DAY = 2 * 24 * 60 * 60 * 1000; const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
const now = new Date(Date.now() + ONE_DAY); const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const startDate = formatDate(startOfMonth); const startDate = formatDate(startOfMonth);
const endDate = formatDate(now); const endDate = formatDate(new Date(Date.now() + ONE_DAY));
const [used, subs] = await Promise.all([ const [used, subs] = await Promise.all([
requestOpenaiClient( requestOpenaiClient(
@ -149,9 +143,8 @@ export async function requestUsage() {
export async function requestChatStream( export async function requestChatStream(
messages: Message[], messages: Message[],
options?: { options?: {
filterBot?: boolean;
modelConfig?: ModelConfig; modelConfig?: ModelConfig;
model?: ModelType; overrideModel?: ModelType;
onMessage: (message: string, done: boolean) => void; onMessage: (message: string, done: boolean) => void;
onError: (error: Error, statusCode?: number) => void; onError: (error: Error, statusCode?: number) => void;
onController?: (controller: AbortController) => void; onController?: (controller: AbortController) => void;
@ -159,8 +152,7 @@ export async function requestChatStream(
) { ) {
const req = makeRequestParam(messages, { const req = makeRequestParam(messages, {
stream: true, stream: true,
filterBot: options?.filterBot, overrideModel: options?.overrideModel,
model: options?.model,
}); });
console.log("[Request] ", req); console.log("[Request] ", req);

View File

@ -83,11 +83,10 @@ interface ChatStore {
currentSessionIndex: number; currentSessionIndex: number;
globalId: number; globalId: number;
clearSessions: () => void; clearSessions: () => void;
removeSession: (index: number) => void;
moveSession: (from: number, to: number) => void; moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void; selectSession: (index: number) => void;
newSession: (mask?: Mask) => void; newSession: (mask?: Mask) => void;
deleteSession: (index?: number) => void; deleteSession: (index: number) => void;
currentSession: () => ChatSession; currentSession: () => ChatSession;
onNewMessage: (message: Message) => void; onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>; onUserInput: (content: string) => Promise<void>;
@ -130,31 +129,6 @@ export const useChatStore = create<ChatStore>()(
}); });
}, },
removeSession(index: number) {
set((state) => {
let nextIndex = state.currentSessionIndex;
const sessions = state.sessions;
if (sessions.length === 1) {
return {
currentSessionIndex: 0,
sessions: [createEmptySession()],
};
}
sessions.splice(index, 1);
if (nextIndex === index) {
nextIndex -= 1;
}
return {
currentSessionIndex: nextIndex,
sessions,
};
});
},
moveSession(from: number, to: number) { moveSession(from: number, to: number) {
set((state) => { set((state) => {
const { sessions, currentSessionIndex: oldIndex } = state; const { sessions, currentSessionIndex: oldIndex } = state;
@ -197,31 +171,46 @@ export const useChatStore = create<ChatStore>()(
})); }));
}, },
deleteSession(i?: number) { deleteSession(index) {
const deletedSession = get().currentSession(); const deletingLastSession = get().sessions.length === 1;
const index = i ?? get().currentSessionIndex; const deletedSession = get().sessions.at(index);
const isLastSession = get().sessions.length === 1;
if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
get().removeSession(index);
showToast( if (!deletedSession) return;
Locale.Home.DeleteToast,
{ const sessions = get().sessions.slice();
text: Locale.Home.Revert, sessions.splice(index, 1);
onClick() {
set((state) => ({ let nextIndex = Math.min(
sessions: state.sessions get().currentSessionIndex,
.slice(0, index) sessions.length - 1,
.concat([deletedSession]) );
.concat(
state.sessions.slice(index + Number(isLastSession)), if (deletingLastSession) {
), nextIndex = 0;
})); sessions.push(createEmptySession());
},
},
5000,
);
} }
// for undo delete action
const restoreState = {
currentSessionIndex: get().currentSessionIndex,
sessions: get().sessions.slice(),
};
set(() => ({
currentSessionIndex: nextIndex,
sessions,
}));
showToast(
Locale.Home.DeleteToast,
{
text: Locale.Home.Revert,
onClick() {
set(() => restoreState);
},
},
5000,
);
}, },
currentSession() { currentSession() {
@ -247,6 +236,9 @@ export const useChatStore = create<ChatStore>()(
}, },
async onUserInput(content) { async onUserInput(content) {
const session = get().currentSession();
const modelConfig = session.mask.modelConfig;
const userMessage: Message = createMessage({ const userMessage: Message = createMessage({
role: "user", role: "user",
content, content,
@ -256,7 +248,7 @@ export const useChatStore = create<ChatStore>()(
role: "assistant", role: "assistant",
streaming: true, streaming: true,
id: userMessage.id! + 1, id: userMessage.id! + 1,
model: useAppConfig.getState().modelConfig.model, model: modelConfig.model,
}); });
// get recent messages // get recent messages
@ -290,14 +282,16 @@ export const useChatStore = create<ChatStore>()(
} }
}, },
onError(error, statusCode) { onError(error, statusCode) {
const isAborted = error.message.includes("aborted");
if (statusCode === 401) { if (statusCode === 401) {
botMessage.content = Locale.Error.Unauthorized; botMessage.content = Locale.Error.Unauthorized;
} else if (!error.message.includes("aborted")) { } else if (!isAborted) {
botMessage.content += "\n\n" + Locale.Store.Error; botMessage.content += "\n\n" + Locale.Store.Error;
} }
botMessage.streaming = false; botMessage.streaming = false;
userMessage.isError = true; userMessage.isError = !isAborted;
botMessage.isError = true; botMessage.isError = !isAborted;
set(() => ({})); set(() => ({}));
ControllerPool.remove(sessionIndex, botMessage.id ?? messageIndex); ControllerPool.remove(sessionIndex, botMessage.id ?? messageIndex);
}, },
@ -309,8 +303,7 @@ export const useChatStore = create<ChatStore>()(
controller, controller,
); );
}, },
filterBot: !useAppConfig.getState().sendBotMessages, modelConfig: { ...modelConfig },
modelConfig: useAppConfig.getState().modelConfig,
}); });
}, },
@ -329,7 +322,7 @@ export const useChatStore = create<ChatStore>()(
getMessagesWithMemory() { getMessagesWithMemory() {
const session = get().currentSession(); const session = get().currentSession();
const config = useAppConfig.getState(); const modelConfig = session.mask.modelConfig;
const messages = session.messages.filter((msg) => !msg.isError); const messages = session.messages.filter((msg) => !msg.isError);
const n = messages.length; const n = messages.length;
@ -337,7 +330,7 @@ export const useChatStore = create<ChatStore>()(
// long term memory // long term memory
if ( if (
session.mask.modelConfig.sendMemory && modelConfig.sendMemory &&
session.memoryPrompt && session.memoryPrompt &&
session.memoryPrompt.length > 0 session.memoryPrompt.length > 0
) { ) {
@ -348,14 +341,14 @@ export const useChatStore = create<ChatStore>()(
// get short term and unmemoried long term memory // get short term and unmemoried long term memory
const shortTermMemoryMessageIndex = Math.max( const shortTermMemoryMessageIndex = Math.max(
0, 0,
n - config.modelConfig.historyMessageCount, n - modelConfig.historyMessageCount,
); );
const longTermMemoryMessageIndex = session.lastSummarizeIndex; const longTermMemoryMessageIndex = session.lastSummarizeIndex;
const oldestIndex = Math.max( const oldestIndex = Math.max(
shortTermMemoryMessageIndex, shortTermMemoryMessageIndex,
longTermMemoryMessageIndex, longTermMemoryMessageIndex,
); );
const threshold = config.modelConfig.compressMessageLengthThreshold; const threshold = modelConfig.compressMessageLengthThreshold;
// get recent messages as many as possible // get recent messages as many as possible
const reversedRecentMessages = []; const reversedRecentMessages = [];
@ -414,17 +407,17 @@ export const useChatStore = create<ChatStore>()(
}); });
} }
const config = useAppConfig.getState(); const modelConfig = session.mask.modelConfig;
let toBeSummarizedMsgs = session.messages.slice( let toBeSummarizedMsgs = session.messages.slice(
session.lastSummarizeIndex, session.lastSummarizeIndex,
); );
const historyMsgLength = countMessages(toBeSummarizedMsgs); const historyMsgLength = countMessages(toBeSummarizedMsgs);
if (historyMsgLength > config?.modelConfig?.max_tokens ?? 4000) { if (historyMsgLength > modelConfig?.max_tokens ?? 4000) {
const n = toBeSummarizedMsgs.length; const n = toBeSummarizedMsgs.length;
toBeSummarizedMsgs = toBeSummarizedMsgs.slice( toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
Math.max(0, n - config.modelConfig.historyMessageCount), Math.max(0, n - modelConfig.historyMessageCount),
); );
} }
@ -437,12 +430,11 @@ export const useChatStore = create<ChatStore>()(
"[Chat History] ", "[Chat History] ",
toBeSummarizedMsgs, toBeSummarizedMsgs,
historyMsgLength, historyMsgLength,
config.modelConfig.compressMessageLengthThreshold, modelConfig.compressMessageLengthThreshold,
); );
if ( if (
historyMsgLength > historyMsgLength > modelConfig.compressMessageLengthThreshold &&
config.modelConfig.compressMessageLengthThreshold &&
session.mask.modelConfig.sendMemory session.mask.modelConfig.sendMemory
) { ) {
requestChatStream( requestChatStream(
@ -452,8 +444,7 @@ export const useChatStore = create<ChatStore>()(
date: "", date: "",
}), }),
{ {
filterBot: false, overrideModel: "gpt-3.5-turbo",
model: "gpt-3.5-turbo",
onMessage(message, done) { onMessage(message, done) {
session.memoryPrompt = message; session.memoryPrompt = message;
if (done) { if (done) {

View File

@ -17,7 +17,6 @@ export enum Theme {
} }
export const DEFAULT_CONFIG = { export const DEFAULT_CONFIG = {
sendBotMessages: true as boolean,
submitKey: SubmitKey.CtrlEnter as SubmitKey, submitKey: SubmitKey.CtrlEnter as SubmitKey,
avatar: "1f603", avatar: "1f603",
fontSize: 14, fontSize: 14,

View File

@ -17,11 +17,12 @@ export interface PromptStore {
prompts: Record<number, Prompt>; prompts: Record<number, Prompt>;
add: (prompt: Prompt) => number; add: (prompt: Prompt) => number;
get: (id: number) => Prompt | undefined;
remove: (id: number) => void; remove: (id: number) => void;
search: (text: string) => Prompt[]; search: (text: string) => Prompt[];
update: (id: number, updater: (prompt: Prompt) => void) => void;
getUserPrompts: () => Prompt[]; getUserPrompts: () => Prompt[];
updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void;
} }
export const SearchService = { export const SearchService = {
@ -81,6 +82,16 @@ export const usePromptStore = create<PromptStore>()(
return prompt.id!; return prompt.id!;
}, },
get(id) {
const targetPrompt = get().prompts[id];
if (!targetPrompt) {
return SearchService.builtinPrompts.find((v) => v.id === id);
}
return targetPrompt;
},
remove(id) { remove(id) {
const prompts = get().prompts; const prompts = get().prompts;
delete prompts[id]; delete prompts[id];
@ -98,7 +109,7 @@ export const usePromptStore = create<PromptStore>()(
return userPrompts; return userPrompts;
}, },
updateUserPrompts(id: number, updater) { update(id: number, updater) {
const prompt = get().prompts[id] ?? { const prompt = get().prompts[id] ?? {
title: "", title: "",
content: "", content: "",

View File

@ -1,5 +1,6 @@
dir="$(dirname "$0")" dir="$(dirname "$0")"
config=$dir/proxychains.conf config=$dir/proxychains.conf
host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //') host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //')
echo "proxying to $host_ip"
cp $dir/proxychains.template.conf $config cp $dir/proxychains.template.conf $config
sed -i "\$s/.*/http $host_ip 7890/" $config sed -i "\$s/.*/http $host_ip 7890/" $config