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";
|
||||
|
||||
.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 {
|
||||
position: absolute;
|
||||
bottom: -50px;
|
||||
@ -81,3 +105,35 @@
|
||||
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 BrainIcon from "../icons/brain.svg";
|
||||
import RenameIcon from "../icons/rename.svg";
|
||||
import ExportIcon from "../icons/share.svg";
|
||||
import ReturnIcon from "../icons/return.svg";
|
||||
import CopyIcon from "../icons/copy.svg";
|
||||
@ -14,6 +15,12 @@ import DeleteIcon from "../icons/delete.svg";
|
||||
import MaxIcon from "../icons/max.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 {
|
||||
Message,
|
||||
SubmitKey,
|
||||
@ -22,6 +29,7 @@ import {
|
||||
ROLES,
|
||||
createMessage,
|
||||
useAccessStore,
|
||||
Theme,
|
||||
} from "../store";
|
||||
|
||||
import {
|
||||
@ -60,7 +68,11 @@ export function Avatar(props: { role: Message["role"] }) {
|
||||
const config = useChatStore((state) => state.config);
|
||||
|
||||
if (props.role !== "user") {
|
||||
return <BotIcon className={styles["user-avtar"]} />;
|
||||
return (
|
||||
<div className="no-dark">
|
||||
<BotIcon className={styles["user-avtar"]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -312,26 +324,95 @@ export function PromptHints(props: {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function useScrollToBottom() {
|
||||
// for auto-scroll
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const scrollToBottom = () => {
|
||||
const dom = scrollRef.current;
|
||||
if (dom) {
|
||||
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
||||
}
|
||||
};
|
||||
|
||||
// auto scroll
|
||||
useLayoutEffect(() => {
|
||||
const dom = scrollRef.current;
|
||||
if (dom && autoScroll) {
|
||||
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
||||
}
|
||||
autoScroll && scrollToBottom();
|
||||
});
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
autoScroll,
|
||||
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: {
|
||||
showSideBar?: () => void;
|
||||
sideBarShowing?: boolean;
|
||||
@ -350,7 +431,7 @@ export function Chat(props: {
|
||||
const [beforeInput, setBeforeInput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||
const { scrollRef, setAutoScroll } = useScrollToBottom();
|
||||
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
||||
const [hitBottom, setHitBottom] = useState(false);
|
||||
|
||||
const onChatBodyScroll = (e: HTMLElement) => {
|
||||
@ -375,16 +456,6 @@ export function Chat(props: {
|
||||
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
|
||||
const [inputRows, setInputRows] = useState(2);
|
||||
const measure = useDebouncedCallback(
|
||||
@ -409,7 +480,6 @@ export function Chat(props: {
|
||||
// only search prompts when user input is short
|
||||
const SEARCH_TEXT_LIMIT = 30;
|
||||
const onInput = (text: string) => {
|
||||
scrollInput();
|
||||
setUserInput(text);
|
||||
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
|
||||
for (let i = botIndex; i >= 0; i -= 1) {
|
||||
if (messages[i].role === "user") {
|
||||
let lastUserMessageIndex: number | null = null;
|
||||
for (let i = 0; i < session.messages.length; i += 1) {
|
||||
const message = session.messages[i];
|
||||
if (message.id === messageId) {
|
||||
break;
|
||||
}
|
||||
if (message.role === "user") {
|
||||
lastUserMessageIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
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(messages[i].content)
|
||||
.onUserInput(session.messages[userIndex].content)
|
||||
.then(() => setIsLoading(false));
|
||||
chatStore.updateCurrentSession((session) =>
|
||||
session.messages.splice(i, 2),
|
||||
);
|
||||
deleteMessage(userIndex);
|
||||
inputRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const config = useChatStore((state) => state.config);
|
||||
@ -489,6 +583,8 @@ export function Chat(props: {
|
||||
const context: RenderMessage[] = session.context.slice();
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const [dialog, setDialog] = useState(false)
|
||||
const [dialogValue, setDialogValue] = useState('')
|
||||
|
||||
if (
|
||||
context.length === 0 &&
|
||||
@ -496,10 +592,17 @@ export function Chat(props: {
|
||||
) {
|
||||
const copiedHello = Object.assign({}, BOT_HELLO);
|
||||
if (!accessStore.isAuthorized()) {
|
||||
copiedHello.content = Locale.Error.Unauthorized;
|
||||
setDialog(true)
|
||||
// copiedHello.content = Locale.Error.Unauthorized;
|
||||
}
|
||||
context.push(copiedHello);
|
||||
}
|
||||
const handleClick = () =>{
|
||||
accessStore.updateToken(dialogValue);
|
||||
setDialog(false)
|
||||
// const copiedHello = Object.assign({}, BOT_HELLO);
|
||||
// context.push(copiedHello);
|
||||
}
|
||||
|
||||
// preview messages
|
||||
const messages = context
|
||||
@ -533,6 +636,13 @@ export function Chat(props: {
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
if (props.sideBarShowing && isMobileScreen()) return;
|
||||
@ -546,14 +656,7 @@ export function Chat(props: {
|
||||
<div className={styles["window-header-title"]}>
|
||||
<div
|
||||
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
||||
onClickCapture={() => {
|
||||
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
||||
if (newTopic && newTopic !== session.topic) {
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.topic = newTopic!),
|
||||
);
|
||||
}
|
||||
}}
|
||||
onClickCapture={renameSession}
|
||||
>
|
||||
{session.topic}
|
||||
</div>
|
||||
@ -572,12 +675,9 @@ export function Chat(props: {
|
||||
</div>
|
||||
<div className={styles["window-action-button"]}>
|
||||
<IconButton
|
||||
icon={<BrainIcon />}
|
||||
icon={<RenameIcon />}
|
||||
bordered
|
||||
title={Locale.Chat.Actions.CompressedHistory}
|
||||
onClick={() => {
|
||||
setShowPromptModal(true);
|
||||
}}
|
||||
onClick={renameSession}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["window-action-button"]}>
|
||||
@ -656,12 +756,20 @@ export function Chat(props: {
|
||||
{Locale.Chat.Actions.Stop}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onResend(i)}
|
||||
onClick={() => onDelete(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Delete}
|
||||
</div>
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onResend(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Retry}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
@ -672,22 +780,20 @@ export function Chat(props: {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(message.preview || message.content.length === 0) &&
|
||||
!isUser ? (
|
||||
<LoadingIcon />
|
||||
) : (
|
||||
<div
|
||||
className="markdown-body"
|
||||
style={{ fontSize: `${fontSize}px` }}
|
||||
<Markdown
|
||||
content={message.content}
|
||||
loading={
|
||||
(message.preview || message.content.length === 0) &&
|
||||
!isUser
|
||||
}
|
||||
onContextMenu={(e) => onRightClick(e, message)}
|
||||
onDoubleClickCapture={() => {
|
||||
if (!isMobileScreen()) return;
|
||||
setUserInput(message.content);
|
||||
}}
|
||||
>
|
||||
<Markdown content={message.content} />
|
||||
</div>
|
||||
)}
|
||||
fontSize={fontSize}
|
||||
parentRef={scrollRef}
|
||||
/>
|
||||
</div>
|
||||
{!isUser && !message.preview && (
|
||||
<div className={styles["chat-message-actions"]}>
|
||||
@ -704,6 +810,12 @@ export function Chat(props: {
|
||||
|
||||
<div className={styles["chat-input-panel"]}>
|
||||
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
||||
|
||||
<ChatActions
|
||||
showPromptModal={() => setShowPromptModal(true)}
|
||||
scrollToBottom={scrollToBottom}
|
||||
hitBottom={hitBottom}
|
||||
/>
|
||||
<div className={styles["chat-input-panel-inner"]}>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
@ -729,6 +841,20 @@ export function Chat(props: {
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user