diff --git a/sim/app/w/[id]/components/control-bar/control-bar.tsx b/sim/app/w/[id]/components/control-bar/control-bar.tsx index 65e708a78e..75f6c9fdc9 100644 --- a/sim/app/w/[id]/components/control-bar/control-bar.tsx +++ b/sim/app/w/[id]/components/control-bar/control-bar.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { formatDistanceToNow } from 'date-fns' -import { Bell, History, Loader2, Play, Rocket, Store, Trash2 } from 'lucide-react' +import { Bell, ChevronDown, History, Loader2, Play, Rocket, Store, Trash2 } from 'lucide-react' import { AlertDialog, AlertDialogAction, @@ -22,6 +22,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { Progress } from '@/components/ui/progress' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { createLogger } from '@/lib/logs/console-logger' import { cn } from '@/lib/utils' @@ -35,6 +36,9 @@ import { NotificationDropdownItem } from './components/notification-dropdown-ite const logger = createLogger('ControlBar') +// Predefined run count options +const RUN_COUNT_OPTIONS = [1, 5, 10, 25, 50, 100] + /** * Control bar for managing workflows - handles editing, deletion, deployment, * history, notifications and execution. @@ -76,6 +80,12 @@ export function ControlBar() { // Marketplace modal state const [isMarketplaceModalOpen, setIsMarketplaceModalOpen] = useState(false) + // Multiple runs state + const [runCount, setRunCount] = useState(1) + const [completedRuns, setCompletedRuns] = useState(0) + const [isMultiRunning, setIsMultiRunning] = useState(false) + const [showRunProgress, setShowRunProgress] = useState(false) + // Get notifications for current workflow const workflowNotifications = activeWorkflowId ? getWorkflowNotifications(activeWorkflowId) @@ -286,6 +296,35 @@ export function ControlBar() { setIsMarketplaceModalOpen(true) } + /** + * Handle multiple workflow runs + */ + const handleMultipleRuns = async () => { + if (isExecuting || isMultiRunning || runCount <= 0) return + + // Reset state for a new batch of runs + setCompletedRuns(0) + setIsMultiRunning(true) + setShowRunProgress(runCount > 1) + + try { + // Run the workflow multiple times sequentially + for (let i = 0; i < runCount; i++) { + await handleRunWorkflow() + setCompletedRuns(i + 1) + } + } catch (error) { + logger.error('Error during multiple workflow runs:', { error }) + addNotification('error', 'Failed to complete all workflow runs', activeWorkflowId) + } finally { + setIsMultiRunning(false) + // Keep progress visible for a moment after completion + if (runCount > 1) { + setTimeout(() => setShowRunProgress(false), 2000) + } + } + } + /** * Render workflow name section (editable/non-editable) */ @@ -449,44 +488,51 @@ export function ControlBar() { /** * Render notifications dropdown */ - const renderNotificationsDropdown = () => ( - - - - - - - - {!notificationsOpen && Notifications} - + const renderNotificationsDropdown = () => { + // Ensure we're only showing notifications for the current workflow + const currentWorkflowNotifications = activeWorkflowId + ? notifications.filter((n) => n.workflowId === activeWorkflowId) + : [] - {workflowNotifications.length === 0 ? ( - - - No new notifications - - - ) : ( - - {[...workflowNotifications] - .sort((a, b) => b.timestamp - a.timestamp) - .map((notification) => ( - - ))} - - )} - - ) + return ( + + + + + + + + {!notificationsOpen && Notifications} + + + {currentWorkflowNotifications.length === 0 ? ( + + + No new notifications + + + ) : ( + + {[...currentWorkflowNotifications] + .sort((a, b) => b.timestamp - a.timestamp) + .map((notification) => ( + + ))} + + )} + + ) + } /** * Render publish button @@ -520,31 +566,78 @@ export function ControlBar() { ) /** - * Render run workflow button + * Render run workflow button with multi-run dropdown */ const renderRunButton = () => ( - + +
+ {/* Main Run Button */} + + + {/* Dropdown Trigger */} + + + + + + {RUN_COUNT_OPTIONS.map((count) => ( + setRunCount(count)} + className={cn('justify-center', runCount === count && 'bg-muted')} + > + {count} + + ))} + + +
+ ) return ( diff --git a/sim/app/w/[id]/components/notifications/notifications.tsx b/sim/app/w/[id]/components/notifications/notifications.tsx index 15f66b1848..c477be9777 100644 --- a/sim/app/w/[id]/components/notifications/notifications.tsx +++ b/sim/app/w/[id]/components/notifications/notifications.tsx @@ -18,6 +18,7 @@ import { createLogger } from '@/lib/logs/console-logger' import { cn } from '@/lib/utils' import { useNotificationStore } from '@/stores/notifications/store' import { Notification } from '@/stores/notifications/types' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('Notifications') @@ -92,19 +93,22 @@ function DeleteApiConfirmation({ export function NotificationList() { // Store access const { notifications, hideNotification } = useNotificationStore() + const { activeWorkflowId } = useWorkflowRegistry() // Local state const [fadingNotifications, setFadingNotifications] = useState>(new Set()) - // Filter to only show visible notifications - const visibleNotifications = notifications.filter((n) => n.isVisible) + // Filter to only show visible notifications for the current workflow + const visibleNotifications = notifications.filter( + (n) => n.isVisible && n.workflowId === activeWorkflowId + ) // Handle auto-dismissal of non-persistent notifications useEffect(() => { // Setup timers for each notification const timers: ReturnType[] = [] - notifications.forEach((notification) => { + visibleNotifications.forEach((notification) => { // Skip if already hidden or marked as persistent if (!notification.isVisible || notification.options?.isPersistent) return @@ -128,7 +132,7 @@ export function NotificationList() { // Cleanup timers on unmount or when notifications change return () => timers.forEach(clearTimeout) - }, [notifications, hideNotification]) + }, [visibleNotifications, hideNotification]) // Early return if no notifications to show if (visibleNotifications.length === 0) return null diff --git a/sim/components/ui/progress.tsx b/sim/components/ui/progress.tsx new file mode 100644 index 0000000000..e41fb24df8 --- /dev/null +++ b/sim/components/ui/progress.tsx @@ -0,0 +1,24 @@ +'use client' + +import * as React from 'react' +import * as ProgressPrimitive from '@radix-ui/react-progress' +import { cn } from '@/lib/utils' + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/sim/package-lock.json b/sim/package-lock.json index 957e593378..e312645e8a 100644 --- a/sim/package-lock.json +++ b/sim/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "sim", "version": "0.1.0", + "license": "Apache-2.0", "workspaces": [ "packages/@sim/*", "packages/simstudio" @@ -23,6 +24,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.5", + "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.2", @@ -3753,6 +3755,30 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz", + "integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", diff --git a/sim/package.json b/sim/package.json index a7d7050424..771d0e332f 100644 --- a/sim/package.json +++ b/sim/package.json @@ -38,6 +38,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.5", + "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.2",