Compare commits

..

22 Commits
v2.0.1 ... v2.1

Author SHA1 Message Date
Yifei Zhang
435e82c824 Merge pull request #1196 from ClarenceDan/main
fix: Resolve markdown link issue
2023-05-02 20:19:47 +08:00
Clarence Dan
aeda7520fe fix: Resolve markdown link issue
Resolved Markdown Issue
This pull request also resolves an issue where internal links were not redirecting properly in markdown, and optimizes the behavior for external links to open in a new window.
2023-05-02 11:18:18 +08:00
Yifei Zhang
56ef8e3ebf Merge pull request #1192 from Yidadaa/bugfix-0501
fixup
2023-05-02 03:12:11 +08:00
Yidadaa
717c123b82 feat: improve mask ui 2023-05-02 03:10:13 +08:00
Yidadaa
132f6c8420 feat: improve mask ui 2023-05-02 03:01:42 +08:00
Yidadaa
116e16e30d fixup 2023-05-02 02:52:25 +08:00
Yifei Zhang
5dbb6afc60 Merge pull request #1191 from Yidadaa/bugfix-0501
fixup
2023-05-02 02:46:50 +08:00
Yidadaa
ae8050a3f7 fixup 2023-05-02 02:45:15 +08:00
Yifei Zhang
8870e966a6 Merge pull request #1190 from Yidadaa/bugfix-0501
Bugfix 0501
2023-05-02 02:40:32 +08:00
Yidadaa
f5a5cffdec fixup 2023-05-02 02:38:30 +08:00
Yidadaa
220c622f8f fixup 2023-05-02 02:37:15 +08:00
Yidadaa
e509749421 perf: improve prompt list performance 2023-05-02 02:26:43 +08:00
Yidadaa
a69cec89fb perf: close #909 reduce message items render time 2023-05-02 00:31:44 +08:00
Yidadaa
8f5c289818 fix: #751 do not cache request 2023-05-01 23:48:23 +08:00
Yifei Zhang
5e544891aa Merge pull request #1189 from Yidadaa/bugfix-0501
Bugfix 0501
2023-05-01 23:44:32 +08:00
Yidadaa
1aaf4ae5bc fix: #1126 can not select prompt 2023-05-01 23:39:54 +08:00
Yidadaa
9f3188fe45 fix: #1124 mask model config does not works 2023-05-01 23:37:02 +08:00
Yidadaa
b2fc7d476a fix: #1147 edit mask after creating a new mask 2023-05-01 23:23:39 +08:00
Yidadaa
c37885e743 fix: #1130 #1131 delete right session 2023-05-01 23:21:28 +08:00
Yidadaa
0209ace221 fix: #1154 wrong date range when query usage 2023-05-01 22:53:33 +08:00
Yifei Zhang
b0cd8579f1 Merge pull request #1144 from 0x9be00ff1/fix-mask-download
fix: mask download not working #1119
2023-04-28 20:43:52 +08:00
Zhenyu Zhu
ba0753c418 fix: mask download not working 2023-04-28 19:25:03 +08:00
25 changed files with 436 additions and 274 deletions

View File

@@ -26,8 +26,11 @@ export async function requestOpenai(req: NextRequest) {
headers: {
"Content-Type": "application/json",
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,
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 />
</div>
</div>
@@ -77,14 +80,14 @@ export function ChatItem(props: {
}
export function ChatList(props: { narrow?: boolean }) {
const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
useChatStore((state) => [
const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
(state) => [
state.sessions,
state.currentSessionIndex,
state.selectSession,
state.removeSession,
state.moveSession,
]);
],
);
const chatStore = useChatStore();
const navigate = useNavigate();

View File

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

View File

@@ -9,6 +9,7 @@ import { useRef, useState, RefObject, useEffect } from "react";
import { copyToClipboard } from "../utils";
import LoadingIcon from "../icons/three-dots.svg";
import React from "react";
export function PreCode(props: { children: any }) {
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(
props: {
content: string;
loading?: boolean;
fontSize?: number;
parentRef: RefObject<HTMLDivElement>;
defaultShow?: boolean;
} & React.DOMAttributes<HTMLDivElement>,
) {
const mdRef = useRef<HTMLDivElement>(null);
const renderedHeight = useRef(0);
const inView = useRef(!!props.defaultShow);
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(() => {
// to triggr rerender
setCounter(counter + 1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.loading]);
const checkInView = () => {
if (parent && md) {
const parentBounds = parent.getBoundingClientRect();
const twoScreenHeight = Math.max(500, parentBounds.height * 2);
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 =
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.current && md) {
renderedHeight.current = Math.max(
renderedHeight.current,
md.getBoundingClientRect().height,
);
}
};
if (inView) {
rendered.current = true;
}
return inView;
}
})();
const shouldLoading = props.loading || !inView;
checkInView();
return (
<div
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}
onContextMenu={props.onContextMenu}
onDoubleClickCapture={props.onDoubleClickCapture}
>
{shouldLoading ? (
<LoadingIcon />
) : (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[
RehypeKatex,
[
RehypeHighlight,
{
detect: false,
ignoreMissing: true,
},
],
]}
components={{
pre: PreCode,
}}
linkTarget={"_blank"}
>
{props.content}
</ReactMarkdown>
)}
{inView.current &&
(props.loading ? (
<LoadingIcon />
) : (
<MarkdownContent content={props.content} />
))}
</div>
);
}

