Merge pull request #1874 from janhq/feat/snackbar-component

This commit is contained in:
Faisal Amir 2024-01-31 21:00:38 +07:00 committed by GitHub
commit 71fcaa30f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 152 additions and 58 deletions

View File

@ -61,6 +61,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
toaster({ toaster({
title: 'Success!', title: 'Success!',
description: `Model ${model.id} has been started.`, description: `Model ${model.id} has been started.`,
type: 'success',
}) })
setStateModel(() => ({ setStateModel(() => ({
state: 'stop', state: 'stop',

View File

@ -82,7 +82,7 @@ const Providers = (props: PropsWithChildren) => {
</TooltipProvider> </TooltipProvider>
{!isMac && <GPUDriverPrompt />} {!isMac && <GPUDriverPrompt />}
</EventListenerWrapper> </EventListenerWrapper>
<Toaster position="top-right" /> <Toaster />
</FeatureToggleWrapper> </FeatureToggleWrapper>
</KeyListener> </KeyListener>
)} )}

View File

@ -6,7 +6,99 @@ import { twMerge } from 'tailwind-merge'
type Props = { type Props = {
title?: string title?: string
description?: string description?: string
type?: 'default' | 'error' | 'success' type?: 'default' | 'error' | 'success' | 'warning'
}
const ErrorIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20 10C20 15.5228 15.5228 20 10 20H0.993697C0.110179 20 -0.332289 18.9229 0.292453 18.2929L2.2495 16.3195C0.843343 14.597 1.21409e-08 12.397 1.21409e-08 10C1.21409e-08 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10ZM13.2071 6.79289C13.5976 7.18342 13.5976 7.81658 13.2071 8.20711L11.4142 10L13.2071 11.7929C13.5976 12.1834 13.5976 12.8166 13.2071 13.2071C12.8166 13.5976 12.1834 13.5976 11.7929 13.2071L10 11.4142L8.20711 13.2071C7.81658 13.5976 7.18342 13.5976 6.79289 13.2071C6.40237 12.8166 6.40237 12.1834 6.79289 11.7929L8.58579 10L6.79289 8.20711C6.40237 7.81658 6.40237 7.18342 6.79289 6.79289C7.18342 6.40237 7.81658 6.40237 8.20711 6.79289L10 8.58579L11.7929 6.79289C12.1834 6.40237 12.8166 6.40237 13.2071 6.79289Z"
fill="#EA2E4E"
/>
</svg>
)
}
const WarningIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20 10C20 15.5228 15.5228 20 10 20H0.993697C0.110179 20 -0.332289 18.9229 0.292453 18.2929L2.2495 16.3195C0.843343 14.597 1.21409e-08 12.397 1.21409e-08 10C1.21409e-08 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10ZM10.99 6C10.99 5.44772 10.5446 5 9.99502 5C9.44549 5 9 5.44772 9 6V10C9 10.5523 9.44549 11 9.99502 11C10.5446 11 10.99 10.5523 10.99 10V6ZM9.99502 13C9.44549 13 9 13.4477 9 14C9 14.5523 9.44549 15 9.99502 15H10.005C10.5545 15 11 14.5523 11 14C11 13.4477 10.5545 13 10.005 13H9.99502Z"
fill="#FACC15"
/>
</svg>
)
}
const SuccessIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20 10C20 15.5228 15.5228 20 10 20H0.993697C0.110179 20 -0.332289 18.9229 0.292453 18.2929L2.2495 16.3195C0.843343 14.597 1.21409e-08 12.397 1.21409e-08 10C1.21409e-08 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10ZM13.7071 8.70711C14.0976 8.31658 14.0976 7.68342 13.7071 7.29289C13.3166 6.90237 12.6834 6.90237 12.2929 7.29289L9 10.5858L7.70711 9.2929C7.31658 8.90237 6.68342 8.90237 6.29289 9.2929C5.90237 9.68342 5.90237 10.3166 6.29289 10.7071L8.29289 12.7071C8.48043 12.8946 8.73478 13 9 13C9.26522 13 9.51957 12.8946 9.70711 12.7071L13.7071 8.70711Z"
fill="#34D399"
/>
</svg>
)
}
const DefaultIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 2.11188e-08 4.47715 2.11188e-08 10C2.11188e-08 12.397 0.843343 14.597 2.2495 16.3195L0.292453 18.2929C-0.332289 18.9229 0.110179 20 0.993697 20H10ZM5.5 8C5.5 7.44772 5.94772 7 6.5 7H13.5C14.0523 7 14.5 7.44772 14.5 8C14.5 8.55229 14.0523 9 13.5 9H6.5C5.94772 9 5.5 8.55229 5.5 8ZM6.5 11C5.94772 11 5.5 11.4477 5.5 12C5.5 12.5523 5.94772 13 6.5 13H9.5C10.0523 13 10.5 12.5523 10.5 12C10.5 11.4477 10.0523 11 9.5 11H6.5Z"
fill="#60A5FA"
/>
</svg>
)
}
const renderIcon = (type: string) => {
switch (type) {
case 'warning':
return <WarningIcon />
case 'error':
return <ErrorIcon />
case 'success':
return <SuccessIcon />
default:
return <DefaultIcon />
}
} }
export function toaster(props: Props) { export function toaster(props: Props) {
@ -16,37 +108,52 @@ export function toaster(props: Props) {
return ( return (
<div <div
className={twMerge( className={twMerge(
'unset-drag relative flex min-w-[200px] max-w-[350px] gap-x-4 rounded-lg border border-border bg-background px-4 py-3', 'unset-drag dark:bg-zinc-white relative flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white dark:border dark:border-border',
t.visible ? 'animate-enter' : 'animate-leave', t.visible ? 'animate-enter' : 'animate-leave'
type === 'success' && 'bg-primary text-primary-foreground'
)} )}
> >
<div> <div className="flex items-start gap-x-3 dark:text-black">
<h1 <div className="mt-1">{renderIcon(type)}</div>
className={twMerge( <div className="pr-4">
'font-medium', <h1 className="font-bold">{title}</h1>
type === 'success' && 'font-medium text-primary-foreground' <p>{description}</p>
)}
>
{title}
</h1>
<p
className={twMerge(
'mt-1 text-muted-foreground',
type === 'success' && 'text-primary-foreground/80'
)}
>
{description}
</p>
</div> </div>
<XIcon <XIcon
size={24} size={24}
className="absolute right-2 top-2 w-4 cursor-pointer text-muted-foreground" className="absolute right-2 top-2 w-4 cursor-pointer dark:text-black"
onClick={() => toast.dismiss(t.id)} onClick={() => toast.dismiss(t.id)}
/> />
</div> </div>
</div>
) )
}, },
{ id: 'toast', duration: 3000 } { id: 'toast', duration: 2000, position: 'top-right' }
)
}
export function snackbar(props: Props) {
const { description, type = 'default' } = props
return toast.custom(
(t) => {
return (
<div
className={twMerge(
'unset-drag dark:bg-zinc-white relative bottom-2 flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white dark:border dark:border-border',
t.visible ? 'animate-enter' : 'animate-leave'
)}
>
<div className="flex items-start gap-x-3 dark:text-black">
<div>{renderIcon(type)}</div>
<p className="pr-4">{description}</p>
<XIcon
size={24}
className="absolute right-2 top-1/2 w-4 -translate-y-1/2 cursor-pointer dark:text-black"
onClick={() => toast.dismiss(t.id)}
/>
</div>
</div>
)
},
{ id: 'snackbar', duration: 2000, position: 'bottom-center' }
) )
} }

