mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-09 11:36:38 +08:00
add dialog
This commit is contained in:
parent
1974050c4a
commit
09dbbdc429
@ -1,5 +1,29 @@
|
|||||||
@import "../styles/animation.scss";
|
@import "../styles/animation.scss";
|
||||||
|
|
||||||
|
.chat-input-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.chat-input-action {
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--black);
|
||||||
|
border: var(--border-in-light);
|
||||||
|
padding: 4px 10px;
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.prompt-toast {
|
.prompt-toast {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -50px;
|
bottom: -50px;
|
||||||
@ -81,3 +105,35 @@
|
|||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dialog{
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: rgba(0,0,0,0.2);
|
||||||
|
color: var(--black);
|
||||||
|
.dialog-modal{
|
||||||
|
position: absolute;
|
||||||
|
left: 10%;
|
||||||
|
right: 10%;
|
||||||
|
width: 30%;
|
||||||
|
height: 300px;
|
||||||
|
top: 20%;
|
||||||
|
background-color: var(--white);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--black);
|
||||||
|
|
||||||
|
border: var(--border-in-light);
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
animation: slide-in-from-top ease 0.3s;
|
||||||
|
.title{
|
||||||
|
border-bottom: 1px solid #e6e5e5;
|
||||||
|
position: relative;
|
||||||
|
color: var(--black);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.content-input{
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { memo, 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";
|
||||||
|
import RenameIcon from "../icons/rename.svg";
|
||||||
import ExportIcon from "../icons/share.svg";
|
import ExportIcon from "../icons/share.svg";
|
||||||
import ReturnIcon from "../icons/return.svg";
|
import ReturnIcon from "../icons/return.svg";
|
||||||
import CopyIcon from "../icons/copy.svg";
|
import CopyIcon from "../icons/copy.svg";
|
||||||
@ -14,6 +15,12 @@ import DeleteIcon from "../icons/delete.svg";
|
|||||||
import MaxIcon from "../icons/max.svg";
|
import MaxIcon from "../icons/max.svg";
|
||||||
import MinIcon from "../icons/min.svg";
|
import MinIcon from "../icons/min.svg";
|
||||||
|
|
||||||
|
import LightIcon from "../icons/light.svg";
|
||||||
|
import DarkIcon from "../icons/dark.svg";
|
||||||
|
import AutoIcon from "../icons/auto.svg";
|
||||||
|
import BottomIcon from "../icons/bottom.svg";
|
||||||
|
import StopIcon from "../icons/pause.svg";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Message,
|
Message,
|
||||||
SubmitKey,
|
SubmitKey,
|
||||||
@ -22,6 +29,7 @@ import {
|
|||||||
ROLES,
|
ROLES,
|
||||||
createMessage,
|
createMessage,
|
||||||
useAccessStore,
|
useAccessStore,
|
||||||
|
Theme,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -60,7 +68,11 @@ export function Avatar(props: { role: Message["role"] }) {
|
|||||||
const config = useChatStore((state) => state.config);
|
const config = useChatStore((state) => state.config);
|
||||||
|
|
||||||
if (props.role !== "user") {
|
if (props.role !== "user") {
|
||||||
return <BotIcon className={styles["user-avtar"]} />;
|
return (
|
||||||
|
<div className="no-dark">
|
||||||
|
<BotIcon className={styles["user-avtar"]} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -312,26 +324,95 @@ export function PromptHints(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function useScrollToBottom() {
|
function useScrollToBottom() {
|
||||||
// for auto-scroll
|
// for auto-scroll
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
const dom = scrollRef.current;
|
||||||
|
if (dom) {
|
||||||
|
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// auto scroll
|
// auto scroll
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const dom = scrollRef.current;
|
autoScroll && scrollToBottom();
|
||||||
if (dom && autoScroll) {
|
|
||||||
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scrollRef,
|
scrollRef,
|
||||||
autoScroll,
|
autoScroll,
|
||||||
setAutoScroll,
|
setAutoScroll,
|
||||||
|
scrollToBottom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ChatActions(props: {
|
||||||
|
showPromptModal: () => void;
|
||||||
|
scrollToBottom: () => void;
|
||||||
|
hitBottom: boolean;
|
||||||
|
}) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
|
// switch themes
|
||||||
|
const theme = chatStore.config.theme;
|
||||||
|
function nextTheme() {
|
||||||
|
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
|
||||||
|
const themeIndex = themes.indexOf(theme);
|
||||||
|
const nextIndex = (themeIndex + 1) % themes.length;
|
||||||
|
const nextTheme = themes[nextIndex];
|
||||||
|
chatStore.updateConfig((config) => (config.theme = nextTheme));
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop all responses
|
||||||
|
const couldStop = ControllerPool.hasPending();
|
||||||
|
const stopAll = () => ControllerPool.stopAll();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={chatStyle["chat-input-actions"]}>
|
||||||
|
{couldStop && (
|
||||||
|
<div
|
||||||
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||||
|
onClick={stopAll}
|
||||||
|
>
|
||||||
|
<StopIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!props.hitBottom && (
|
||||||
|
<div
|
||||||
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||||
|
onClick={props.scrollToBottom}
|
||||||
|
>
|
||||||
|
<BottomIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{props.hitBottom && (
|
||||||
|
<div
|
||||||
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||||
|
onClick={props.showPromptModal}
|
||||||
|
>
|
||||||
|
<BrainIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||||
|
onClick={nextTheme}
|
||||||
|
>
|
||||||
|
{theme === Theme.Auto ? (
|
||||||
|
<AutoIcon />
|
||||||
|
) : theme === Theme.Light ? (
|
||||||
|
<LightIcon />
|
||||||
|
) : theme === Theme.Dark ? (
|
||||||
|
<DarkIcon />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Chat(props: {
|
export function Chat(props: {
|
||||||
showSideBar?: () => void;
|
showSideBar?: () => void;
|
||||||
sideBarShowing?: boolean;
|
sideBarShowing?: boolean;
|
||||||
@ -350,7 +431,7 @@ export function Chat(props: {
|
|||||||
const [beforeInput, setBeforeInput] = useState("");
|
const [beforeInput, setBeforeInput] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { submitKey, shouldSubmit } = useSubmitHandler();
|
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||||
const { scrollRef, setAutoScroll } = useScrollToBottom();
|
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
||||||
const [hitBottom, setHitBottom] = useState(false);
|
const [hitBottom, setHitBottom] = useState(false);
|
||||||
|
|
||||||
const onChatBodyScroll = (e: HTMLElement) => {
|
const onChatBodyScroll = (e: HTMLElement) => {
|
||||||
@ -375,16 +456,6 @@ export function Chat(props: {
|
|||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollInput = () => {
|
|
||||||
const dom = inputRef.current;
|
|
||||||
if (!dom) return;
|
|
||||||
const paddingBottomNum: number = parseInt(
|
|
||||||
window.getComputedStyle(dom).paddingBottom,
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
|
|
||||||
};
|
|
||||||
|
|
||||||
// auto grow input
|
// auto grow input
|
||||||
const [inputRows, setInputRows] = useState(2);
|
const [inputRows, setInputRows] = useState(2);
|
||||||
const measure = useDebouncedCallback(
|
const measure = useDebouncedCallback(
|
||||||
@ -409,7 +480,6 @@ export function Chat(props: {
|
|||||||
// only search prompts when user input is short
|
// only search prompts when user input is short
|
||||||
const SEARCH_TEXT_LIMIT = 30;
|
const SEARCH_TEXT_LIMIT = 30;
|
||||||
const onInput = (text: string) => {
|
const onInput = (text: string) => {
|
||||||
scrollInput();
|
|
||||||
setUserInput(text);
|
setUserInput(text);
|
||||||
const n = text.trim().length;
|
const n = text.trim().length;
|
||||||
|
|
||||||
@ -467,21 +537,45 @@ export function Chat(props: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onResend = (botIndex: number) => {
|
const findLastUesrIndex = (messageId: number) => {
|
||||||
// find last user input message and resend
|
// find last user input message and resend
|
||||||
for (let i = botIndex; i >= 0; i -= 1) {
|
let lastUserMessageIndex: number | null = null;
|
||||||
if (messages[i].role === "user") {
|
for (let i = 0; i < session.messages.length; i += 1) {
|
||||||
setIsLoading(true);
|
const message = session.messages[i];
|
||||||
chatStore
|
if (message.id === messageId) {
|
||||||
.onUserInput(messages[i].content)
|
break;
|
||||||
.then(() => setIsLoading(false));
|
}
|
||||||
chatStore.updateCurrentSession((session) =>
|
if (message.role === "user") {
|
||||||
session.messages.splice(i, 2),
|
lastUserMessageIndex = i;
|
||||||
);
|
|
||||||
inputRef.current?.focus();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return lastUserMessageIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMessage = (userIndex: number) => {
|
||||||
|
chatStore.updateCurrentSession((session) =>
|
||||||
|
session.messages.splice(userIndex, 2),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (botMessageId: number) => {
|
||||||
|
const userIndex = findLastUesrIndex(botMessageId);
|
||||||
|
if (userIndex === null) return;
|
||||||
|
deleteMessage(userIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResend = (botMessageId: number) => {
|
||||||
|
// find last user input message and resend
|
||||||
|
const userIndex = findLastUesrIndex(botMessageId);
|
||||||
|
if (userIndex === null) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
chatStore
|
||||||
|
.onUserInput(session.messages[userIndex].content)
|
||||||
|
.then(() => setIsLoading(false));
|
||||||
|
deleteMessage(userIndex);
|
||||||
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = useChatStore((state) => state.config);
|
const config = useChatStore((state) => state.config);
|
||||||
@ -489,6 +583,8 @@ export function Chat(props: {
|
|||||||
const context: RenderMessage[] = session.context.slice();
|
const context: RenderMessage[] = session.context.slice();
|
||||||
|
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
|
const [dialog, setDialog] = useState(false)
|
||||||
|
const [dialogValue, setDialogValue] = useState('')
|
||||||
|
|
||||||
if (
|
if (
|
||||||
context.length === 0 &&
|
context.length === 0 &&
|
||||||
@ -496,10 +592,17 @@ export function Chat(props: {
|
|||||||
) {
|
) {
|
||||||
const copiedHello = Object.assign({}, BOT_HELLO);
|
const copiedHello = Object.assign({}, BOT_HELLO);
|
||||||
if (!accessStore.isAuthorized()) {
|
if (!accessStore.isAuthorized()) {
|
||||||
copiedHello.content = Locale.Error.Unauthorized;
|
setDialog(true)
|
||||||
|
// copiedHello.content = Locale.Error.Unauthorized;
|
||||||
}
|
}
|
||||||
context.push(copiedHello);
|
context.push(copiedHello);
|
||||||
}
|
}
|
||||||
|
const handleClick = () =>{
|
||||||
|
accessStore.updateToken(dialogValue);
|
||||||
|
setDialog(false)
|
||||||
|
// const copiedHello = Object.assign({}, BOT_HELLO);
|
||||||
|
// context.push(copiedHello);
|
||||||
|
}
|
||||||
|
|
||||||
// preview messages
|
// preview messages
|
||||||
const messages = context
|
const messages = context
|
||||||
@ -533,6 +636,13 @@ export function Chat(props: {
|
|||||||
|
|
||||||
const [showPromptModal, setShowPromptModal] = useState(false);
|
const [showPromptModal, setShowPromptModal] = useState(false);
|
||||||
|
|
||||||
|
const renameSession = () => {
|
||||||
|
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
||||||
|
if (newTopic && newTopic !== session.topic) {
|
||||||
|
chatStore.updateCurrentSession((session) => (session.topic = newTopic!));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Auto focus
|
// Auto focus
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.sideBarShowing && isMobileScreen()) return;
|
if (props.sideBarShowing && isMobileScreen()) return;
|
||||||
@ -546,14 +656,7 @@ export function Chat(props: {
|
|||||||
<div className={styles["window-header-title"]}>
|
<div className={styles["window-header-title"]}>
|
||||||
<div
|
<div
|
||||||
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
||||||
onClickCapture={() => {
|
onClickCapture={renameSession}
|
||||||
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
|
||||||
if (newTopic && newTopic !== session.topic) {
|
|
||||||
chatStore.updateCurrentSession(
|
|
||||||
(session) => (session.topic = newTopic!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{session.topic}
|
{session.topic}
|
||||||
</div>
|
</div>
|
||||||
@ -572,12 +675,9 @@ export function Chat(props: {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<BrainIcon />}
|
icon={<RenameIcon />}
|
||||||
bordered
|
bordered
|
||||||
title={Locale.Chat.Actions.CompressedHistory}
|
onClick={renameSession}
|
||||||
onClick={() => {
|
|
||||||
setShowPromptModal(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
@ -656,12 +756,20 @@ export function Chat(props: {
|
|||||||
{Locale.Chat.Actions.Stop}
|
{Locale.Chat.Actions.Stop}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<>
|
||||||
className={styles["chat-message-top-action"]}
|
<div
|
||||||
onClick={() => onResend(i)}
|
className={styles["chat-message-top-action"]}
|
||||||
>
|
onClick={() => onDelete(message.id ?? i)}
|
||||||
{Locale.Chat.Actions.Retry}
|
>
|
||||||
</div>
|
{Locale.Chat.Actions.Delete}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles["chat-message-top-action"]}
|
||||||
|
onClick={() => onResend(message.id ?? i)}
|
||||||
|
>
|
||||||
|
{Locale.Chat.Actions.Retry}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -672,22 +780,20 @@ export function Chat(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(message.preview || message.content.length === 0) &&
|
<Markdown
|
||||||
!isUser ? (
|
content={message.content}
|
||||||
<LoadingIcon />
|
loading={
|
||||||
) : (
|
(message.preview || message.content.length === 0) &&
|
||||||
<div
|
!isUser
|
||||||
className="markdown-body"
|
}
|
||||||
style={{ fontSize: `${fontSize}px` }}
|
onContextMenu={(e) => onRightClick(e, message)}
|
||||||
onContextMenu={(e) => onRightClick(e, message)}
|
onDoubleClickCapture={() => {
|
||||||
onDoubleClickCapture={() => {
|
if (!isMobileScreen()) return;
|
||||||
if (!isMobileScreen()) return;
|
setUserInput(message.content);
|
||||||
setUserInput(message.content);
|
}}
|
||||||
}}
|
fontSize={fontSize}
|
||||||
>
|
parentRef={scrollRef}
|
||||||
<Markdown content={message.content} />
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{!isUser && !message.preview && (
|
{!isUser && !message.preview && (
|
||||||
<div className={styles["chat-message-actions"]}>
|
<div className={styles["chat-message-actions"]}>
|
||||||
@ -704,6 +810,12 @@ export function Chat(props: {
|
|||||||
|
|
||||||
<div className={styles["chat-input-panel"]}>
|
<div className={styles["chat-input-panel"]}>
|
||||||
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
||||||
|
|
||||||
|
<ChatActions
|
||||||
|
showPromptModal={() => setShowPromptModal(true)}
|
||||||
|
scrollToBottom={scrollToBottom}
|
||||||
|
hitBottom={hitBottom}
|
||||||
|
/>
|
||||||
<div className={styles["chat-input-panel-inner"]}>
|
<div className={styles["chat-input-panel-inner"]}>
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@ -729,6 +841,20 @@ export function Chat(props: {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{dialog ?
|
||||||
|
<div className="dialog">
|
||||||
|
<div className="dialog-modal">
|
||||||
|
<div className="title">认证过期,请重新输入ACCESS-CODE</div>
|
||||||
|
<div className="content-input">
|
||||||
|
<input value={dialogValue} type="text" placeholder='请输入ACCESS-CODE' onChange={(e) => {
|
||||||
|
accessStore.updateToken(e.currentTarget.value);
|
||||||
|
setDialogValue(e.currentTarget.value);
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
<button style="margin-top: 30px" onClick={handleClick}>确定</button>
|
||||||
|
</div>
|
||||||
|
</div> : ''}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user