Merge pull request #574 from Yidadaa/bugfix0406

fix: toast, renaming and revert delete session
This commit is contained in:
Yifei Zhang 2023-04-07 00:50:43 +08:00 committed by GitHub
commit cc56121e67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 116 additions and 38 deletions

View File

@ -12,7 +12,7 @@
user-select: none; user-select: none;
outline: none; outline: none;
border: none; border: none;
color: rgb(51, 51, 51); color: var(--black);
&[disabled] { &[disabled] {
cursor: not-allowed; cursor: not-allowed;

View File

@ -59,6 +59,7 @@ export function ChatList() {
state.removeSession, state.removeSession,
state.moveSession, state.moveSession,
]); ]);
const chatStore = useChatStore();
const onDragEnd: OnDragEndResponder = (result) => { const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result; const { destination, source } = result;
@ -95,10 +96,7 @@ export function ChatList() {
index={i} index={i}
selected={i === selectedIndex} selected={i === selectedIndex}
onClick={() => selectSession(i)} onClick={() => selectSession(i)}
onDelete={() => onDelete={chatStore.deleteSession}
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
removeSession(i)
}
/> />
))} ))}
{provided.placeholder} {provided.placeholder}

View File

@ -4,7 +4,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 ExportIcon from "../icons/export.svg"; import ExportIcon from "../icons/export.svg";
import MenuIcon from "../icons/menu.svg"; 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";
@ -404,6 +404,7 @@ export function Chat(props: {
// submit user input // submit user input
const onUserSubmit = () => { const onUserSubmit = () => {
if (userInput.length <= 0) return;
setIsLoading(true); setIsLoading(true);
chatStore.onUserInput(userInput).then(() => setIsLoading(false)); chatStore.onUserInput(userInput).then(() => setIsLoading(false));
setUserInput(""); setUserInput("");
@ -420,7 +421,6 @@ export function Chat(props: {
// check if should send message // check if should send message
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (shouldSubmit(e)) { if (shouldSubmit(e)) {
setAutoScroll(true);
onUserSubmit(); onUserSubmit();
e.preventDefault(); e.preventDefault();
} }
@ -507,13 +507,10 @@ export function Chat(props: {
return ( return (
<div className={styles.chat} key={session.id}> <div className={styles.chat} key={session.id}>
<div className={styles["window-header"]}> <div className={styles["window-header"]}>
<div <div className={styles["window-header-title"]}>
className={styles["window-header-title"]}
onClick={props?.showSideBar}
>
<div <div
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`} className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
onClick={() => { onClickCapture={() => {
const newTopic = prompt(Locale.Chat.Rename, session.topic); const newTopic = prompt(Locale.Chat.Rename, session.topic);
if (newTopic && newTopic !== session.topic) { if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession( chatStore.updateCurrentSession(
@ -531,7 +528,7 @@ export function Chat(props: {
<div className={styles["window-actions"]}> <div className={styles["window-actions"]}>
<div className={styles["window-action-button"] + " " + styles.mobile}> <div className={styles["window-action-button"] + " " + styles.mobile}>
<IconButton <IconButton
icon={<MenuIcon />} icon={<ReturnIcon />}
bordered bordered
title={Locale.Chat.Actions.ChatList} title={Locale.Chat.Actions.ChatList}
onClick={props?.showSideBar} onClick={props?.showSideBar}
@ -667,7 +664,7 @@ export function Chat(props: {
onInput={(e) => onInput(e.currentTarget.value)} onInput={(e) => onInput(e.currentTarget.value)}
value={userInput} value={userInput}
onKeyDown={onInputKeyDown} onKeyDown={onInputKeyDown}
onFocus={() => setAutoScroll(isMobileScreen())} onFocus={() => setAutoScroll(true)}
onBlur={() => { onBlur={() => {
setAutoScroll(false); setAutoScroll(false);
setTimeout(() => setPromptHints([]), 500); setTimeout(() => setPromptHints([]), 500);
@ -679,7 +676,6 @@ export function Chat(props: {
text={Locale.Chat.Send} text={Locale.Chat.Send}
className={styles["chat-input-send"]} className={styles["chat-input-send"]}
noDark noDark
disabled={!userInput}
onClick={onUserSubmit} onClick={onUserSubmit}
/> />
</div> </div>

View File

@ -93,6 +93,7 @@ function _Home() {
state.removeSession, state.removeSession,
], ],
); );
const chatStore = useChatStore();
const loading = !useHasHydrated(); const loading = !useHasHydrated();
const [showSideBar, setShowSideBar] = useState(true); const [showSideBar, setShowSideBar] = useState(true);
@ -142,11 +143,7 @@ function _Home() {
<div className={styles["sidebar-action"] + " " + styles.mobile}> <div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton <IconButton
icon={<CloseIcon />} icon={<CloseIcon />}
onClick={() => { onClick={chatStore.deleteSession}
if (confirm(Locale.Home.DeleteChat)) {
removeSession(currentIndex);
}
}}
/> />
</div> </div>
<div className={styles["sidebar-action"]}> <div className={styles["sidebar-action"]}>

View File

@ -135,9 +135,25 @@
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
border: var(--border-in-light); border: var(--border-in-light);
color: var(--black); color: var(--black);
padding: 10px 30px; padding: 10px 20px;
border-radius: 50px; border-radius: 50px;
margin-bottom: 20px; margin-bottom: 20px;
display: flex;
align-items: center;
.toast-action {
padding-left: 20px;
color: var(--primary);
opacity: 0.8;
border: 0;
background: none;
cursor: pointer;
font-family: inherit;
&:hover {
opacity: 1;
}
}
} }
} }
@ -160,4 +176,4 @@
max-height: 50vh; max-height: 50vh;
} }
} }
} }

View File

@ -110,17 +110,37 @@ export function showModal(props: ModalProps) {
root.render(<Modal {...props} onClose={closeModal}></Modal>); root.render(<Modal {...props} onClose={closeModal}></Modal>);
} }
export type ToastProps = { content: string }; export type ToastProps = {
content: string;
action?: {
text: string;
onClick: () => void;
};
};
export function Toast(props: ToastProps) { export function Toast(props: ToastProps) {
return ( return (
<div className={styles["toast-container"]}> <div className={styles["toast-container"]}>
<div className={styles["toast-content"]}>{props.content}</div> <div className={styles["toast-content"]}>
<span>{props.content}</span>
{props.action && (
<button
onClick={props.action.onClick}
className={styles["toast-action"]}
>
{props.action.text}
</button>
)}
</div>
</div> </div>
); );
} }
export function showToast(content: string, delay = 3000) { export function showToast(
content: string,
action?: ToastProps["action"],
delay = 3000,
) {
const div = document.createElement("div"); const div = document.createElement("div");
div.className = styles.show; div.className = styles.show;
document.body.appendChild(div); document.body.appendChild(div);
@ -139,7 +159,7 @@ export function showToast(content: string, delay = 3000) {
close(); close();
}, delay); }, delay);
root.render(<Toast content={content} />); root.render(<Toast content={content} action={action} />);
} }
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & { export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {

21
app/icons/return.svg Normal file
View File

@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
height="16" viewBox="0 0 16 16" fill="none">
<defs>
<rect id="path_0" x="0" y="0" width="16" height="16" />
</defs>
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
<mask id="bg-mask-0" fill="white">
<use xlink:href="#path_0"></use>
</mask>
<g mask="url(#bg-mask-0)">
<path id="路径 1"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 2.6666666666666665) rotate(0 1.1666333333333334 2.1666666666666665)"
d="M2.33,0L0,2L2.33,4.33 " />
<path id="路径 2"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 4.666666666666666) rotate(0 6.000006859869576 4.333333333333333)"
d="M0,0L7.66,0C9.96,0 11.91,1.87 12,4.17C12.09,6.59 10.09,8.67 7.66,8.67L2,8.67 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1013 B

View File

@ -47,6 +47,8 @@ const cn = {
Home: { Home: {
NewChat: "新的聊天", NewChat: "新的聊天",
DeleteChat: "确认删除选中的对话?", DeleteChat: "确认删除选中的对话?",
DeleteToast: "已删除会话",
Revert: "撤销",
}, },
Settings: { Settings: {
Title: "设置", Title: "设置",

View File

@ -50,6 +50,8 @@ const en: LocaleType = {
Home: { Home: {
NewChat: "New Chat", NewChat: "New Chat",
DeleteChat: "Confirm to delete the selected conversation?", DeleteChat: "Confirm to delete the selected conversation?",
DeleteToast: "Chat Deleted",
Revert: "Revert",
}, },
Settings: { Settings: {
Title: "Settings", Title: "Settings",

View File

@ -50,6 +50,8 @@ const es: LocaleType = {
Home: { Home: {
NewChat: "Nuevo chat", NewChat: "Nuevo chat",
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?", DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
DeleteToast: "Chat Deleted",
Revert: "Revert",
}, },
Settings: { Settings: {
Title: "Configuración", Title: "Configuración",

View File

@ -50,6 +50,8 @@ const it: LocaleType = {
Home: { Home: {
NewChat: "Nuova Chat", NewChat: "Nuova Chat",
DeleteChat: "Confermare la cancellazione della conversazione selezionata?", DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
DeleteToast: "Chat Deleted",
Revert: "Revert",
}, },
Settings: { Settings: {
Title: "Impostazioni", Title: "Impostazioni",

View File

@ -48,6 +48,8 @@ const tw: LocaleType = {
Home: { Home: {
NewChat: "新的對話", NewChat: "新的對話",
DeleteChat: "確定要刪除選取的對話嗎?", DeleteChat: "確定要刪除選取的對話嗎?",
DeleteToast: "已刪除對話",
Revert: "撤銷",
}, },
Settings: { Settings: {
Title: "設定", Title: "設定",

View File

@ -7,9 +7,10 @@ import {
requestChatStream, requestChatStream,
requestWithPrompt, requestWithPrompt,
} from "../requests"; } from "../requests";
import { trimTopic } from "../utils"; import { isMobileScreen, trimTopic } from "../utils";
import Locale from "../locales"; import Locale from "../locales";
import { showToast } from "../components/ui-lib";
export type Message = ChatCompletionResponseMessage & { export type Message = ChatCompletionResponseMessage & {
date: string; date: string;
@ -204,6 +205,7 @@ interface ChatStore {
moveSession: (from: number, to: number) => void; moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void; selectSession: (index: number) => void;
newSession: () => void; newSession: () => void;
deleteSession: () => void;
currentSession: () => ChatSession; currentSession: () => ChatSession;
onNewMessage: (message: Message) => void; onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>; onUserInput: (content: string) => Promise<void>;
@ -324,6 +326,26 @@ export const useChatStore = create<ChatStore>()(
})); }));
}, },
deleteSession() {
const deletedSession = get().currentSession();
const index = get().currentSessionIndex;
const isLastSession = get().sessions.length === 1;
if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
get().removeSession(index);
}
showToast(Locale.Home.DeleteToast, {
text: Locale.Home.Revert,
onClick() {
set((state) => ({
sessions: state.sessions
.slice(0, index)
.concat([deletedSession])
.concat(state.sessions.slice(index + Number(isLastSession))),
}));
},
});
},
currentSession() { currentSession() {
let index = get().currentSessionIndex; let index = get().currentSessionIndex;
const sessions = get().sessions; const sessions = get().sessions;

View File

@ -7,23 +7,21 @@ export function trimTopic(topic: string) {
} }
export async function copyToClipboard(text: string) { export async function copyToClipboard(text: string) {
if (navigator.clipboard) { try {
navigator.clipboard.writeText(text).catch(err => { await navigator.clipboard.writeText(text);
console.error('Failed to copy: ', err); } catch (error) {
}); const textArea = document.createElement("textarea");
} else {
const textArea = document.createElement('textarea');
textArea.value = text; textArea.value = text;
document.body.appendChild(textArea); document.body.appendChild(textArea);
textArea.focus(); textArea.focus();
textArea.select(); textArea.select();
try { try {
document.execCommand('copy'); document.execCommand("copy");
console.log('Text copied to clipboard'); } catch (error) {
} catch (err) { showToast(Locale.Copy.Failed);
console.error('Failed to copy: ', err);
} }
document.body.removeChild(textArea); } finally {
showToast(Locale.Copy.Success);
} }
} }