add dialog

This commit is contained in:
lishiyun1227 2023-04-17 13:29:59 +08:00
parent 1974050c4a
commit 09dbbdc429
2 changed files with 247 additions and 65 deletions

View File

@ -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;
}
}
}

View File

@ -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>
); );
} }