mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement: added scrolling navigation to logs + general improvements
This commit is contained in:
@@ -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<string | null>(null)
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(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 (
|
||||
<div
|
||||
@@ -195,19 +244,57 @@ export function Sidebar({ log, isOpen, onClose }: LogSidebarProps) {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b">
|
||||
<h2 className="text-base font-medium">Log Details</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="flex items-center space-x-1">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => hasPrev && handleNavigate(onNavigatePrev!)}
|
||||
disabled={!hasPrev}
|
||||
aria-label="Previous log"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Previous log (↑)</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => hasNext && handleNavigate(onNavigateNext!)}
|
||||
disabled={!hasNext}
|
||||
aria-label="Next log"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Next log (↓)</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollArea className="h-[calc(100vh-64px-49px)]">
|
||||
<ScrollArea className="h-[calc(100vh-64px-49px)]" ref={scrollAreaRef}>
|
||||
{' '}
|
||||
{/* Adjust for header height */}
|
||||
<div className="p-4 space-y-4">
|
||||
|
||||
+128
-6
@@ -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<WorkflowLog | null>(null)
|
||||
const [selectedLogIndex, setSelectedLogIndex] = useState<number>(-1)
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
|
||||
const selectedRowRef = useRef<HTMLTableRowElement | null>(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 (
|
||||
<div className="flex flex-col h-[100vh]">
|
||||
{/* Add the animation styles */}
|
||||
<style jsx global>
|
||||
{selectedRowAnimation}
|
||||
</style>
|
||||
|
||||
<ControlBar />
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<Filters />
|
||||
@@ -186,17 +287,25 @@ export default function Logs() {
|
||||
<tbody>
|
||||
{filteredLogs.map((log) => {
|
||||
const formattedDate = formatDate(log.createdAt)
|
||||
const isSelected = selectedLog?.id === log.id
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={log.id}
|
||||
className="border-b hover:bg-accent/30 transition-colors cursor-pointer"
|
||||
ref={isSelected ? selectedRowRef : null}
|
||||
className={`border-b transition-colors cursor-pointer ${
|
||||
isSelected
|
||||
? 'bg-accent/40 hover:bg-accent/50 border-l-2 selected-row'
|
||||
: 'hover:bg-accent/30'
|
||||
}`}
|
||||
onClick={() => handleLogClick(log)}
|
||||
>
|
||||
{/* Time column */}
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="text-xs font-medium flex items-center">
|
||||
<div
|
||||
className={`text-xs font-medium flex items-center ${isSelected ? 'text-foreground' : ''}`}
|
||||
>
|
||||
<span>{formattedDate.formatted}</span>
|
||||
<span className="mx-1.5 text-muted-foreground hidden xl:inline">
|
||||
•
|
||||
@@ -260,14 +369,19 @@ export default function Logs() {
|
||||
|
||||
{/* Message column */}
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-sm truncate" title={log.message}>
|
||||
<div
|
||||
className={`text-sm truncate ${isSelected ? 'text-foreground' : ''}`}
|
||||
title={log.message}
|
||||
>
|
||||
{log.message}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Duration column */}
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div
|
||||
className={`text-xs ${isSelected ? 'text-foreground' : 'text-muted-foreground'}`}
|
||||
>
|
||||
{log.duration || '—'}
|
||||
</div>
|
||||
</td>
|
||||
@@ -283,7 +397,15 @@ export default function Logs() {
|
||||
</div>
|
||||
|
||||
{/* Log Sidebar */}
|
||||
<Sidebar log={selectedLog} isOpen={isSidebarOpen} onClose={handleCloseSidebar} />
|
||||
<Sidebar
|
||||
log={selectedLog}
|
||||
isOpen={isSidebarOpen}
|
||||
onClose={handleCloseSidebar}
|
||||
onNavigateNext={handleNavigateNext}
|
||||
onNavigatePrev={handleNavigatePrev}
|
||||
hasNext={selectedLogIndex < filteredLogs.length - 1}
|
||||
hasPrev={selectedLogIndex > 0}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user