feat(Toast): add Toast nearly identical to ChatGPT's (#1108)

This commit is contained in:
Danny Avila 2023-10-27 15:48:05 -04:00 committed by GitHub
parent ba5ab86037
commit 81a90d245b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 281 additions and 3 deletions

View file

@ -37,6 +37,7 @@
"@radix-ui/react-slider": "^1.1.1", "@radix-ui/react-slider": "^1.1.1",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.3", "@radix-ui/react-tabs": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.6", "@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-query": "^4.28.0", "@tanstack/react-query": "^4.28.0",
"@zattoo/use-double-click": "1.2.0", "@zattoo/use-double-click": "1.2.0",

View file

@ -1,8 +1,10 @@
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import * as RadixToast from '@radix-ui/react-toast';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
import { ScreenshotProvider, ThemeProvider, useApiErrorBoundary } from './hooks'; import { ScreenshotProvider, ThemeProvider, useApiErrorBoundary } from './hooks';
import Toast from './components/ui/Toast';
import { router } from './routes'; import { router } from './routes';
const App = () => { const App = () => {
@ -22,8 +24,12 @@ const App = () => {
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<RecoilRoot> <RecoilRoot>
<ThemeProvider> <ThemeProvider>
<RouterProvider router={router} /> <RadixToast.Provider>
<ReactQueryDevtools initialIsOpen={false} position="top-right" /> <RouterProvider router={router} />
<ReactQueryDevtools initialIsOpen={false} position="top-right" />
<Toast />
<RadixToast.Viewport className="pointer-events-none fixed inset-0 z-[60] mx-auto my-2 flex max-w-[560px] flex-col items-stretch justify-start md:pb-5" />
</RadixToast.Provider>
</ThemeProvider> </ThemeProvider>
</RecoilRoot> </RecoilRoot>
</QueryClientProvider> </QueryClientProvider>

View file

@ -21,6 +21,13 @@ export enum ESide {
Left = 'left', Left = 'left',
} }
export enum NotificationSeverity {
INFO = 'info',
SUCCESS = 'success',
WARNING = 'warning',
ERROR = 'error',
}
export type TBaseSettingsProps = { export type TBaseSettingsProps = {
conversation: TConversation | TPreset | null; conversation: TConversation | TPreset | null;
className?: string; className?: string;

View file

@ -0,0 +1,58 @@
import * as RadixToast from '@radix-ui/react-toast';
import { NotificationSeverity } from '~/common/types';
import { useToast } from '~/hooks';
export default function Toast() {
const { toast, onOpenChange } = useToast();
const severityClassName = {
[NotificationSeverity.INFO]: 'border-gray-500 bg-gray-500',
[NotificationSeverity.SUCCESS]: 'border-green-500 bg-green-500',
[NotificationSeverity.WARNING]: 'border-orange-500 bg-orange-500',
[NotificationSeverity.ERROR]: 'border-red-500 bg-red-500',
};
return (
<RadixToast.Root
open={toast.open}
onOpenChange={onOpenChange}
className="toast-root"
style={{
height: '74px',
marginBottom: '0px',
}}
>
<div className="w-full p-1 text-center md:w-auto md:text-justify">
<div
className={`alert-root pointer-events-auto inline-flex flex-row gap-2 rounded-md border px-3 py-2 text-white ${
severityClassName[toast.severity]
}`}
role="alert"
>
{toast.showIcon && (
<div className="mt-1 flex-shrink-0 flex-grow-0">
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="icon-sm"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
)}
<RadixToast.Description className="flex-1 justify-center gap-2">
<div className="whitespace-pre-wrap text-left">{toast.message}</div>
</RadixToast.Description>
</div>
</div>
</RadixToast.Root>
);
}

View file

@ -2,6 +2,7 @@ export * from './AuthContext';
export * from './ThemeContext'; export * from './ThemeContext';
export * from './ScreenshotContext'; export * from './ScreenshotContext';
export * from './ApiErrorBoundaryContext'; export * from './ApiErrorBoundaryContext';
export { default as useToast } from './useToast';
export { default as useTimeout } from './useTimeout'; export { default as useTimeout } from './useTimeout';
export { default as useUserKey } from './useUserKey'; export { default as useUserKey } from './useUserKey';
export { default as useDebounce } from './useDebounce'; export { default as useDebounce } from './useDebounce';

View file

@ -0,0 +1,43 @@
import { useRef, useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { NotificationSeverity } from '~/common';
import store from '~/store';
export default function useToast(timeoutDuration = 100) {
const [toast, setToast] = useRecoilState(store.toastState);
const timerRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
}
};
}, []);
type TShowToast = {
message: string;
severity?: NotificationSeverity;
showIcon?: boolean;
};
const showToast = ({
message,
severity = NotificationSeverity.SUCCESS,
showIcon = true,
}: TShowToast) => {
setToast({ ...toast, open: false });
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
}
timerRef.current = window.setTimeout(() => {
setToast({ open: true, message, severity, showIcon });
}, timeoutDuration);
};
return {
toast,
onOpenChange: (open: boolean) => setToast({ ...toast, open }),
showToast,
};
}

