diff --git a/sim/app/w/logs/components/sidebar/sidebar.tsx b/sim/app/w/logs/components/sidebar/sidebar.tsx index f2befcefaf..7f9c7ed0aa 100644 --- a/sim/app/w/logs/components/sidebar/sidebar.tsx +++ b/sim/app/w/logs/components/sidebar/sidebar.tsx @@ -1,10 +1,11 @@ 'use client' -import { useEffect, useMemo, useState } from 'react' -import { X } from 'lucide-react' +import { useEffect, useMemo, useRef, useState } from 'react' +import { ChevronDown, ChevronUp, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { CopyButton } from '@/components/ui/copy-button' import { ScrollArea } from '@/components/ui/scroll-area' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { WorkflowLog } from '@/app/w/logs/stores/types' import { formatDate } from '@/app/w/logs/utils/format-date' import { ToolCallsDisplay } from '../tool-calls/tool-calls-display' @@ -14,6 +15,10 @@ interface LogSidebarProps { log: WorkflowLog | null isOpen: boolean onClose: () => void + onNavigateNext?: () => void + onNavigatePrev?: () => void + hasNext?: boolean + hasPrev?: boolean } /** @@ -113,15 +118,39 @@ const formatSingleJsonContent = (content: string): JSX.Element => { ) } -export function Sidebar({ log, isOpen, onClose }: LogSidebarProps) { +export function Sidebar({ + log, + isOpen, + onClose, + onNavigateNext, + onNavigatePrev, + hasNext = false, + hasPrev = false, +}: LogSidebarProps) { const [width, setWidth] = useState(400) // Default width from the original styles const [isDragging, setIsDragging] = useState(false) + const [currentLogId, setCurrentLogId] = useState(null) + const scrollAreaRef = useRef(null) + + // Update currentLogId when log changes + useEffect(() => { + if (log?.id) { + setCurrentLogId(log.id) + } + }, [log?.id]) const formattedContent = useMemo(() => { if (!log) return null return formatJsonContent(log.message) }, [log]) + // Reset scroll position when log changes + useEffect(() => { + if (scrollAreaRef.current) { + scrollAreaRef.current.scrollTop = 0 + } + }, [log?.id]) + // Determine if this is a workflow execution log const isWorkflowExecutionLog = useMemo(() => { if (!log) return false @@ -173,11 +202,31 @@ export function Sidebar({ log, isOpen, onClose }: LogSidebarProps) { if (e.key === 'Escape' && isOpen) { onClose() } + + // Add keyboard shortcuts for navigation + if (isOpen) { + // Up arrow key for previous log + if (e.key === 'ArrowUp' && hasPrev && onNavigatePrev) { + e.preventDefault() + handleNavigate(onNavigatePrev) + } + + // Down arrow key for next log + if (e.key === 'ArrowDown' && hasNext && onNavigateNext) { + e.preventDefault() + handleNavigate(onNavigateNext) + } + } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [isOpen, onClose]) + }, [isOpen, onClose, hasPrev, hasNext, onNavigatePrev, onNavigateNext]) + + // Handle navigation + const handleNavigate = (navigateFunction: () => void) => { + navigateFunction() + } return (

Log Details

- +
+ + + + + + Previous log (↑) + + + + + + + + + Next log (↓) + + + + +
{/* Content */} - + {' '} {/* Adjust for header height */}
diff --git a/sim/app/w/logs/logs.tsx b/sim/app/w/logs/logs.tsx index c627fa835f..fc271db160 100644 --- a/sim/app/w/logs/logs.tsx +++ b/sim/app/w/logs/logs.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { AlertCircle, Info, Loader2 } from 'lucide-react' import { createLogger } from '@/lib/logs/console-logger' import { ControlBar } from './components/control-bar/control-bar' @@ -31,10 +31,25 @@ const getTriggerBadgeStyles = (trigger: string) => { : 'bg-blue-100 dark:bg-blue-950/40 text-blue-700 dark:text-blue-400' } +// Add a new CSS class for the selected row animation +const selectedRowAnimation = ` + @keyframes borderPulse { + 0% { border-left-color: hsl(var(--primary) / 0.3); } + 50% { border-left-color: hsl(var(--primary) / 0.7); } + 100% { border-left-color: hsl(var(--primary) / 0.5); } + } + .selected-row { + animation: borderPulse 1s ease-in-out; + border-left-color: hsl(var(--primary) / 0.5); + } +` + export default function Logs() { const { filteredLogs, logs, loading, error, setLogs, setLoading, setError } = useFilterStore() const [selectedLog, setSelectedLog] = useState(null) + const [selectedLogIndex, setSelectedLogIndex] = useState(-1) const [isSidebarOpen, setIsSidebarOpen] = useState(false) + const selectedRowRef = useRef(null) // Group logs by executionId to identify the last log in each group const executionGroups = useMemo(() => { @@ -63,14 +78,45 @@ export default function Logs() { // Handle log click const handleLogClick = (log: WorkflowLog) => { setSelectedLog(log) + // Find the index of the clicked log in the filtered logs array + const index = filteredLogs.findIndex((l) => l.id === log.id) + setSelectedLogIndex(index) setIsSidebarOpen(true) } + // Navigate to the next log + const handleNavigateNext = () => { + if (selectedLogIndex < filteredLogs.length - 1) { + const nextIndex = selectedLogIndex + 1 + setSelectedLogIndex(nextIndex) + setSelectedLog(filteredLogs[nextIndex]) + } + } + + // Navigate to the previous log + const handleNavigatePrev = () => { + if (selectedLogIndex > 0) { + const prevIndex = selectedLogIndex - 1 + setSelectedLogIndex(prevIndex) + setSelectedLog(filteredLogs[prevIndex]) + } + } + // Close sidebar const handleCloseSidebar = () => { setIsSidebarOpen(false) } + // Scroll selected log into view when it changes + useEffect(() => { + if (selectedRowRef.current) { + selectedRowRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }) + } + }, [selectedLogIndex]) + // Fetch logs on component mount useEffect(() => { const fetchLogs = async () => { @@ -101,8 +147,63 @@ export default function Logs() { fetchLogs() }, [setLogs, setLoading, setError]) + // Add keyboard navigation for the logs table + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Only handle keyboard navigation if we have logs and a log is selected + if (filteredLogs.length === 0) return + + // If no log is selected yet, select the first one on arrow key press + if (selectedLogIndex === -1 && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) { + e.preventDefault() + setSelectedLogIndex(0) + setSelectedLog(filteredLogs[0]) + return + } + + // Up arrow key for previous log + if (e.key === 'ArrowUp' && !e.metaKey && !e.ctrlKey && selectedLogIndex > 0) { + e.preventDefault() + handleNavigatePrev() + } + + // Down arrow key for next log + if ( + e.key === 'ArrowDown' && + !e.metaKey && + !e.ctrlKey && + selectedLogIndex < filteredLogs.length - 1 + ) { + e.preventDefault() + handleNavigateNext() + } + + // Enter key to open/close sidebar + if (e.key === 'Enter' && selectedLog) { + e.preventDefault() + setIsSidebarOpen(!isSidebarOpen) + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [ + filteredLogs, + selectedLogIndex, + isSidebarOpen, + selectedLog, + handleNavigateNext, + handleNavigatePrev, + setIsSidebarOpen, + ]) + return (
+ {/* Add the animation styles */} + +
@@ -186,17 +287,25 @@ export default function Logs() { {filteredLogs.map((log) => { const formattedDate = formatDate(log.createdAt) + const isSelected = selectedLog?.id === log.id return ( handleLogClick(log)} > {/* Time column */}
-
+
{formattedDate.formatted} • @@ -260,14 +369,19 @@ export default function Logs() { {/* Message column */} -
+
{log.message}
{/* Duration column */} -
+
{log.duration || '—'}
@@ -283,7 +397,15 @@ export default function Logs() {
{/* Log Sidebar */} - + 0} + />
) }