forked from XiaoMo/ChatGPT-Next-Web
feat: add session config modal
This commit is contained in:
parent
2e3aa940fb
commit
7345639af3
@ -53,6 +53,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.section-title-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.context-prompt {
|
.context-prompt {
|
||||||
.context-prompt-row {
|
.context-prompt-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -81,25 +95,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.memory-prompt {
|
.memory-prompt {
|
||||||
margin-top: 20px;
|
margin: 20px 0;
|
||||||
|
|
||||||
.memory-prompt-title {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.memory-prompt-action {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.memory-prompt-content {
|
.memory-prompt-content {
|
||||||
background-color: var(--gray);
|
background-color: var(--white);
|
||||||
border-radius: 6px;
|
color: var(--black);
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useDebounce, useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
|
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
|
||||||
|
|
||||||
import SendWhiteIcon from "../icons/send-white.svg";
|
import SendWhiteIcon from "../icons/send-white.svg";
|
||||||
@ -9,8 +9,6 @@ import ReturnIcon from "../icons/return.svg";
|
|||||||
import CopyIcon from "../icons/copy.svg";
|
import CopyIcon from "../icons/copy.svg";
|
||||||
import DownloadIcon from "../icons/download.svg";
|
import DownloadIcon from "../icons/download.svg";
|
||||||
import LoadingIcon from "../icons/three-dots.svg";
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
import BotIcon from "../icons/bot.svg";
|
|
||||||
import BlackBotIcon from "../icons/black-bot.svg";
|
|
||||||
import AddIcon from "../icons/add.svg";
|
import AddIcon from "../icons/add.svg";
|
||||||
import DeleteIcon from "../icons/delete.svg";
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
import MaxIcon from "../icons/max.svg";
|
import MaxIcon from "../icons/max.svg";
|
||||||
@ -33,12 +31,13 @@ import {
|
|||||||
Theme,
|
Theme,
|
||||||
ModelType,
|
ModelType,
|
||||||
useAppConfig,
|
useAppConfig,
|
||||||
|
ModelConfig,
|
||||||
|
DEFAULT_TOPIC,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
downloadAs,
|
downloadAs,
|
||||||
getEmojiUrl,
|
|
||||||
selectOrCopy,
|
selectOrCopy,
|
||||||
autoGrowTextArea,
|
autoGrowTextArea,
|
||||||
useMobileScreen,
|
useMobileScreen,
|
||||||
@ -54,10 +53,11 @@ import { IconButton } from "./button";
|
|||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
import chatStyle from "./chat.module.scss";
|
import chatStyle from "./chat.module.scss";
|
||||||
|
|
||||||
import { Input, Modal, showModal } from "./ui-lib";
|
import { Input, List, ListItem, Modal, Popover, showModal } from "./ui-lib";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Path } from "../constant";
|
import { Path } from "../constant";
|
||||||
|
import { ModelConfigList } from "./model-config";
|
||||||
|
import { AvatarPicker } from "./emoji";
|
||||||
const Markdown = dynamic(
|
const Markdown = dynamic(
|
||||||
async () => memo((await import("./markdown")).Markdown),
|
async () => memo((await import("./markdown")).Markdown),
|
||||||
{
|
{
|
||||||
@ -65,32 +65,10 @@ const Markdown = dynamic(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
|
const Avatar = dynamic(async () => (await import("./emoji")).Avatar, {
|
||||||
loading: () => <LoadingIcon />,
|
loading: () => <LoadingIcon />,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Avatar(props: { role: Message["role"]; model?: ModelType }) {
|
|
||||||
const config = useAppConfig();
|
|
||||||
|
|
||||||
if (props.role !== "user") {
|
|
||||||
return (
|
|
||||||
<div className="no-dark">
|
|
||||||
{props.model?.startsWith("gpt-4") ? (
|
|
||||||
<BlackBotIcon className={styles["user-avtar"]} />
|
|
||||||
) : (
|
|
||||||
<BotIcon className={styles["user-avtar"]} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles["user-avtar"]}>
|
|
||||||
<Emoji unified={config.avatar} size={18} getEmojiUrl={getEmojiUrl} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportMessages(messages: Message[], topic: string) {
|
function exportMessages(messages: Message[], topic: string) {
|
||||||
const mdText =
|
const mdText =
|
||||||
`# ${topic}\n\n` +
|
`# ${topic}\n\n` +
|
||||||
@ -129,15 +107,13 @@ function exportMessages(messages: Message[], topic: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function PromptToast(props: {
|
function ContextPrompts() {
|
||||||
showToast?: boolean;
|
|
||||||
showModal?: boolean;
|
|
||||||
setShowModal: (_: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const session = chatStore.currentSession();
|
const session = chatStore.currentSession();
|
||||||
const context = session.context;
|
const context = session.context;
|
||||||
|
|
||||||
|
const [showPicker, setShowPicker] = useState(false);
|
||||||
|
|
||||||
const addContextPrompt = (prompt: Message) => {
|
const addContextPrompt = (prompt: Message) => {
|
||||||
chatStore.updateCurrentSession((session) => {
|
chatStore.updateCurrentSession((session) => {
|
||||||
session.context.push(prompt);
|
session.context.push(prompt);
|
||||||
@ -156,6 +132,165 @@ function PromptToast(props: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
|
||||||
|
{context.map((c, i) => (
|
||||||
|
<div className={chatStyle["context-prompt-row"]} key={i}>
|
||||||
|
<select
|
||||||
|
value={c.role}
|
||||||
|
className={chatStyle["context-role"]}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateContextPrompt(i, {
|
||||||
|
...c,
|
||||||
|
role: e.target.value as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ROLES.map((r) => (
|
||||||
|
<option key={r} value={r}>
|
||||||
|
{r}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Input
|
||||||
|
value={c.content}
|
||||||
|
type="text"
|
||||||
|
className={chatStyle["context-content"]}
|
||||||
|
rows={1}
|
||||||
|
onInput={(e) =>
|
||||||
|
updateContextPrompt(i, {
|
||||||
|
...c,
|
||||||
|
content: e.currentTarget.value as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
className={chatStyle["context-delete-button"]}
|
||||||
|
onClick={() => removeContextPrompt(i)}
|
||||||
|
bordered
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className={chatStyle["context-prompt-row"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<AddIcon />}
|
||||||
|
text={Locale.Context.Add}
|
||||||
|
bordered
|
||||||
|
className={chatStyle["context-prompt-button"]}
|
||||||
|
onClick={() =>
|
||||||
|
addContextPrompt({
|
||||||
|
role: "system",
|
||||||
|
content: "",
|
||||||
|
date: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<List>
|
||||||
|
<ListItem title={"角色头像"}>
|
||||||
|
<Popover
|
||||||
|
content={
|
||||||
|
<AvatarPicker
|
||||||
|
onEmojiClick={(emoji) =>
|
||||||
|
chatStore.updateCurrentSession(
|
||||||
|
(session) => (session.avatar = emoji),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></AvatarPicker>
|
||||||
|
}
|
||||||
|
open={showPicker}
|
||||||
|
onClose={() => setShowPicker(false)}
|
||||||
|
>
|
||||||
|
<div onClick={() => setShowPicker(true)}>
|
||||||
|
{session.avatar ? (
|
||||||
|
<Avatar avatar={session.avatar} />
|
||||||
|
) : (
|
||||||
|
<Avatar model={session.modelConfig.model} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem title={"对话标题"}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={session.topic}
|
||||||
|
onInput={(e) =>
|
||||||
|
chatStore.updateCurrentSession(
|
||||||
|
(session) => (session.topic = e.currentTarget.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of
|
||||||
|
${session.messages.length})`}
|
||||||
|
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
|
||||||
|
></ListItem>
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SessionConfigModel(props: { onClose: () => void }) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const config = useAppConfig();
|
||||||
|
const session = chatStore.currentSession();
|
||||||
|
const context = session.context;
|
||||||
|
|
||||||
|
const updateConfig = (updater: (config: ModelConfig) => void) => {
|
||||||
|
const config = { ...session.modelConfig };
|
||||||
|
updater(config);
|
||||||
|
chatStore.updateCurrentSession((session) => (session.modelConfig = config));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={Locale.Context.Edit}
|
||||||
|
onClose={() => props.onClose()}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
key="reset"
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
bordered
|
||||||
|
text="重置预设"
|
||||||
|
onClick={() =>
|
||||||
|
confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession()
|
||||||
|
}
|
||||||
|
/>,
|
||||||
|
<IconButton
|
||||||
|
key="copy"
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
bordered
|
||||||
|
text="保存预设"
|
||||||
|
onClick={() => copyToClipboard(session.memoryPrompt)}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ContextPrompts />
|
||||||
|
|
||||||
|
<ModelConfigList
|
||||||
|
modelConfig={session.modelConfig}
|
||||||
|
updateConfig={updateConfig}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PromptToast(props: {
|
||||||
|
showToast?: boolean;
|
||||||
|
showModal?: boolean;
|
||||||
|
setShowModal: (_: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const session = chatStore.currentSession();
|
||||||
|
const context = session.context;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
|
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
|
||||||
{props.showToast && (
|
{props.showToast && (
|
||||||
@ -171,115 +306,7 @@ function PromptToast(props: {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{props.showModal && (
|
{props.showModal && (
|
||||||
<div className="modal-mask">
|
<SessionConfigModel onClose={() => props.setShowModal(false)} />
|
||||||
<Modal
|
|
||||||
title={Locale.Context.Edit}
|
|
||||||
onClose={() => props.setShowModal(false)}
|
|
||||||
actions={[
|
|
||||||
<IconButton
|
|
||||||
key="reset"
|
|
||||||
icon={<CopyIcon />}
|
|
||||||
bordered
|
|
||||||
text={Locale.Memory.Reset}
|
|
||||||
onClick={() =>
|
|
||||||
confirm(Locale.Memory.ResetConfirm) &&
|
|
||||||
chatStore.resetSession()
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
<IconButton
|
|
||||||
key="copy"
|
|
||||||
icon={<CopyIcon />}
|
|
||||||
bordered
|
|
||||||
text={Locale.Memory.Copy}
|
|
||||||
onClick={() => copyToClipboard(session.memoryPrompt)}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<div className={chatStyle["context-prompt"]}>
|
|
||||||
{context.map((c, i) => (
|
|
||||||
<div className={chatStyle["context-prompt-row"]} key={i}>
|
|
||||||
<select
|
|
||||||
value={c.role}
|
|
||||||
className={chatStyle["context-role"]}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateContextPrompt(i, {
|
|
||||||
...c,
|
|
||||||
role: e.target.value as any,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{ROLES.map((r) => (
|
|
||||||
<option key={r} value={r}>
|
|
||||||
{r}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<Input
|
|
||||||
value={c.content}
|
|
||||||
type="text"
|
|
||||||
className={chatStyle["context-content"]}
|
|
||||||
rows={1}
|
|
||||||
onInput={(e) =>
|
|
||||||
updateContextPrompt(i, {
|
|
||||||
...c,
|
|
||||||
content: e.currentTarget.value as any,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon={<DeleteIcon />}
|
|
||||||
className={chatStyle["context-delete-button"]}
|
|
||||||
onClick={() => removeContextPrompt(i)}
|
|
||||||
bordered
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className={chatStyle["context-prompt-row"]}>
|
|
||||||
<IconButton
|
|
||||||
icon={<AddIcon />}
|
|
||||||
text={Locale.Context.Add}
|
|
||||||
bordered
|
|
||||||
className={chatStyle["context-prompt-button"]}
|
|
||||||
onClick={() =>
|
|
||||||
addContextPrompt({
|
|
||||||
role: "system",
|
|
||||||
content: "",
|
|
||||||
date: "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={chatStyle["memory-prompt"]}>
|
|
||||||
<div className={chatStyle["memory-prompt-title"]}>
|
|
||||||
<span>
|
|
||||||
{Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
|
|
||||||
{session.messages.length})
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<label className={chatStyle["memory-prompt-action"]}>
|
|
||||||
{Locale.Memory.Send}
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={session.sendMemory}
|
|
||||||
onChange={() =>
|
|
||||||
chatStore.updateCurrentSession(
|
|
||||||
(session) =>
|
|
||||||
(session.sendMemory = !session.sendMemory),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
></input>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={chatStyle["memory-prompt-content"]}>
|
|
||||||
{session.memoryPrompt || Locale.Memory.EmptyContent}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -654,7 +681,7 @@ export function Chat() {
|
|||||||
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
||||||
onClickCapture={renameSession}
|
onClickCapture={renameSession}
|
||||||
>
|
>
|
||||||
{session.topic}
|
{!session.topic ? DEFAULT_TOPIC : session.topic}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-header-sub-title"]}>
|
<div className={styles["window-header-sub-title"]}>
|
||||||
{Locale.Chat.SubTitle(session.messages.length)}
|
{Locale.Chat.SubTitle(session.messages.length)}
|
||||||
@ -739,7 +766,13 @@ export function Chat() {
|
|||||||
>
|
>
|
||||||
<div className={styles["chat-message-container"]}>
|
<div className={styles["chat-message-container"]}>
|
||||||
<div className={styles["chat-message-avatar"]}>
|
<div className={styles["chat-message-avatar"]}>
|
||||||
<Avatar role={message.role} model={message.model} />
|
{message.role === "user" ? (
|
||||||
|
<Avatar avatar={config.avatar} />
|
||||||
|
) : session.avatar ? (
|
||||||
|
<Avatar avatar={session.avatar} />
|
||||||
|
) : (
|
||||||
|
<Avatar model={message.model ?? "gpt-3.5-turbo"} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showTyping && (
|
{showTyping && (
|
||||||
<div className={styles["chat-message-status"]}>
|
<div className={styles["chat-message-status"]}>
|
||||||
|
59
app/components/emoji.tsx
Normal file
59
app/components/emoji.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import EmojiPicker, {
|
||||||
|
Emoji,
|
||||||
|
EmojiStyle,
|
||||||
|
Theme as EmojiTheme,
|
||||||
|
} from "emoji-picker-react";
|
||||||
|
|
||||||
|
import { ModelType } from "../store";
|
||||||
|
|
||||||
|
import BotIcon from "../icons/bot.svg";
|
||||||
|
import BlackBotIcon from "../icons/black-bot.svg";
|
||||||
|
|
||||||
|
export function getEmojiUrl(unified: string, style: EmojiStyle) {
|
||||||
|
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AvatarPicker(props: {
|
||||||
|
onEmojiClick: (emojiId: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<EmojiPicker
|
||||||
|
lazyLoadEmojis
|
||||||
|
theme={EmojiTheme.AUTO}
|
||||||
|
getEmojiUrl={getEmojiUrl}
|
||||||
|
onEmojiClick={(e) => {
|
||||||
|
props.onEmojiClick(e.unified);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Avatar(props: { model?: ModelType; avatar?: string }) {
|
||||||
|
if (props.model) {
|
||||||
|
return (
|
||||||
|
<div className="no-dark">
|
||||||
|
{props.model?.startsWith("gpt-4") ? (
|
||||||
|
<BlackBotIcon className="user-avtar" />
|
||||||
|
) : (
|
||||||
|
<BotIcon className="user-avtar" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="user-avtar">
|
||||||
|
{props.avatar && <EmojiAvatar avatar={props.avatar} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmojiAvatar(props: { avatar: string; size?: number }) {
|
||||||
|
return (
|
||||||
|
<Emoji
|
||||||
|
unified={props.avatar}
|
||||||
|
size={props.size ?? 18}
|
||||||
|
getEmojiUrl={getEmojiUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -368,17 +368,6 @@
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avtar {
|
|
||||||
height: 30px;
|
|
||||||
width: 30px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: var(--border-in-light);
|
|
||||||
box-shadow: var(--card-shadow);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-item {
|
.chat-message-item {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
141
app/components/model-config.tsx
Normal file
141
app/components/model-config.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import styles from "./settings.module.scss";
|
||||||
|
import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
|
||||||
|
|
||||||
|
import Locale from "../locales";
|
||||||
|
import { InputRange } from "./input-range";
|
||||||
|
import { List, ListItem } from "./ui-lib";
|
||||||
|
|
||||||
|
export function ModelConfigList(props: {
|
||||||
|
modelConfig: ModelConfig;
|
||||||
|
updateConfig: (updater: (config: ModelConfig) => void) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
<ListItem title={Locale.Settings.Model}>
|
||||||
|
<select
|
||||||
|
value={props.modelConfig.model}
|
||||||
|
onChange={(e) => {
|
||||||
|
props.updateConfig(
|
||||||
|
(config) =>
|
||||||
|
(config.model = ModalConfigValidator.model(
|
||||||
|
e.currentTarget.value,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ALL_MODELS.map((v) => (
|
||||||
|
<option value={v.name} key={v.name} disabled={!v.available}>
|
||||||
|
{v.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Temperature.Title}
|
||||||
|
subTitle={Locale.Settings.Temperature.SubTitle}
|
||||||
|
>
|
||||||
|
<InputRange
|
||||||
|
value={props.modelConfig.temperature?.toFixed(1)}
|
||||||
|
min="0"
|
||||||
|
max="2"
|
||||||
|
step="0.1"
|
||||||
|
onChange={(e) => {
|
||||||
|
props.updateConfig(
|
||||||
|
(config) =>
|
||||||
|
(config.temperature = ModalConfigValidator.temperature(
|
||||||
|
e.currentTarget.valueAsNumber,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></InputRange>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.MaxTokens.Title}
|
||||||
|
subTitle={Locale.Settings.MaxTokens.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={100}
|
||||||
|
max={32000}
|
||||||
|
value={props.modelConfig.max_tokens}
|
||||||
|
onChange={(e) =>
|
||||||
|
props.updateConfig(
|
||||||
|
(config) =>
|
||||||
|
(config.max_tokens = ModalConfigValidator.max_tokens(
|
||||||
|
e.currentTarget.valueAsNumber,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.PresencePenlty.Title}
|
||||||
|
subTitle={Locale.Settings.PresencePenlty.SubTitle}
|
||||||
|
>
|
||||||
|
<InputRange
|
||||||
|
value={props.modelConfig.presence_penalty?.toFixed(1)}
|
||||||
|
min="-2"
|
||||||
|
max="2"
|
||||||
|
step="0.1"
|
||||||
|
onChange={(e) => {
|
||||||
|
props.updateConfig(
|
||||||
|
(config) =>
|
||||||
|
(config.presence_penalty =
|
||||||
|
ModalConfigValidator.presence_penalty(
|
||||||
|
e.currentTarget.valueAsNumber,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></InputRange>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.HistoryCount.Title}
|
||||||
|
subTitle={Locale.Settings.HistoryCount.SubTitle}
|
||||||
|
>
|
||||||
|
<InputRange
|
||||||
|
title={props.modelConfig.historyMessageCount.toString()}
|
||||||
|
value={props.modelConfig.historyMessageCount}
|
||||||
|
min="0"
|
||||||
|
max="25"
|
||||||
|
step="1"
|
||||||
|
onChange={(e) =>
|
||||||
|
props.updateConfig(
|
||||||
|
(config) => (config.historyMessageCount = e.target.valueAsNumber),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></InputRange>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.CompressThreshold.Title}
|
||||||
|
subTitle={Locale.Settings.CompressThreshold.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={500}
|
||||||
|
max={4000}
|
||||||
|
value={props.modelConfig.compressMessageLengthThreshold}
|
||||||
|
onChange={(e) =>
|
||||||
|
props.updateConfig(
|
||||||
|
(config) =>
|
||||||
|
(config.compressMessageLengthThreshold =
|
||||||
|
e.currentTarget.valueAsNumber),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={props.modelConfig.sendMemory}
|
||||||
|
onChange={(e) =>
|
||||||
|
props.updateConfig(
|
||||||
|
(config) => (config.sendMemory = e.currentTarget.checked),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
@ -5,16 +5,6 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-sub-title {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
|
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
|
||||||
|
|
||||||
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-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";
|
||||||
@ -10,30 +8,27 @@ 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 { 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 { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
import {
|
import {
|
||||||
SubmitKey,
|
SubmitKey,
|
||||||
useChatStore,
|
useChatStore,
|
||||||
Theme,
|
Theme,
|
||||||
ALL_MODELS,
|
|
||||||
useUpdateStore,
|
useUpdateStore,
|
||||||
useAccessStore,
|
useAccessStore,
|
||||||
ModalConfigValidator,
|
|
||||||
useAppConfig,
|
useAppConfig,
|
||||||
ChatConfig,
|
|
||||||
ModelConfig,
|
|
||||||
} from "../store";
|
} from "../store";
|
||||||
import { Avatar } from "./chat";
|
|
||||||
|
|
||||||
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
||||||
import { copyToClipboard, getEmojiUrl } from "../utils";
|
import { copyToClipboard } from "../utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Path, UPDATE_URL } from "../constant";
|
import { Path, UPDATE_URL } from "../constant";
|
||||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||||
import { ErrorBoundary } from "./error";
|
import { ErrorBoundary } from "./error";
|
||||||
import { InputRange } from "./input-range";
|
import { InputRange } from "./input-range";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Avatar, AvatarPicker } from "./emoji";
|
||||||
|
|
||||||
function UserPromptModal(props: { onClose?: () => void }) {
|
function UserPromptModal(props: { onClose?: () => void }) {
|
||||||
const promptStore = usePromptStore();
|
const promptStore = usePromptStore();
|
||||||
@ -136,148 +131,6 @@ function UserPromptModal(props: { onClose?: () => void }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingItem(props: {
|
|
||||||
title: string;
|
|
||||||
subTitle?: string;
|
|
||||||
children: JSX.Element;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ListItem>
|
|
||||||
<div className={styles["settings-title"]}>
|
|
||||||
<div>{props.title}</div>
|
|
||||||
{props.subTitle && (
|
|
||||||
<div className={styles["settings-sub-title"]}>{props.subTitle}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{props.children}
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModelConfigList(props: {
|
|
||||||
modelConfig: ModelConfig;
|
|
||||||
updateConfig: (updater: (config: ModelConfig) => void) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SettingItem title={Locale.Settings.Model}>
|
|
||||||
<select
|
|
||||||
value={props.modelConfig.model}
|
|
||||||
onChange={(e) => {
|
|
||||||
props.updateConfig(
|
|
||||||
(config) =>
|
|
||||||
(config.model = ModalConfigValidator.model(
|
|
||||||
e.currentTarget.value,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ALL_MODELS.map((v) => (
|
|
||||||
<option value={v.name} key={v.name} disabled={!v.available}>
|
|
||||||
{v.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem
|
|
||||||
title={Locale.Settings.Temperature.Title}
|
|
||||||
subTitle={Locale.Settings.Temperature.SubTitle}
|
|
||||||
>
|
|
||||||
<InputRange
|
|
||||||
value={props.modelConfig.temperature?.toFixed(1)}
|
|
||||||
min="0"
|
|
||||||
max="2"
|
|
||||||
step="0.1"
|
|
||||||
onChange={(e) => {
|
|
||||||
props.updateConfig(
|
|
||||||
(config) =>
|
|
||||||
(config.temperature = ModalConfigValidator.temperature(
|
|
||||||
e.currentTarget.valueAsNumber,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
></InputRange>
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem
|
|
||||||
title={Locale.Settings.MaxTokens.Title}
|
|
||||||
subTitle={Locale.Settings.MaxTokens.SubTitle}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={100}
|
|
||||||
max={32000}
|
|
||||||
value={props.modelConfig.max_tokens}
|
|
||||||
onChange={(e) =>
|
|
||||||
props.updateConfig(
|
|
||||||
(config) =>
|
|
||||||
(config.max_tokens = ModalConfigValidator.max_tokens(
|
|
||||||
e.currentTarget.valueAsNumber,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
></input>
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem
|
|
||||||
title={Locale.Settings.PresencePenlty.Title}
|
|
||||||
subTitle={Locale.Settings.PresencePenlty.SubTitle}
|
|
||||||
>
|
|
||||||
<InputRange
|
|
||||||
value={props.modelConfig.presence_penalty?.toFixed(1)}
|
|
||||||
min="-2"
|
|
||||||
max="2"
|
|
||||||
step="0.1"
|
|
||||||
onChange={(e) => {
|
|
||||||
props.updateConfig(
|
|
||||||
(config) =>
|
|
||||||
(config.presence_penalty =
|
|
||||||
ModalConfigValidator.presence_penalty(
|
|
||||||
e.currentTarget.valueAsNumber,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
></InputRange>
|
|
||||||
</SettingItem>
|
|
||||||
|
|
||||||
<SettingItem
|
|
||||||
title={Locale.Settings.HistoryCount.Title}
|
|
||||||
subTitle={Locale.Settings.HistoryCount.SubTitle}
|
|
||||||
>
|
|
||||||
<InputRange
|
|
||||||
title={props.modelConfig.historyMessageCount.toString()}
|
|
||||||
value={props.modelConfig.historyMessageCount}
|
|
||||||
min="0"
|
|
||||||
max="25"
|
|
||||||
step="1"
|
|
||||||
onChange={(e) =>
|
|
||||||
props.updateConfig(
|
|
||||||
(config) => (config.historyMessageCount = e.target.valueAsNumber),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
></InputRange>
|
|
||||||
</SettingItem>
|
|
||||||
|
|
||||||
<SettingItem
|
|
||||||
title={Locale.Settings.CompressThreshold.Title}
|
|
||||||
subTitle={Locale.Settings.CompressThreshold.SubTitle}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={500}
|
|
||||||
max={4000}
|
|
||||||
value={props.modelConfig.compressMessageLengthThreshold}
|
|
||||||
onChange={(e) =>
|
|
||||||
props.updateConfig(
|
|
||||||
(config) =>
|
|
||||||
(config.compressMessageLengthThreshold =
|
|
||||||
e.currentTarget.valueAsNumber),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
></input>
|
|
||||||
</SettingItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
@ -401,16 +254,13 @@ export function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles["settings"]}>
|
<div className={styles["settings"]}>
|
||||||
<List>
|
<List>
|
||||||
<SettingItem title={Locale.Settings.Avatar}>
|
<ListItem title={Locale.Settings.Avatar}>
|
||||||
<Popover
|
<Popover
|
||||||
onClose={() => setShowEmojiPicker(false)}
|
onClose={() => setShowEmojiPicker(false)}
|
||||||
content={
|
content={
|
||||||
<EmojiPicker
|
<AvatarPicker
|
||||||
lazyLoadEmojis
|
onEmojiClick={(avatar: string) => {
|
||||||
theme={EmojiTheme.AUTO}
|
updateConfig((config) => (config.avatar = avatar));
|
||||||
getEmojiUrl={getEmojiUrl}
|
|
||||||
onEmojiClick={(e) => {
|
|
||||||
updateConfig((config) => (config.avatar = e.unified));
|
|
||||||
setShowEmojiPicker(false);
|
setShowEmojiPicker(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -421,12 +271,12 @@ export function Settings() {
|
|||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
onClick={() => setShowEmojiPicker(true)}
|
onClick={() => setShowEmojiPicker(true)}
|
||||||
>
|
>
|
||||||
<Avatar role="user" />
|
<Avatar avatar={config.avatar} />
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
|
|
||||||
<SettingItem
|
<ListItem
|
||||||
title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
|
title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
|
||||||
subTitle={
|
subTitle={
|
||||||
checkingUpdate
|
checkingUpdate
|
||||||
@ -449,9 +299,9 @@ export function Settings() {
|
|||||||
onClick={() => checkUpdate(true)}
|
onClick={() => checkUpdate(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
|
|
||||||
<SettingItem title={Locale.Settings.SendKey}>
|
<ListItem title={Locale.Settings.SendKey}>
|
||||||
<select
|
<select
|
||||||
value={config.submitKey}
|
value={config.submitKey}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -467,12 +317,9 @@ export function Settings() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem title={Locale.Settings.Theme}>
|
||||||
<div className={styles["settings-title"]}>
|
|
||||||
{Locale.Settings.Theme}
|
|
||||||
</div>
|
|
||||||
<select
|
<select
|
||||||
value={config.theme}
|
value={config.theme}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -489,7 +336,7 @@ export function Settings() {
|
|||||||
</select>
|
</select>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<SettingItem title={Locale.Settings.Lang.Name}>
|
<ListItem title={Locale.Settings.Lang.Name}>
|
||||||
<select
|
<select
|
||||||
value={getLang()}
|
value={getLang()}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -502,9 +349,9 @@ export function Settings() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
|
|
||||||
<SettingItem
|
<ListItem
|
||||||
title={Locale.Settings.FontSize.Title}
|
title={Locale.Settings.FontSize.Title}
|
||||||
subTitle={Locale.Settings.FontSize.SubTitle}
|
subTitle={Locale.Settings.FontSize.SubTitle}
|
||||||
>
|
>
|
||||||
@ -521,9 +368,9 @@ export function Settings() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
></InputRange>
|
></InputRange>
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
|
|
||||||
<SettingItem title={Locale.Settings.TightBorder}>
|
<ListItem title={Locale.Settings.TightBorder}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={config.tightBorder}
|
checked={config.tightBorder}
|
||||||
@ -533,9 +380,9 @@ export function Settings() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
|
|
||||||
<SettingItem title={Locale.Settings.SendPreviewBubble}>
|
<ListItem title={Locale.Settings.SendPreviewBubble}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={config.sendPreviewBubble}
|
checked={config.sendPreviewBubble}
|
||||||
@ -546,12 +393,12 @@ export function Settings() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
{enabledAccessControl ? (
|
{enabledAccessControl ? (
|
||||||
<SettingItem
|
<ListItem
|
||||||
title={Locale.Settings.AccessCode.Title}
|
title={Locale.Settings.AccessCode.Title}
|
||||||
subTitle={Locale.Settings.AccessCode.SubTitle}
|
subTitle={Locale.Settings.AccessCode.SubTitle}
|
||||||
>
|
>
|
||||||
@ -563,12 +410,12 @@ export function Settings() {
|
|||||||
accessStore.updateCode(e.currentTarget.value);
|
accessStore.updateCode(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SettingItem
|
<ListItem
|
||||||
title={Locale.Settings.Token.Title}
|
title={Locale.Settings.Token.Title}
|
||||||
subTitle={Locale.Settings.Token.SubTitle}
|
subTitle={Locale.Settings.Token.SubTitle}
|
||||||
>
|
>
|
||||||
@ -580,9 +427,9 @@ export function Settings() {
|
|||||||
accessStore.updateToken(e.currentTarget.value);
|
accessStore.updateToken(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
|
|
||||||
<SettingItem
|
<ListItem
|
||||||
title={Locale.Settings.Usage.Title}
|
title={Locale.Settings.Usage.Title}
|
||||||
subTitle={
|
subTitle={
|
||||||
showUsage
|
showUsage
|
||||||
@ -604,11 +451,11 @@ export function Settings() {
|
|||||||
onClick={checkUsage}
|
onClick={checkUsage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
<SettingItem
|
<ListItem
|
||||||
title={Locale.Settings.Prompt.Disable.Title}
|
title={Locale.Settings.Prompt.Disable.Title}
|
||||||
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
|
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
|
||||||
>
|
>
|
||||||
@ -622,9 +469,9 @@ export function Settings() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
|
|
||||||
<SettingItem
|
<ListItem
|
||||||
title={Locale.Settings.Prompt.List}
|
title={Locale.Settings.Prompt.List}
|
||||||
subTitle={Locale.Settings.Prompt.ListCount(
|
subTitle={Locale.Settings.Prompt.ListCount(
|
||||||
builtinCount,
|
builtinCount,
|
||||||
@ -636,19 +483,17 @@ export function Settings() {
|
|||||||
text={Locale.Settings.Prompt.Edit}
|
text={Locale.Settings.Prompt.Edit}
|
||||||
onClick={() => setShowPromptModal(true)}
|
onClick={() => setShowPromptModal(true)}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<List>
|
<ModelConfigList
|
||||||
<ModelConfigList
|
modelConfig={config.modelConfig}
|
||||||
modelConfig={config.modelConfig}
|
updateConfig={(upater) => {
|
||||||
updateConfig={(upater) => {
|
const modelConfig = { ...config.modelConfig };
|
||||||
const modelConfig = { ...config.modelConfig };
|
upater(modelConfig);
|
||||||
upater(modelConfig);
|
config.update((config) => (config.modelConfig = modelConfig));
|
||||||
config.update((config) => (config.modelConfig = modelConfig));
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</List>
|
|
||||||
|
|
||||||
{shouldShowPromptModal && (
|
{shouldShowPromptModal && (
|
||||||
<UserPromptModal onClose={() => setShowPromptModal(false)} />
|
<UserPromptModal onClose={() => setShowPromptModal(false)} />
|
||||||
|
@ -35,6 +35,16 @@
|
|||||||
border-bottom: var(--border-in-light);
|
border-bottom: var(--border-in-light);
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
animation: slide-in ease 0.6s;
|
animation: slide-in ease 0.6s;
|
||||||
|
|
||||||
|
.list-item-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-sub-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
@ -89,6 +99,8 @@
|
|||||||
padding: var(--modal-padding);
|
padding: var(--modal-padding);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
border-top: var(--border-in-light);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -33,12 +33,22 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListItem(props: { children: JSX.Element[] }) {
|
export function ListItem(props: {
|
||||||
if (props.children.length > 2) {
|
title: string;
|
||||||
throw Error("Only Support Two Children");
|
subTitle?: string;
|
||||||
}
|
children?: JSX.Element | JSX.Element[];
|
||||||
|
}) {
|
||||||
return <div className={styles["list-item"]}>{props.children}</div>;
|
return (
|
||||||
|
<div className={styles["list-item"]}>
|
||||||
|
<div className={styles["list-item-title"]}>
|
||||||
|
<div>{props.title}</div>
|
||||||
|
{props.subTitle && (
|
||||||
|
<div className={styles["list-item-sub-title"]}>{props.subTitle}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function List(props: { children: JSX.Element[] | JSX.Element }) {
|
export function List(props: { children: JSX.Element[] | JSX.Element }) {
|
||||||
@ -63,7 +73,7 @@ export function Loading() {
|
|||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
title: string;
|
title: string;
|
||||||
children?: JSX.Element;
|
children?: JSX.Element | JSX.Element[];
|
||||||
actions?: JSX.Element[];
|
actions?: JSX.Element[];
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ const cn = {
|
|||||||
},
|
},
|
||||||
Memory: {
|
Memory: {
|
||||||
Title: "历史摘要",
|
Title: "历史摘要",
|
||||||
EmptyContent: "尚未总结",
|
EmptyContent: "对话内容过短,无需总结",
|
||||||
Send: "启用总结并发送摘要",
|
Send: "启用总结并发送摘要",
|
||||||
Copy: "复制摘要",
|
Copy: "复制摘要",
|
||||||
Reset: "重置对话",
|
Reset: "重置对话",
|
||||||
@ -172,8 +172,8 @@ const cn = {
|
|||||||
},
|
},
|
||||||
Context: {
|
Context: {
|
||||||
Toast: (x: any) => `已设置 ${x} 条前置上下文`,
|
Toast: (x: any) => `已设置 ${x} 条前置上下文`,
|
||||||
Edit: "前置上下文和历史记忆",
|
Edit: "当前对话设置",
|
||||||
Add: "新增一条",
|
Add: "新增预设对话",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import { isMobileScreen, trimTopic } from "../utils";
|
|||||||
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
import { showToast } from "../components/ui-lib";
|
import { showToast } from "../components/ui-lib";
|
||||||
import { ModelType, useAppConfig } from "./config";
|
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||||
|
|
||||||
export type Message = ChatCompletionResponseMessage & {
|
export type Message = ChatCompletionResponseMessage & {
|
||||||
date: string;
|
date: string;
|
||||||
@ -42,16 +42,18 @@ export interface ChatStat {
|
|||||||
export interface ChatSession {
|
export interface ChatSession {
|
||||||
id: number;
|
id: number;
|
||||||
topic: string;
|
topic: string;
|
||||||
sendMemory: boolean;
|
avatar?: string;
|
||||||
memoryPrompt: string;
|
memoryPrompt: string;
|
||||||
context: Message[];
|
context: Message[];
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
stat: ChatStat;
|
stat: ChatStat;
|
||||||
lastUpdate: string;
|
lastUpdate: string;
|
||||||
lastSummarizeIndex: number;
|
lastSummarizeIndex: number;
|
||||||
|
|
||||||
|
modelConfig: ModelConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
|
export const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
|
||||||
export const BOT_HELLO: Message = createMessage({
|
export const BOT_HELLO: Message = createMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: Locale.Store.BotHello,
|
content: Locale.Store.BotHello,
|
||||||
@ -63,7 +65,6 @@ function createEmptySession(): ChatSession {
|
|||||||
return {
|
return {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
topic: DEFAULT_TOPIC,
|
topic: DEFAULT_TOPIC,
|
||||||
sendMemory: true,
|
|
||||||
memoryPrompt: "",
|
memoryPrompt: "",
|
||||||
context: [],
|
context: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
@ -74,6 +75,8 @@ function createEmptySession(): ChatSession {
|
|||||||
},
|
},
|
||||||
lastUpdate: createDate,
|
lastUpdate: createDate,
|
||||||
lastSummarizeIndex: 0,
|
lastSummarizeIndex: 0,
|
||||||
|
|
||||||
|
modelConfig: useAppConfig.getState().modelConfig,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -32,6 +32,7 @@ const DEFAULT_CONFIG = {
|
|||||||
temperature: 1,
|
temperature: 1,
|
||||||
max_tokens: 2000,
|
max_tokens: 2000,
|
||||||
presence_penalty: 0,
|
presence_penalty: 0,
|
||||||
|
sendMemory: true,
|
||||||
historyMessageCount: 4,
|
historyMessageCount: 4,
|
||||||
compressMessageLengthThreshold: 1000,
|
compressMessageLengthThreshold: 1000,
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export * from "./app";
|
export * from "./chat";
|
||||||
export * from "./update";
|
export * from "./update";
|
||||||
export * from "./access";
|
export * from "./access";
|
||||||
export * from "./config";
|
export * from "./config";
|
||||||
|
@ -325,3 +325,14 @@ pre {
|
|||||||
min-width: 80%;
|
min-width: 80%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-avtar {
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { EmojiStyle } from "emoji-picker-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { showToast } from "./components/ui-lib";
|
import { showToast } from "./components/ui-lib";
|
||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
@ -90,10 +89,6 @@ export function selectOrCopy(el: HTMLElement, content: string) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEmojiUrl(unified: string, style: EmojiStyle) {
|
|
||||||
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDomContentWidth(dom: HTMLElement) {
|
function getDomContentWidth(dom: HTMLElement) {
|
||||||
const style = window.getComputedStyle(dom);
|
const style = window.getComputedStyle(dom);
|
||||||
const paddingWidth =
|
const paddingWidth =
|
||||||
|
Loading…
Reference in New Issue
Block a user