View file

@ -4,6 +4,7 @@ import endpoints from './endpoints';
import models from './models'; import models from './models';
import user from './user'; import user from './user';
import text from './text'; import text from './text';
import toast from './toast';
import submission from './submission'; import submission from './submission';
import search from './search'; import search from './search';
import preset from './preset'; import preset from './preset';
@ -17,6 +18,7 @@ export default {
...models, ...models,
...user, ...user,
...text, ...text,
...toast,
...submission, ...submission,
...search, ...search,
...preset, ...preset,

14
client/src/store/toast.ts Normal file
View file

@ -0,0 +1,14 @@
import { atom } from 'recoil';
import { NotificationSeverity } from '~/common';
const toastState = atom({
key: 'toastState',
default: {
open: false,
message: '',
severity: NotificationSeverity.SUCCESS,
showIcon: true,
},
});
export default { toastState };

View file

@ -1532,3 +1532,64 @@ button.scroll-convo {
.hidden-visibility { .hidden-visibility {
visibility: hidden; visibility: hidden;
} }
.toast-root {
align-items:center;
display:flex;
flex-direction:column;
height:0;
transition:all .24s cubic-bezier(0,0,.2,1)
}
.toast-root[data-state=open] {
-webkit-animation:toast-open .24s cubic-bezier(.175,.885,.32,1.175) both;
animation:toast-open .24s cubic-bezier(.175,.885,.32,1.175) both
}
.toast-root[data-state=closed] {
-webkit-animation:toast-close .12s cubic-bezier(.4,0,1,1) both;
animation:toast-close .12s cubic-bezier(.4,0,1,1) both
}
.toast-root .alert-root {
box-shadow:0 0 1px rgba(67,90,111,.3),0 5px 8px -4px rgba(67,90,111,.3);
flex-shrink:0;
pointer-events:all
}
@-webkit-keyframes toast-open {
0% {
opacity:0;
-webkit-transform:translateY(-100%);
transform:translateY(-100%)
}
to {
-webkit-transform:translateY(0);
transform:translateY(0)
}
}
@keyframes toast-open {
0% {
opacity:0;
-webkit-transform:translateY(-100%);
transform:translateY(-100%)
}
to {
-webkit-transform:translateY(0);
transform:translateY(0)
}
}
@-webkit-keyframes toast-close {
0% {
opacity:1
}
to {
opacity:0
}
}
@keyframes toast-close {
0% {
opacity:1
}
to {
opacity:0
}
}

87
package-lock.json generated
View file

@ -607,6 +607,7 @@
"@radix-ui/react-slider": "^1.1.1", "@radix-ui/react-slider": "^1.1.1",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.3", "@radix-ui/react-tabs": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.6", "@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-query": "^4.28.0", "@tanstack/react-query": "^4.28.0",
"@zattoo/use-double-click": "1.2.0", "@zattoo/use-double-click": "1.2.0",
@ -6331,6 +6332,90 @@
} }
} }
}, },
"node_modules/@radix-ui/react-toast": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz",
"integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-visually-hidden": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
"integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-escape-keydown": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
"integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": { "node_modules/@radix-ui/react-tooltip": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.6.tgz",
@ -23994,7 +24079,7 @@
}, },
"packages/data-provider": { "packages/data-provider": {
"name": "librechat-data-provider", "name": "librechat-data-provider",
"version": "0.2.0", "version": "0.2.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@tanstack/react-query": "^4.28.0", "@tanstack/react-query": "^4.28.0",