diff --git a/app/components/button.module.scss b/app/components/button.module.scss index 5b0bbe06..e7d5d894 100644 --- a/app/components/button.module.scss +++ b/app/components/button.module.scss @@ -12,7 +12,7 @@ user-select: none; outline: none; border: none; - color: rgb(51, 51, 51); + color: var(--black); &[disabled] { cursor: not-allowed; diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index 8d02805f..ab5d849f 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -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} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index b6920964..4ab61644 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -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) => { if (shouldSubmit(e)) { - setAutoScroll(true); onUserSubmit(); e.preventDefault(); } @@ -507,13 +507,10 @@ export function Chat(props: { return (
-
+
{ + onClickCapture={() => { const newTopic = prompt(Locale.Chat.Rename, session.topic); if (newTopic && newTopic !== session.topic) { chatStore.updateCurrentSession( @@ -531,7 +528,7 @@ export function Chat(props: {
} + icon={} 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} />
diff --git a/app/components/home.tsx b/app/components/home.tsx index 9e57cb87..b6a2161d 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -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() {
} - onClick={() => { - if (confirm(Locale.Home.DeleteChat)) { - removeSession(currentIndex); - } - }} + onClick={chatStore.deleteSession} />
diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index 95091cd0..83eb614f 100644 --- a/app/components/ui-lib.module.scss +++ b/app/components/ui-lib.module.scss @@ -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; } } -} \ No newline at end of file +} diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index 6761e7f9..a72aa868 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -110,17 +110,37 @@ export function showModal(props: ModalProps) { root.render(); } -export type ToastProps = { content: string }; +export type ToastProps = { + content: string; + action?: { + text: string; + onClick: () => void; + }; +}; export function Toast(props: ToastProps) { return (
-
{props.content}
+
+ {props.content} + {props.action && ( + + )} +
); } -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(); + root.render(); } export type InputProps = React.HTMLProps & { diff --git a/app/icons/return.svg b/app/icons/return.svg new file mode 100644 index 00000000..eba5e78f --- /dev/null +++ b/app/icons/return.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 033e1c2c..e21272a1 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -47,6 +47,8 @@ const cn = { Home: { NewChat: "新的聊天", DeleteChat: "确认删除选中的对话?", + DeleteToast: "已删除会话", + Revert: "撤销", }, Settings: { Title: "设置", diff --git a/app/locales/en.ts b/app/locales/en.ts index aefc2e57..61d20b60 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -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", diff --git a/app/locales/es.ts b/app/locales/es.ts index 6997d5f6..5a83cb55 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -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", diff --git a/app/locales/it.ts b/app/locales/it.ts index 95e6747b..7108090e 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -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", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 4340fe2c..ff1794b5 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -48,6 +48,8 @@ const tw: LocaleType = { Home: { NewChat: "新的對話", DeleteChat: "確定要刪除選取的對話嗎?", + DeleteToast: "已刪除對話", + Revert: "撤銷", }, Settings: { Title: "設定", diff --git a/app/store/app.ts b/app/store/app.ts index c63fa9d4..e72163eb 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -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; @@ -324,6 +326,26 @@ export const useChatStore = create()( })); }, + 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; diff --git a/app/utils.ts b/app/utils.ts index 9fcb1182..bb44e072 100644 --- a/app/utils.ts +++ b/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); } }