View File

@@ -1,16 +1,4 @@
@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 {
height: 100%;
display: flex;
@@ -23,8 +11,9 @@
.mask-filter {
width: 100%;
max-width: 100%;
margin-bottom: 10px;
animation: search-in ease 0.3s;
margin-bottom: 20px;
animation: slide-in ease 0.3s;
height: 40px;
display: flex;
@@ -32,8 +21,6 @@
flex-grow: 1;
max-width: 100%;
min-width: 0;
margin-bottom: 20px;
animation: search-in ease 0.3s;
}
.mask-filter-lang {
@@ -45,10 +32,7 @@
height: 100%;
margin-left: 10px;
box-sizing: border-box;
button {
padding: 10px;
}
min-width: 80px;
}
}

View File

@@ -291,14 +291,16 @@ export function MaskPage() {
))}
</select>
<div className={styles["mask-create"]}>
<IconButton
icon={<AddIcon />}
text={Locale.Mask.Page.Create}
bordered
onClick={() => maskStore.create()}
/>
</div>
<IconButton
className={styles["mask-create"]}
icon={<AddIcon />}
text={Locale.Mask.Page.Create}
bordered
onClick={() => {
const createdMask = maskStore.create();
setEditingMaskId(createdMask.id);
}}
/>
</div>
<div>
@@ -368,6 +370,9 @@ export function MaskPage() {
text={Locale.Mask.EditModal.Download}
key="export"
bordered
onClick={() =>
downloadAs(JSON.stringify(editingMask), "mask.json")
}
/>,
<IconButton
key="copy"

View File

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

View File

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

View File

@@ -7,6 +7,20 @@
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 {
min-height: 40vh;
@@ -18,47 +32,42 @@
}
.user-prompt-list {
padding: 10px 0;
border: var(--border-in-light);
border-radius: 10px;
.user-prompt-item {
margin-bottom: 10px;
widows: 100%;
display: flex;
justify-content: space-between;
padding: 10px;
&:not(:last-child) {
border-bottom: var(--border-in-light);
}
.user-prompt-header {
display: flex;
widows: 100%;
margin-bottom: 5px;
max-width: calc(100% - 100px);
.user-prompt-title {
flex-grow: 1;
max-width: 100%;
margin-right: 5px;
padding: 5px;
font-size: 12px;
text-align: left;
font-size: 14px;
line-height: 2;
font-weight: bold;
}
.user-prompt-buttons {
display: flex;
align-items: center;
.user-prompt-button {
height: 100%;
&:not(:last-child) {
margin-right: 5px;
}
}
.user-prompt-content {
font-size: 12px;
}
}
.user-prompt-content {
width: 100%;
box-sizing: border-box;
padding: 5px;
margin-right: 10px;
font-size: 12px;
flex-grow: 1;
.user-prompt-buttons {
display: flex;
align-items: center;
.user-prompt-button {
height: 100%;
&: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 ResetIcon from "../icons/reload.svg";
import AddIcon from "../icons/add.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 { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib";
import { ModelConfigList } from "./model-config";
@@ -30,6 +32,55 @@ import { InputRange } from "./input-range";
import { useNavigate } from "react-router-dom";
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 }) {
const promptStore = usePromptStore();
const userPrompts = promptStore.getUserPrompts();
@@ -39,6 +90,8 @@ function UserPromptModal(props: { onClose?: () => void }) {
const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
const [editingPromptId, setEditingPromptId] = useState<number>();
useEffect(() => {
if (searchInput.length > 0) {
const searchResult = SearchService.search(searchInput);
@@ -56,8 +109,13 @@ function UserPromptModal(props: { onClose?: () => void }) {
actions={[
<IconButton
key="add"
onClick={() => promptStore.add({ title: "", content: "" })}
icon={<ClearIcon />}
onClick={() =>
promptStore.add({
title: "Empty Prompt",
content: "Empty Prompt Content",
})
}
icon={<AddIcon />}
bordered
text={Locale.Settings.Prompt.Modal.Add}
/>,
@@ -76,57 +134,51 @@ function UserPromptModal(props: { onClose?: () => void }) {
{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 className={styles["user-prompt-title"]}>{v.title}</div>
<div className={styles["user-prompt-content"] + " one-line"}>
{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 className={styles["user-prompt-buttons"]}>
{v.isUser && (
<IconButton
icon={<ClearIcon />}
className={styles["user-prompt-button"]}
onClick={() => promptStore.remove(v.id!)}
/>
)}
{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>
</Modal>
{editingPromptId !== undefined && (
<EditPromptModal
id={editingPromptId!}
onClose={() => setEditingPromptId(undefined)}
/>
)}
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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