From 4af8c26d02e3dd74373d5c0fa835a79f3542d032 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Tue, 21 Mar 2023 14:56:27 +0000 Subject: [PATCH] feat: use toast instead of alert --- app/components/home.module.scss | 1 - app/components/home.tsx | 204 +++++++++++++++++++----------- app/components/ui-lib.module.scss | 40 +++++- app/components/ui-lib.tsx | 116 ++++++++++++----- app/locales/cn.ts | 1 + app/locales/en.ts | 1 + app/utils.ts | 41 ++++-- 7 files changed, 286 insertions(+), 118 deletions(-) diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 2a5a85bb..837c6752 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -10,7 +10,6 @@ min-width: 600px; min-height: 480px; max-width: 900px; - max-height: 720px; display: flex; overflow: hidden; diff --git a/app/components/home.tsx b/app/components/home.tsx index c544f71b..569c38ea 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -22,31 +22,31 @@ import DownloadIcon from "../icons/download.svg"; import { Message, SubmitKey, useChatStore, ChatSession } from "../store"; import { showModal } from "./ui-lib"; -import { copyToClipboard, downloadAs, isIOS } from "../utils"; -import Locale from '../locales' +import { copyToClipboard, downloadAs, isIOS, selectOrCopy } from "../utils"; +import Locale from "../locales"; import dynamic from "next/dynamic"; -export function Loading(props: { - noLogo?: boolean -}) { - return
- {!props.noLogo && } - -
+export function Loading(props: { noLogo?: boolean }) { + return ( +
+ {!props.noLogo && } + +
+ ); } -const Markdown = dynamic(async () => (await import('./markdown')).Markdown, { - loading: () => -}) +const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { + loading: () => , +}); -const Settings = dynamic(async () => (await import('./settings')).Settings, { - loading: () => -}) +const Settings = dynamic(async () => (await import("./settings")).Settings, { + loading: () => , +}); const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, { - loading: () => -}) + loading: () => , +}); export function Avatar(props: { role: Message["role"] }) { const config = useChatStore((state) => state.config); @@ -72,13 +72,16 @@ export function ChatItem(props: { }) { return (
{props.title}
-
{Locale.ChatItem.ChatItemCount(props.count)}
+
+ {Locale.ChatItem.ChatItemCount(props.count)} +
{props.time}
@@ -163,34 +166,34 @@ export function Chat(props: { showSideBar?: () => void }) { .concat( isLoading ? [ - { - role: "assistant", - content: "……", - date: new Date().toLocaleString(), - preview: true, - }, - ] + { + role: "assistant", + content: "……", + date: new Date().toLocaleString(), + preview: true, + }, + ] : [] ) .concat( userInput.length > 0 ? [ - { - role: "user", - content: userInput, - date: new Date().toLocaleString(), - preview: true, - }, - ] + { + role: "user", + content: userInput, + date: new Date().toLocaleString(), + preview: true, + }, + ] : [] ); useEffect(() => { - const dom = latestMessageRef.current + const dom = latestMessageRef.current; if (dom && !isIOS()) { dom.scrollIntoView({ behavior: "smooth", - block: "end" + block: "end", }); } }); @@ -198,8 +201,13 @@ export function Chat(props: { showSideBar?: () => void }) { return (
-
-
{session.topic}
+
+
+ {session.topic} +
{Locale.Chat.SubTitle(session.messages.length)}
@@ -219,7 +227,7 @@ export function Chat(props: { showSideBar?: () => void }) { bordered title={Locale.Chat.Actions.CompressedHistory} onClick={() => { - showMemoryPrompt(session) + showMemoryPrompt(session); }} />
@@ -229,7 +237,7 @@ export function Chat(props: { showSideBar?: () => void }) { bordered title={Locale.Chat.Actions.Export} onClick={() => { - exportMessages(session.messages, session.topic) + exportMessages(session.messages, session.topic); }} />
@@ -252,14 +260,23 @@ export function Chat(props: { showSideBar?: () => void }) {
{(message.preview || message.streaming) && ( -
{Locale.Chat.Typing}
+
+ {Locale.Chat.Typing} +
)}
{(message.preview || message.content.length === 0) && - !isUser ? ( + !isUser ? ( ) : ( -
+
{ + if (selectOrCopy(e.currentTarget, message.content)) { + e.preventDefault() + } + }} + >
)} @@ -317,33 +334,71 @@ function useSwitchTheme() { } function exportMessages(messages: Message[], topic: string) { - const mdText = `# ${topic}\n\n` + messages.map(m => { - return m.role === 'user' ? `## ${m.content}` : m.content.trim() - }).join('\n\n') - const filename = `${topic}.md` + const mdText = + `# ${topic}\n\n` + + messages + .map((m) => { + return m.role === "user" ? `## ${m.content}` : m.content.trim(); + }) + .join("\n\n"); + const filename = `${topic}.md`; showModal({ - title: Locale.Export.Title, children:
-
{mdText}
-
, actions: [ - } bordered text={Locale.Export.Copy} onClick={() => copyToClipboard(mdText)} />, - } bordered text={Locale.Export.Download} onClick={() => downloadAs(mdText, filename)} /> - ] - }) + title: Locale.Export.Title, + children: ( +
+
{mdText}
+
+ ), + actions: [ + } + bordered + text={Locale.Export.Copy} + onClick={() => copyToClipboard(mdText)} + />, + } + bordered + text={Locale.Export.Download} + onClick={() => downloadAs(mdText, filename)} + />, + ], + }); } function showMemoryPrompt(session: ChatSession) { showModal({ - title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`, children:
-
{session.memoryPrompt || Locale.Memory.EmptyContent}
-
, actions: [ - } bordered text={Locale.Memory.Copy} onClick={() => copyToClipboard(session.memoryPrompt)} />, - ] - }) + title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`, + children: ( +
+
+          {session.memoryPrompt || Locale.Memory.EmptyContent}
+        
+
+ ), + actions: [ + } + bordered + text={Locale.Memory.Copy} + onClick={() => copyToClipboard(session.memoryPrompt)} + />, + ], + }); } export function Home() { - const [createNewSession, currentIndex, removeSession] = useChatStore((state) => [state.newSession, state.currentSessionIndex, state.removeSession]); + const [createNewSession, currentIndex, removeSession] = useChatStore( + (state) => [ + state.newSession, + state.currentSessionIndex, + state.removeSession, + ] + ); const loading = !useChatStore?.persist?.hasHydrated(); const [showSideBar, setShowSideBar] = useState(true); @@ -359,8 +414,9 @@ export function Home() { return (
{ - setOpenSettings(false) - setShowSideBar(false) + setOpenSettings(false); + setShowSideBar(false); }} > @@ -391,8 +447,8 @@ export function Home() { } onClick={() => { - if (confirm('删除选中的对话?')) { - removeSession(currentIndex) + if (confirm(Locale.Home.DeleteChat)) { + removeSession(currentIndex); } }} /> @@ -401,8 +457,8 @@ export function Home() { } onClick={() => { - setOpenSettings(true) - setShowSideBar(false) + setOpenSettings(true); + setShowSideBar(false); }} />
@@ -424,10 +480,12 @@ export function Home() {
{openSettings ? ( - { - setOpenSettings(false) - setShowSideBar(true) - }} /> + { + setOpenSettings(false); + setShowSideBar(true); + }} + /> ) : ( setShowSideBar(true)} /> )} diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index a3040c34..5c7925b0 100644 --- a/app/components/ui-lib.module.scss +++ b/app/components/ui-lib.module.scss @@ -63,6 +63,7 @@ background-color: var(--white); border-radius: 12px; width: 50vw; + animation: slide-in ease 0.3s; --modal-padding: 20px; @@ -111,6 +112,43 @@ } } +.show { + opacity: 1; + transition: all ease 0.3s; + transform: translateY(0); + position: fixed; + left: 0; + bottom: 0; + animation: slide-in ease 0.6s; + z-index: 99999; +} + +.hide { + opacity: 0; + transition: all ease 0.3s; + transform: translateY(20px); +} + +.toast-container { + position: fixed; + bottom: 0; + left: 0; + width: 100vw; + display: flex; + justify-content: center; + + .toast-content { + font-size: 14px; + background-color: var(--white); + box-shadow: var(--card-shadow); + border: var(--border-in-light); + color: var(--black); + padding: 10px 30px; + border-radius: 50px; + margin-bottom: 20px; + } +} + @media only screen and (max-width: 600px) { .modal-container { width: 90vw; @@ -119,4 +157,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 fff1af31..344d2068 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -1,7 +1,7 @@ import styles from "./ui-lib.module.scss"; import LoadingIcon from "../icons/three-dots.svg"; import CloseIcon from "../icons/close.svg"; -import { createRoot } from 'react-dom/client' +import { createRoot } from "react-dom/client"; export function Popover(props: { children: JSX.Element; @@ -41,50 +41,102 @@ export function List(props: { children: JSX.Element[] }) { } export function Loading() { - return
+ return ( +
+ +
+ ); } interface ModalProps { - title: string, - children?: JSX.Element, - actions?: JSX.Element[], - onClose?: () => void, + title: string; + children?: JSX.Element; + actions?: JSX.Element[]; + onClose?: () => void; } export function Modal(props: ModalProps) { - return
-
-
{props.title}
+ return ( +
+
+
{props.title}
-
- +
+ +
+
+ +
{props.children}
+ +
+
+ {props.actions?.map((action, i) => ( +
+ {action} +
+ ))} +
- -
{props.children}
- -
-
- {props.actions?.map((action, i) =>
{action}
)} -
-
-
+ ); } export function showModal(props: ModalProps) { - const div = document.createElement('div') + const div = document.createElement("div"); div.className = "modal-mask"; - document.body.appendChild(div) + document.body.appendChild(div); - const root = createRoot(div) - root.render( { + const root = createRoot(div); + const closeModal = () => { props.onClose?.(); root.unmount(); div.remove(); - }}>) -} \ No newline at end of file + }; + + div.onclick = (e) => { + if (e.target === div) { + closeModal(); + } + }; + + root.render(); +} + +export type ToastProps = { content: string }; + +export function Toast(props: ToastProps) { + return ( +
+
{props.content}
+
+ ); +} + +export function showToast(content: string, delay = 3000) { + const div = document.createElement("div"); + div.className = styles.show; + document.body.appendChild(div); + + const root = createRoot(div); + const close = () => { + div.classList.add(styles.hide); + + setTimeout(() => { + root.unmount(); + div.remove(); + }, 300); + }; + + setTimeout(() => { + close(); + }, delay); + + root.render(); +} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 52b279da..c7409ca0 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -26,6 +26,7 @@ const cn = { }, Home: { NewChat: '新的聊天', + DeleteChat: '确认删除选中的对话?', }, Settings: { Title: '设置', diff --git a/app/locales/en.ts b/app/locales/en.ts index d6e89293..55de3afd 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -27,6 +27,7 @@ const en: LocaleType = { }, Home: { NewChat: 'New Chat', + DeleteChat: 'Confirm to delete the selected conversation?', }, Settings: { Title: 'Settings', diff --git a/app/utils.ts b/app/utils.ts index 80136c6e..86298c98 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -1,4 +1,5 @@ -import Locale from './locales' +import { showToast } from "./components/ui-lib"; +import Locale from "./locales"; export function trimTopic(topic: string) { const s = topic.split(""); @@ -13,19 +14,25 @@ export function trimTopic(topic: string) { } export function copyToClipboard(text: string) { - navigator.clipboard.writeText(text).then(res => { - alert(Locale.Copy.Success) - }).catch(err => { - alert(Locale.Copy.Failed) - }) + navigator.clipboard + .writeText(text) + .then((res) => { + showToast(Locale.Copy.Success); + }) + .catch((err) => { + showToast(Locale.Copy.Failed); + }); } export function downloadAs(text: string, filename: string) { - const element = document.createElement('a'); - element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); - element.setAttribute('download', filename); + const element = document.createElement("a"); + element.setAttribute( + "href", + "data:text/plain;charset=utf-8," + encodeURIComponent(text) + ); + element.setAttribute("download", filename); - element.style.display = 'none'; + element.style.display = "none"; document.body.appendChild(element); element.click(); @@ -36,4 +43,16 @@ export function downloadAs(text: string, filename: string) { export function isIOS() { const userAgent = navigator.userAgent.toLowerCase(); return /iphone|ipad|ipod/.test(userAgent); -} \ No newline at end of file +} + +export function selectOrCopy(el: HTMLElement, content: string) { + const currentSelection = window.getSelection(); + + if (currentSelection?.type === "Range") { + return false; + } + + copyToClipboard(content); + + return true; +}