forked from XiaoMo/ChatGPT-Next-Web
Merge pull request #574 from Yidadaa/bugfix0406
fix: toast, renaming and revert delete session
This commit is contained in:
commit
cc56121e67
@ -12,7 +12,7 @@
|
||||
user-select: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
color: rgb(51, 51, 51);
|
||||
color: var(--black);
|
||||
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
|
@ -59,6 +59,7 @@ export function ChatList() {
|
||||
state.removeSession,
|
||||
state.moveSession,
|
||||
]);
|
||||
const chatStore = useChatStore();
|
||||
|
||||
const onDragEnd: OnDragEndResponder = (result) => {
|
||||
const { destination, source } = result;
|
||||
@ -95,10 +96,7 @@ export function ChatList() {
|
||||
index={i}
|
||||
selected={i === selectedIndex}
|
||||
onClick={() => selectSession(i)}
|
||||
onDelete={() =>
|
||||
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
|
||||
removeSession(i)
|
||||
}
|
||||
onDelete={chatStore.deleteSession}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
|
@ -4,7 +4,7 @@ import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
|
||||
import SendWhiteIcon from "../icons/send-white.svg";
|
||||
import BrainIcon from "../icons/brain.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 DownloadIcon from "../icons/download.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
@ -404,6 +404,7 @@ export function Chat(props: {
|
||||
|
||||
// submit user input
|
||||
const onUserSubmit = () => {
|
||||
if (userInput.length <= 0) return;
|
||||
setIsLoading(true);
|
||||
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
||||
setUserInput("");
|
||||
@ -420,7 +421,6 @@ export function Chat(props: {
|
||||
// check if should send message
|
||||
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (shouldSubmit(e)) {
|
||||
setAutoScroll(true);
|
||||
onUserSubmit();
|
||||
e.preventDefault();
|
||||
}
|
||||
@ -507,13 +507,10 @@ export function Chat(props: {
|
||||
return (
|
||||
<div className={styles.chat} key={session.id}>
|
||||
<div className={styles["window-header"]}>
|
||||
<div
|
||||
className={styles["window-header-title"]}
|
||||
onClick={props?.showSideBar}
|
||||
>
|
||||
<div className={styles["window-header-title"]}>
|
||||
<div
|
||||
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
||||
onClick={() => {
|
||||
onClickCapture={() => {
|
||||
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
||||
if (newTopic && newTopic !== session.topic) {
|
||||
chatStore.updateCurrentSession(
|
||||
@ -531,7 +528,7 @@ export function Chat(props: {
|
||||
<div className={styles["window-actions"]}>
|
||||
<div className={styles["window-action-button"] + " " + styles.mobile}>
|
||||
<IconButton
|
||||
icon={<MenuIcon />}
|
||||
icon={<ReturnIcon />}
|
||||
bordered
|
||||
title={Locale.Chat.Actions.ChatList}
|
||||
onClick={props?.showSideBar}
|
||||
@ -667,7 +664,7 @@ export function Chat(props: {
|
||||
onInput={(e) => onInput(e.currentTarget.value)}
|
||||
value={userInput}
|
||||
onKeyDown={onInputKeyDown}
|
||||
onFocus={() => setAutoScroll(isMobileScreen())}
|
||||
onFocus={() => setAutoScroll(true)}
|
||||
onBlur={() => {
|
||||
setAutoScroll(false);
|
||||
setTimeout(() => setPromptHints([]), 500);
|
||||
@ -679,7 +676,6 @@ export function Chat(props: {
|
||||
text={Locale.Chat.Send}
|
||||
className={styles["chat-input-send"]}
|
||||
noDark
|
||||
disabled={!userInput}
|
||||
onClick={onUserSubmit}
|
||||
/>
|
||||
</div>
|
||||
|
@ -93,6 +93,7 @@ function _Home() {
|
||||
state.removeSession,
|
||||
],
|
||||
);
|
||||
const chatStore = useChatStore();
|
||||
const loading = !useHasHydrated();
|
||||
const [showSideBar, setShowSideBar] = useState(true);
|
||||
|
||||
@ -142,11 +143,7 @@ function _Home() {
|
||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
onClick={() => {
|
||||
if (confirm(Locale.Home.DeleteChat)) {
|
||||
removeSession(currentIndex);
|
||||
}
|
||||
}}
|
||||
onClick={chatStore.deleteSession}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
|
@ -135,9 +135,25 @@
|
||||
box-shadow: var(--card-shadow);
|
||||
border: var(--border-in-light);
|
||||
color: var(--black);
|
||||
padding: 10px 30px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 50px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,17 +110,37 @@ export function showModal(props: ModalProps) {
|
||||
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) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export function showToast(content: string, delay = 3000) {
|
||||
export function showToast(
|
||||
content: string,
|
||||
action?: ToastProps["action"],
|
||||
delay = 3000,
|
||||
) {
|
||||
const div = document.createElement("div");
|
||||
div.className = styles.show;
|
||||
document.body.appendChild(div);
|
||||
@ -139,7 +159,7 @@ export function showToast(content: string, delay = 3000) {
|
||||
close();
|
||||
}, delay);
|
||||
|
||||
root.render(<Toast content={content} />);
|
||||
root.render(<Toast content={content} action={action} />);
|
||||
}
|
||||
|
||||
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
|
||||
|
21
app/icons/return.svg
Normal file
21
app/icons/return.svg
Normal 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 |
@ -47,6 +47,8 @@ const cn = {
|
||||
Home: {
|
||||
NewChat: "新的聊天",
|
||||
DeleteChat: "确认删除选中的对话?",
|
||||
DeleteToast: "已删除会话",
|
||||
Revert: "撤销",
|
||||
},
|
||||
Settings: {
|
||||
Title: "设置",
|
||||
|
@ -50,6 +50,8 @@ const en: LocaleType = {
|
||||
Home: {
|
||||
NewChat: "New Chat",
|
||||
DeleteChat: "Confirm to delete the selected conversation?",
|
||||
DeleteToast: "Chat Deleted",
|
||||
Revert: "Revert",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Settings",
|
||||
|
@ -50,6 +50,8 @@ const es: LocaleType = {
|
||||
Home: {
|
||||
NewChat: "Nuevo chat",
|
||||
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
|
||||
DeleteToast: "Chat Deleted",
|
||||
Revert: "Revert",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Configuración",
|
||||
|
@ -50,6 +50,8 @@ const it: LocaleType = {
|
||||
Home: {
|
||||
NewChat: "Nuova Chat",
|
||||
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
|
||||
DeleteToast: "Chat Deleted",
|
||||
Revert: "Revert",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Impostazioni",
|
||||
|
@ -48,6 +48,8 @@ const tw: LocaleType = {
|
||||
Home: {
|
||||
NewChat: "新的對話",
|
||||
DeleteChat: "確定要刪除選取的對話嗎?",
|
||||
DeleteToast: "已刪除對話",
|
||||
Revert: "撤銷",
|
||||
},
|
||||
Settings: {
|
||||
Title: "設定",
|
||||
|
@ -7,9 +7,10 @@ import {
|
||||
requestChatStream,
|
||||
requestWithPrompt,
|
||||
} from "../requests";
|
||||
import { trimTopic } from "../utils";
|
||||
import { isMobileScreen, trimTopic } from "../utils";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
|
||||
export type Message = ChatCompletionResponseMessage & {
|
||||
date: string;
|
||||
@ -204,6 +205,7 @@ interface ChatStore {
|
||||
moveSession: (from: number, to: number) => void;
|
||||
selectSession: (index: number) => void;
|
||||
newSession: () => void;
|
||||
deleteSession: () => void;
|
||||
currentSession: () => ChatSession;
|
||||
onNewMessage: (message: Message) => 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() {
|
||||
let index = get().currentSessionIndex;
|
||||
const sessions = get().sessions;
|
||||
|
20
app/utils.ts
20
app/utils.ts
@ -7,23 +7,21 @@ export function trimTopic(topic: string) {
|
||||
}
|
||||
|
||||
export async function copyToClipboard(text: string) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
} else {
|
||||
const textArea = document.createElement('textarea');
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (error) {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
console.log('Text copied to clipboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
document.execCommand("copy");
|
||||
} catch (error) {
|
||||
showToast(Locale.Copy.Failed);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
} finally {
|
||||
showToast(Locale.Copy.Success);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user