mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
Added notifications and check notification history
This commit is contained in:
+54
-74
@@ -26,6 +26,8 @@ import { initializeStateLogger } from '@/stores/workflow/state-logger'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { Executor } from '@/executor'
|
||||
import { BlockState, SubBlockState } from '@/stores/workflow/types'
|
||||
import { NotificationList } from '@/app/w/components/notifications/notifications'
|
||||
import { useNotificationStore } from '@/stores/notifications/notifications-store'
|
||||
|
||||
/**
|
||||
* Represents the data structure for a workflow node
|
||||
@@ -104,6 +106,7 @@ function WorkflowCanvas() {
|
||||
} = useWorkflowStore()
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
const [executionResult, setExecutionResult] = useState<any>(null)
|
||||
const { addNotification } = useNotificationStore()
|
||||
|
||||
// Convert blocks to ReactFlow nodes
|
||||
const nodes = Object.values(blocks).map((block) => ({
|
||||
@@ -240,24 +243,26 @@ function WorkflowCanvas() {
|
||||
* @throws {Error} If no initial block is found or block configuration is invalid
|
||||
*/
|
||||
const getInitialNode = () => {
|
||||
const initialBlockId = Object.values(blocks).find(block =>
|
||||
!edges.some(edge => edge.target === block.id)
|
||||
)?.id;
|
||||
const initialBlockId = Object.values(blocks).find(
|
||||
(block) => !edges.some((edge) => edge.target === block.id)
|
||||
)?.id
|
||||
|
||||
if (!initialBlockId) {
|
||||
throw new Error('Could not determine the initial block in the workflow');
|
||||
throw new Error('Could not determine the initial block in the workflow')
|
||||
}
|
||||
|
||||
const blockConfig = getBlock(blocks[initialBlockId].type);
|
||||
const blockConfig = getBlock(blocks[initialBlockId].type)
|
||||
if (!blockConfig) {
|
||||
throw new Error(`Block configuration not found for type: ${blocks[initialBlockId].type}`);
|
||||
throw new Error(
|
||||
`Block configuration not found for type: ${blocks[initialBlockId].type}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
block: blocks[initialBlockId],
|
||||
config: blockConfig
|
||||
};
|
||||
};
|
||||
config: blockConfig,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the initial input parameters based on the block type
|
||||
@@ -272,18 +277,18 @@ function WorkflowCanvas() {
|
||||
systemPrompt: block.subBlocks?.['systemPrompt']?.value,
|
||||
temperature: block.subBlocks?.['temperature']?.value,
|
||||
apiKey: block.subBlocks?.['apiKey']?.value,
|
||||
prompt: block.subBlocks?.['systemPrompt']?.value
|
||||
};
|
||||
prompt: block.subBlocks?.['systemPrompt']?.value,
|
||||
}
|
||||
} else if (block.type === 'api') {
|
||||
return {
|
||||
url: block.subBlocks?.['url']?.value,
|
||||
method: block.subBlocks?.['method']?.value,
|
||||
headers: block.subBlocks?.['headers']?.value,
|
||||
body: block.subBlocks?.['body']?.value
|
||||
};
|
||||
body: block.subBlocks?.['body']?.value,
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a block into the format expected by the executor
|
||||
@@ -292,23 +297,23 @@ function WorkflowCanvas() {
|
||||
* @throws {Error} If block configuration or tools are not properly defined
|
||||
*/
|
||||
const serializeBlock = (block: BlockState) => {
|
||||
const blockConfig = getBlock(block.type);
|
||||
const blockConfig = getBlock(block.type)
|
||||
if (!blockConfig) {
|
||||
throw new Error(`Block configuration not found for type: ${block.type}`);
|
||||
throw new Error(`Block configuration not found for type: ${block.type}`)
|
||||
}
|
||||
|
||||
const tools = blockConfig.workflow.tools;
|
||||
const tools = blockConfig.workflow.tools
|
||||
if (!tools || !tools.access || tools.access.length === 0) {
|
||||
throw new Error(`No tools specified for block type: ${block.type}`);
|
||||
throw new Error(`No tools specified for block type: ${block.type}`)
|
||||
}
|
||||
|
||||
// Get the values from subBlocks
|
||||
const params: Record<string, any> = {};
|
||||
const params: Record<string, any> = {}
|
||||
Object.entries(block.subBlocks || {}).forEach(([id, subBlock]) => {
|
||||
if (subBlock) {
|
||||
params[id] = subBlock.value;
|
||||
params[id] = subBlock.value
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return {
|
||||
id: block.id,
|
||||
@@ -320,15 +325,15 @@ function WorkflowCanvas() {
|
||||
interface: {
|
||||
inputs: block.type === 'agent' ? { prompt: 'string' } : {},
|
||||
outputs: {
|
||||
[block.type === 'agent' ? 'response' : 'output']:
|
||||
typeof blockConfig.workflow.outputType === 'string'
|
||||
? blockConfig.workflow.outputType
|
||||
: blockConfig.workflow.outputType.default
|
||||
}
|
||||
}
|
||||
[block.type === 'agent' ? 'response' : 'output']:
|
||||
typeof blockConfig.workflow.outputType === 'string'
|
||||
? blockConfig.workflow.outputType
|
||||
: blockConfig.workflow.outputType.default,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the execution of the workflow
|
||||
@@ -340,19 +345,20 @@ function WorkflowCanvas() {
|
||||
setExecutionResult(null)
|
||||
|
||||
// 1. Get initial node
|
||||
const { block: initialBlock, config: initialBlockConfig } = getInitialNode();
|
||||
const { block: initialBlock, config: initialBlockConfig } =
|
||||
getInitialNode()
|
||||
|
||||
// 2. Serialize the workflow
|
||||
const serializer = new Serializer()
|
||||
const serializedWorkflow = serializer.serializeWorkflow(
|
||||
Object.values(blocks).map(serializeBlock),
|
||||
edges
|
||||
);
|
||||
)
|
||||
|
||||
// 3. Create executor and run workflow
|
||||
const executor = new Executor(serializedWorkflow)
|
||||
const initialInput = getInitialInput(initialBlock, initialBlockConfig);
|
||||
|
||||
const initialInput = getInitialInput(initialBlock, initialBlockConfig)
|
||||
|
||||
const result = await executor.execute(
|
||||
window.location.pathname.split('/').pop() || 'workflow',
|
||||
initialInput
|
||||
@@ -360,63 +366,37 @@ function WorkflowCanvas() {
|
||||
|
||||
// 4. Handle result
|
||||
setExecutionResult(result)
|
||||
|
||||
|
||||
if (result.success) {
|
||||
console.log('Workflow executed successfully:', result.data)
|
||||
addNotification('console', 'Workflow completed successfully')
|
||||
} else {
|
||||
console.error('Workflow execution failed:', result.error)
|
||||
addNotification('error', `Failed to execute workflow: ${result.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Error executing workflow:', error)
|
||||
setExecutionResult({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
})
|
||||
addNotification('error', `Failed to execute workflow: ${error.message}`)
|
||||
} finally {
|
||||
setIsExecuting(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const testNotification = () => {
|
||||
addNotification('console', 'Response: hey')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-[calc(100vh-56px)]">
|
||||
<NotificationList />
|
||||
<style>{keyframeStyles}</style>
|
||||
|
||||
{/* Run Button */}
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<button
|
||||
onClick={handleRunWorkflow}
|
||||
disabled={isExecuting || Object.keys(blocks).length === 0}
|
||||
className={`px-4 py-2 rounded-md text-white ${
|
||||
isExecuting
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-500 hover:bg-blue-600'
|
||||
}`}
|
||||
>
|
||||
{isExecuting ? 'Running...' : 'Run Workflow'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Execution Result */}
|
||||
{executionResult && (
|
||||
<div className={`absolute top-16 right-4 z-10 p-4 rounded-md ${
|
||||
executionResult.success ? 'bg-green-100' : 'bg-red-100'
|
||||
}`}>
|
||||
{executionResult.success ? (
|
||||
<div>
|
||||
<h3 className="font-bold text-green-800">Success</h3>
|
||||
<pre className="mt-2 text-sm">
|
||||
{JSON.stringify(executionResult.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h3 className="font-bold text-red-800">Error</h3>
|
||||
<p className="mt-2 text-sm text-red-600">{executionResult.error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button onClick={testNotification}>Test Notification</button>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { Terminal, AlertCircle } from 'lucide-react'
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
NotificationType,
|
||||
useNotificationStore,
|
||||
} from '@/stores/notifications/notifications-store'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ErrorIcon } from '@/components/icons'
|
||||
|
||||
interface NotificationDropdownItemProps {
|
||||
id: string
|
||||
type: NotificationType
|
||||
message: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const NotificationIcon = {
|
||||
error: ErrorIcon,
|
||||
console: Terminal,
|
||||
}
|
||||
|
||||
const NotificationColors = {
|
||||
error: 'text-destructive',
|
||||
console: 'text-foreground',
|
||||
}
|
||||
|
||||
export function NotificationDropdownItem({
|
||||
id,
|
||||
type,
|
||||
message,
|
||||
timestamp,
|
||||
}: NotificationDropdownItemProps) {
|
||||
const { showNotification } = useNotificationStore()
|
||||
const Icon = NotificationIcon[type]
|
||||
const timeAgo = formatDistanceToNow(timestamp, { addSuffix: true })
|
||||
|
||||
// Truncate message if it's too long
|
||||
const truncatedMessage =
|
||||
message.length > 50 ? `${message.slice(0, 50)}...` : message
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className="flex items-start gap-2 p-3 cursor-pointer"
|
||||
onClick={() => showNotification(id)}
|
||||
>
|
||||
<Icon className={cn('h-4 w-4', NotificationColors[type])} />
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium">
|
||||
{type === 'error' ? 'Error' : 'Console'}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{timeAgo}</span>
|
||||
</div>
|
||||
<p className="text-sm text-foreground">{truncatedMessage}</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -6,8 +8,12 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { History, Bell, Play } from 'lucide-react'
|
||||
import { useNotificationStore } from '@/stores/notifications/notifications-store'
|
||||
import { NotificationDropdownItem } from './components/notification-dropdown-item'
|
||||
|
||||
export function ControlBar() {
|
||||
const { notifications } = useNotificationStore()
|
||||
|
||||
return (
|
||||
<div className="flex h-16 w-full items-center justify-between bg-background px-6 border-b">
|
||||
{/* Left Section - Workflow Info */}
|
||||
@@ -41,9 +47,28 @@ export function ControlBar() {
|
||||
<span className="sr-only">Notifications</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>No new notifications</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
{notifications.length === 0 ? (
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem className="text-sm text-muted-foreground">
|
||||
No new notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
) : (
|
||||
<DropdownMenuContent align="end" className="w-60">
|
||||
{[...notifications]
|
||||
.sort((a, b) => b.timestamp - a.timestamp)
|
||||
.map((notification) => (
|
||||
<NotificationDropdownItem
|
||||
id={notification.id}
|
||||
key={notification.id}
|
||||
type={notification.type}
|
||||
message={notification.message}
|
||||
timestamp={notification.timestamp}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
|
||||
<Button className="gap-2 bg-[#7F2FFF] hover:bg-[#7F2FFF]/90">
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertCircle, Terminal } from 'lucide-react'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { useNotificationStore } from '@/stores/notifications/notifications-store'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ErrorIcon } from '@/components/icons'
|
||||
|
||||
const NOTIFICATION_TIMEOUT = 3000
|
||||
const FADE_DURATION = 300
|
||||
|
||||
const NotificationIcon = {
|
||||
error: ErrorIcon,
|
||||
console: Terminal,
|
||||
}
|
||||
|
||||
const NotificationColors = {
|
||||
error:
|
||||
'border-red-500 bg-red-50 text-destructive dark:border-border dark:text-foreground dark:bg-background',
|
||||
console:
|
||||
'border-border bg-background text-foreground dark:border-border dark:text-foreground dark:bg-background',
|
||||
}
|
||||
|
||||
export function NotificationList() {
|
||||
const { notifications, hideNotification } = useNotificationStore()
|
||||
const [fadingNotifications, setFadingNotifications] = useState<Set<string>>(
|
||||
new Set()
|
||||
)
|
||||
|
||||
// Only show visible notifications in the display
|
||||
const visibleNotifications = notifications.filter((n) => n.isVisible)
|
||||
|
||||
useEffect(() => {
|
||||
notifications.forEach((notification) => {
|
||||
if (!notification.isVisible) return
|
||||
|
||||
// Start fade out
|
||||
const fadeTimer = setTimeout(() => {
|
||||
setFadingNotifications((prev) => new Set([...prev, notification.id]))
|
||||
}, NOTIFICATION_TIMEOUT)
|
||||
|
||||
// Hide notification after fade
|
||||
const hideTimer = setTimeout(() => {
|
||||
hideNotification(notification.id)
|
||||
setFadingNotifications((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.delete(notification.id)
|
||||
return next
|
||||
})
|
||||
}, NOTIFICATION_TIMEOUT + FADE_DURATION)
|
||||
|
||||
return () => {
|
||||
clearTimeout(fadeTimer)
|
||||
clearTimeout(hideTimer)
|
||||
}
|
||||
})
|
||||
}, [notifications, hideNotification])
|
||||
|
||||
if (visibleNotifications.length === 0) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute left-1/2 z-50 space-y-2 max-w-md w-full"
|
||||
style={{
|
||||
top: '30px',
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
>
|
||||
{visibleNotifications.map((notification) => {
|
||||
const Icon = NotificationIcon[notification.type]
|
||||
const isFading = fadingNotifications.has(notification.id)
|
||||
|
||||
return (
|
||||
<Alert
|
||||
key={notification.id}
|
||||
className={cn(
|
||||
'transition-all duration-300 ease-in-out opacity-0 translate-y-[-100%]',
|
||||
isFading
|
||||
? 'animate-notification-fade-out'
|
||||
: 'animate-notification-slide',
|
||||
NotificationColors[notification.type]
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cn('h-4 w-4', {
|
||||
'!text-red-500': notification.type === 'error',
|
||||
'text-foreground': notification.type === 'console',
|
||||
})}
|
||||
/>
|
||||
<AlertTitle className="ml-2">
|
||||
{notification.type === 'error' ? 'Error' : 'Console'}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="ml-2">
|
||||
{notification.message}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
import { NavItem } from './nav-item'
|
||||
import { NavItem } from './components/nav-item'
|
||||
import { Settings, Plus } from 'lucide-react'
|
||||
import {
|
||||
Tooltip,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ToolbarTabs } from './toolbar-tabs'
|
||||
import { ToolbarTabs } from './components/toolbar-tabs'
|
||||
import { ToolbarBlock } from '../toolbar-block/toolbar-block'
|
||||
import { getBlocksByCategory, BlockCategory } from '../../../../blocks'
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { SubBlockConfig } from '../../../../../blocks/types/block'
|
||||
import { SubBlockConfig } from '../../../../../../blocks/types/block'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ShortInput } from './components/short-input'
|
||||
import { LongInput } from './components/long-input'
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { BlockConfig, SubBlockConfig } from '../../../../blocks/types/block'
|
||||
import { SubBlock } from './sub-block/sub-block'
|
||||
import { SubBlock } from './components/sub-block/sub-block'
|
||||
import { Handle, Position } from 'reactflow'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ActionBar } from './action-block/action-block'
|
||||
import { ActionBar } from './components/action-block/action-block'
|
||||
|
||||
interface WorkflowBlockProps {
|
||||
id: string
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
Generated
+11
@@ -20,6 +20,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.469.0",
|
||||
"next": "15.1.3",
|
||||
"react": "^18.2.0",
|
||||
@@ -4671,6 +4672,16 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.469.0",
|
||||
"next": "15.1.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
export type NotificationType = 'error' | 'console'
|
||||
|
||||
export interface Notification {
|
||||
id: string
|
||||
type: NotificationType
|
||||
message: string
|
||||
timestamp: number
|
||||
isVisible: boolean
|
||||
}
|
||||
|
||||
interface NotificationStore {
|
||||
notifications: Notification[]
|
||||
addNotification: (type: NotificationType, message: string) => void
|
||||
hideNotification: (id: string) => void
|
||||
showNotification: (id: string) => void
|
||||
removeNotification: (id: string) => void
|
||||
clearNotifications: () => void
|
||||
}
|
||||
|
||||
export const useNotificationStore = create<NotificationStore>()(
|
||||
devtools(
|
||||
(set) => ({
|
||||
notifications: [],
|
||||
addNotification: (type, message) => {
|
||||
const notification: Notification = {
|
||||
id: crypto.randomUUID(),
|
||||
type,
|
||||
message,
|
||||
timestamp: Date.now(),
|
||||
isVisible: true,
|
||||
}
|
||||
set((state) => ({
|
||||
notifications: [...state.notifications, notification],
|
||||
}))
|
||||
},
|
||||
hideNotification: (id) =>
|
||||
set((state) => ({
|
||||
notifications: state.notifications.map((n) =>
|
||||
n.id === id ? { ...n, isVisible: false } : n
|
||||
),
|
||||
})),
|
||||
showNotification: (id) =>
|
||||
set((state) => ({
|
||||
notifications: state.notifications.map((n) =>
|
||||
n.id === id ? { ...n, isVisible: true } : n
|
||||
),
|
||||
})),
|
||||
removeNotification: (id) =>
|
||||
set((state) => ({
|
||||
notifications: state.notifications.filter((n) => n.id !== id),
|
||||
})),
|
||||
clearNotifications: () => set({ notifications: [] }),
|
||||
}),
|
||||
{ name: 'notification-store' }
|
||||
)
|
||||
)
|
||||
+32
-1
@@ -55,7 +55,38 @@ export default {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
}
|
||||
},
|
||||
keyframes: {
|
||||
'slide-down': {
|
||||
'0%': { transform: 'translate(-50%, -100%)', opacity: '0' },
|
||||
'100%': { transform: 'translate(-50%, 0)', opacity: '1' },
|
||||
},
|
||||
'notification-slide': {
|
||||
'0%': {
|
||||
opacity: '0',
|
||||
transform: 'translateY(-100%)'
|
||||
},
|
||||
'100%': {
|
||||
opacity: '1',
|
||||
transform: 'translateY(0)'
|
||||
},
|
||||
},
|
||||
'notification-fade-out': {
|
||||
'0%': {
|
||||
opacity: '1',
|
||||
transform: 'translateY(0)'
|
||||
},
|
||||
'100%': {
|
||||
opacity: '0',
|
||||
transform: 'translateY(0)'
|
||||
},
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'slide-down': 'slide-down 0.3s ease-out',
|
||||
'notification-slide': 'notification-slide 0.3s ease-out forwards',
|
||||
'notification-fade-out': 'notification-fade-out 0.2s ease-out forwards'
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
|
||||
Reference in New Issue
Block a user