mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-08 18:57:07 +08:00
@@ -6,19 +6,21 @@
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
|
||||
box-shadow: var(--card-shadow);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.border {
|
||||
border: var(--border-in-light);
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
filter: brightness(0.9);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -36,25 +38,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dark-button {
|
||||
div:not(:global(.no-dark))>.icon-button-icon {
|
||||
filter: invert(0.5);
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark) {
|
||||
@include dark-button;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@include dark-button;
|
||||
}
|
||||
|
||||
.icon-button-text {
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ export function IconButton(props: {
|
||||
icon: JSX.Element;
|
||||
text?: string;
|
||||
bordered?: boolean;
|
||||
shadow?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
@@ -14,10 +15,13 @@ export function IconButton(props: {
|
||||
<div
|
||||
className={
|
||||
styles["icon-button"] +
|
||||
` ${props.bordered && styles.border} ${props.className ?? ""}`
|
||||
` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
|
||||
props.className ?? ""
|
||||
} clickable`
|
||||
}
|
||||
onClick={props.onClick}
|
||||
title={props.title}
|
||||
role="button"
|
||||
>
|
||||
<div className={styles["icon-button-icon"]}>{props.icon}</div>
|
||||
{props.text && (
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
} from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { isMobileScreen } from "../utils";
|
||||
|
||||
export function ChatItem(props: {
|
||||
onClick?: () => void;
|
||||
@@ -61,7 +62,10 @@ export function ChatList() {
|
||||
key={i}
|
||||
selected={i === selectedIndex}
|
||||
onClick={() => selectSession(i)}
|
||||
onDelete={() => confirm(Locale.Home.DeleteChat) && removeSession(i)}
|
||||
onDelete={() =>
|
||||
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
|
||||
removeSession(i)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
71
app/components/chat.module.scss
Normal file
71
app/components/chat.module.scss
Normal file
@@ -0,0 +1,71 @@
|
||||
.prompt-toast {
|
||||
position: absolute;
|
||||
bottom: -50px;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: calc(100% - 40px);
|
||||
|
||||
.prompt-toast-inner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
|
||||
border: var(--border-in-light);
|
||||
box-shadow: var(--card-shadow);
|
||||
padding: 10px 20px;
|
||||
border-radius: 100px;
|
||||
|
||||
.prompt-toast-content {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-prompt {
|
||||
.context-prompt-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.context-role {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.context-content {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.context-delete-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.context-prompt-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.memory-prompt {
|
||||
margin-top: 20px;
|
||||
|
||||
.memory-prompt-title {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.memory-prompt-content {
|
||||
background-color: var(--gray);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
@@ -9,6 +9,8 @@ import CopyIcon from "../icons/copy.svg";
|
||||
import DownloadIcon from "../icons/download.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
|
||||
import {
|
||||
Message,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
useChatStore,
|
||||
ChatSession,
|
||||
BOT_HELLO,
|
||||
ROLES,
|
||||
} from "../store";
|
||||
|
||||
import {
|
||||
@@ -33,8 +36,9 @@ import Locale from "../locales";
|
||||
|
||||
import { IconButton } from "./button";
|
||||
import styles from "./home.module.scss";
|
||||
import chatStyle from "./chat.module.scss";
|
||||
|
||||
import { showModal, showToast } from "./ui-lib";
|
||||
import { Modal, showModal, showToast } from "./ui-lib";
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
@@ -94,26 +98,130 @@ function exportMessages(messages: Message[], topic: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function showMemoryPrompt(session: ChatSession) {
|
||||
showModal({
|
||||
title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`,
|
||||
children: (
|
||||
<div className="markdown-body">
|
||||
<pre className={styles["export-content"]}>
|
||||
{session.memoryPrompt || Locale.Memory.EmptyContent}
|
||||
</pre>
|
||||
function PromptToast(props: {
|
||||
showModal: boolean;
|
||||
setShowModal: (_: boolean) => void;
|
||||
}) {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const context = session.context;
|
||||
|
||||
const addContextPrompt = (prompt: Message) => {
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.context.push(prompt);
|
||||
});
|
||||
};
|
||||
|
||||
const removeContextPrompt = (i: number) => {
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.context.splice(i, 1);
|
||||
});
|
||||
};
|
||||
|
||||
const updateContextPrompt = (i: number, prompt: Message) => {
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.context[i] = prompt;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
|
||||
<div
|
||||
className={chatStyle["prompt-toast-inner"] + " clickable"}
|
||||
role="button"
|
||||
onClick={() => props.setShowModal(true)}
|
||||
>
|
||||
<BrainIcon />
|
||||
<span className={chatStyle["prompt-toast-content"]}>
|
||||
已设置 {context.length} 条前置上下文
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
actions: [
|
||||
<IconButton
|
||||
key="copy"
|
||||
icon={<CopyIcon />}
|
||||
bordered
|
||||
text={Locale.Memory.Copy}
|
||||
onClick={() => copyToClipboard(session.memoryPrompt)}
|
||||
/>,
|
||||
],
|
||||
});
|
||||
{props.showModal && (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title="编辑前置上下文"
|
||||
onClose={() => props.setShowModal(false)}
|
||||
actions={[
|
||||
<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"]}
|
||||
onChange={(e) =>
|
||||
updateContextPrompt(i, {
|
||||
...c,
|
||||
content: e.target.value as any,
|
||||
})
|
||||
}
|
||||
></input>
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
className={chatStyle["context-delete-button"]}
|
||||
onClick={() => removeContextPrompt(i)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className={chatStyle["context-prompt-row"]}>
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
text="新增"
|
||||
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"]}>
|
||||
{Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
|
||||
{session.messages.length})
|
||||
</div>
|
||||
<div className={chatStyle["memory-prompt-content"]}>
|
||||
{session.memoryPrompt || Locale.Memory.EmptyContent}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useSubmitHandler() {
|
||||
@@ -172,9 +280,8 @@ function useScrollToBottom() {
|
||||
// auto scroll
|
||||
useLayoutEffect(() => {
|
||||
const dom = scrollRef.current;
|
||||
|
||||
if (dom && autoScroll) {
|
||||
dom.scrollTop = dom.scrollHeight;
|
||||
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 500);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -243,8 +350,12 @@ export function Chat(props: {
|
||||
setPromptHints([]);
|
||||
} else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
||||
// check if need to trigger auto completion
|
||||
if (text.startsWith("/") && text.length > 1) {
|
||||
onSearch(text.slice(1));
|
||||
if (text.startsWith("/")) {
|
||||
let searchText = text.slice(1);
|
||||
if (searchText.length === 0) {
|
||||
searchText = " ";
|
||||
}
|
||||
onSearch(searchText);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -299,8 +410,18 @@ export function Chat(props: {
|
||||
|
||||
const config = useChatStore((state) => state.config);
|
||||
|
||||
const context: RenderMessage[] = session.context.slice();
|
||||
|
||||
if (
|
||||
context.length === 0 &&
|
||||
session.messages.at(0)?.content !== BOT_HELLO.content
|
||||
) {
|
||||
context.push(BOT_HELLO);
|
||||
}
|
||||
|
||||
// preview messages
|
||||
const messages = (session.messages as RenderMessage[])
|
||||
const messages = context
|
||||
.concat(session.messages as RenderMessage[])
|
||||
.concat(
|
||||
isLoading
|
||||
? [
|
||||
@@ -326,6 +447,8 @@ export function Chat(props: {
|
||||
: [],
|
||||
);
|
||||
|
||||
const [showPromptModal, setShowPromptModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={styles.chat} key={session.id}>
|
||||
<div className={styles["window-header"]}>
|
||||
@@ -365,7 +488,7 @@ export function Chat(props: {
|
||||
bordered
|
||||
title={Locale.Chat.Actions.CompressedHistory}
|
||||
onClick={() => {
|
||||
showMemoryPrompt(session);
|
||||
setShowPromptModal(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -380,6 +503,11 @@ export function Chat(props: {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PromptToast
|
||||
showModal={showPromptModal}
|
||||
setShowModal={setShowPromptModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles["chat-body"]} ref={scrollRef}>
|
||||
@@ -402,7 +530,10 @@ export function Chat(props: {
|
||||
{Locale.Chat.Typing}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles["chat-message-item"]}>
|
||||
<div
|
||||
className={styles["chat-message-item"]}
|
||||
onMouseOver={() => inputRef.current?.blur()}
|
||||
>
|
||||
{!isUser &&
|
||||
!(message.preview || message.content.length === 0) && (
|
||||
<div className={styles["chat-message-top-actions"]}>
|
||||
@@ -467,7 +598,7 @@ export function Chat(props: {
|
||||
ref={inputRef}
|
||||
className={styles["chat-input"]}
|
||||
placeholder={Locale.Chat.Input(submitKey)}
|
||||
rows={4}
|
||||
rows={2}
|
||||
onInput={(e) => onInput(e.currentTarget.value)}
|
||||
value={userInput}
|
||||
onKeyDown={onInputKeyDown}
|
||||
|
@@ -218,6 +218,7 @@
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-body-title {
|
||||
|
@@ -149,11 +149,12 @@ export function Home() {
|
||||
setOpenSettings(true);
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<a href={REPO_URL} target="_blank">
|
||||
<IconButton icon={<GithubIcon />} />
|
||||
<IconButton icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,6 +166,7 @@ export function Home() {
|
||||
createNewSession();
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
.window-header {
|
||||
padding: 14px 20px;
|
||||
border-bottom: rgba(0, 0, 0, 0.1) 1px solid;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -32,4 +33,4 @@
|
||||
|
||||
.window-action-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user