mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	Merge pull request #574 from Yidadaa/bugfix0406
fix: toast, renaming and revert delete session
This commit is contained in:
		@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user