View File

@ -42,6 +42,7 @@ export function useActiveModel() {
toaster({ toaster({
title: `Model ${modelId} not found!`, title: `Model ${modelId} not found!`,
description: `Please download the model first.`, description: `Please download the model first.`,
type: 'warning',
}) })
setStateModel(() => ({ setStateModel(() => ({
state: 'start', state: 'start',

View File

@ -19,6 +19,7 @@ export default function useDeleteModel() {
toaster({ toaster({
title: 'Model Deletion Successful', title: 'Model Deletion Successful',
description: `The model ${model.id} has been successfully deleted.`, description: `The model ${model.id} has been successfully deleted.`,
type: 'success',
}) })
} }

View File

@ -86,6 +86,7 @@ export default function useDeleteThread() {
toaster({ toaster({
title: 'Thread successfully deleted.', title: 'Thread successfully deleted.',
description: `Thread ${threadId} has been successfully deleted.`, description: `Thread ${threadId} has been successfully deleted.`,
type: 'success',
}) })
} }

View File

@ -26,6 +26,7 @@ const setDownloadStateSuccessAtom = atom(null, (get, set, modelId: string) => {
toaster({ toaster({
title: 'Download Completed', title: 'Download Completed',
description: `Download ${modelId} completed`, description: `Download ${modelId} completed`,
type: 'success',
}) })
}) })
@ -61,6 +62,7 @@ const setDownloadStateCancelledAtom = atom(
toaster({ toaster({
title: 'Cancel Download', title: 'Cancel Download',
description: `Model ${modelId} cancel download`, description: `Model ${modelId} cancel download`,
type: 'warning',
}) })
return return

View File

@ -160,7 +160,7 @@ export default function useSendChatMessage() {
activeThread.assistants[0].model.id !== selectedModel?.id activeThread.assistants[0].model.id !== selectedModel?.id
) { ) {
if (!selectedModel) { if (!selectedModel) {
toaster({ title: 'Please select a model' }) toaster({ title: 'Please select a model', type: 'warning' })
return return
} }
const assistantId = activeThread.assistants[0].assistant_id ?? '' const assistantId = activeThread.assistants[0].assistant_id ?? ''

View File

@ -5,7 +5,7 @@ import { useDropzone } from 'react-dropzone'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { UploadCloudIcon, XIcon } from 'lucide-react' import { UploadCloudIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
@ -15,6 +15,8 @@ import ModelStart from '@/containers/Loader/ModelStart'
import { currentPromptAtom, fileUploadAtom } from '@/containers/Providers/Jotai' import { currentPromptAtom, fileUploadAtom } from '@/containers/Providers/Jotai'
import { showLeftSideBarAtom } from '@/containers/Providers/KeyListener' import { showLeftSideBarAtom } from '@/containers/Providers/KeyListener'
import { snackbar } from '@/containers/Toast'
import { queuedMessageAtom, reloadModelAtom } from '@/hooks/useSendChatMessage' import { queuedMessageAtom, reloadModelAtom } from '@/hooks/useSendChatMessage'
import ChatBody from '@/screens/Chat/ChatBody' import ChatBody from '@/screens/Chat/ChatBody'
@ -112,8 +114,13 @@ const ChatScreen: React.FC = () => {
}, },
}) })
// TODO @faisal change this until we have sneakbar component
useEffect(() => { useEffect(() => {
if (dragRejected.code) {
snackbar({
description: renderError(dragRejected.code),
type: 'error',
})
}
setTimeout(() => { setTimeout(() => {
if (dragRejected.code) { if (dragRejected.code) {
setDragRejected({ code: '' }) setDragRejected({ code: '' })
@ -134,33 +141,6 @@ const ChatScreen: React.FC = () => {
className="relative flex h-full w-full flex-col overflow-auto bg-background outline-none" className="relative flex h-full w-full flex-col overflow-auto bg-background outline-none"
{...getRootProps()} {...getRootProps()}
> >
{dragRejected.code !== '' && (
<div className="absolute bottom-3 left-1/2 z-50 inline-flex w-full -translate-x-1/2 justify-center px-16">
<div className="flex items-start justify-between gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white dark:border dark:border-border dark:bg-zinc-900">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20 10C20 15.5228 15.5228 20 10 20H0.993697C0.110179 20 -0.332289 18.9229 0.292453 18.2929L2.2495 16.3195C0.843343 14.597 1.21409e-08 12.397 1.21409e-08 10C1.21409e-08 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10ZM13.2071 6.79289C13.5976 7.18342 13.5976 7.81658 13.2071 8.20711L11.4142 10L13.2071 11.7929C13.5976 12.1834 13.5976 12.8166 13.2071 13.2071C12.8166 13.5976 12.1834 13.5976 11.7929 13.2071L10 11.4142L8.20711 13.2071C7.81658 13.5976 7.18342 13.5976 6.79289 13.2071C6.40237 12.8166 6.40237 12.1834 6.79289 11.7929L8.58579 10L6.79289 8.20711C6.40237 7.81658 6.40237 7.18342 6.79289 6.79289C7.18342 6.40237 7.81658 6.40237 8.20711 6.79289L10 8.58579L11.7929 6.79289C12.1834 6.40237 12.8166 6.40237 13.2071 6.79289Z"
fill="#F87171"
/>
</svg>
<p>{renderError(dragRejected.code)}</p>
<XIcon
size={24}
className="cursor-pointer"
onClick={() => setDragRejected({ code: '' })}
/>
</div>
</div>
)}
{dragOver && ( {dragOver && (
<div className="absolute z-50 mx-auto h-full w-full bg-background/50 p-8 backdrop-blur-lg"> <div className="absolute z-50 mx-auto h-full w-full bg-background/50 p-8 backdrop-blur-lg">
<div <div

View File

@ -65,6 +65,7 @@ const Advanced = () => {
toaster({ toaster({
title: 'Logs cleared', title: 'Logs cleared',
description: 'All logs have been cleared.', description: 'All logs have been cleared.',
type: 'success',
}) })
} }