Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
'use client'

import type React from 'react'
import { useRef, useState } from 'react'
import { useState } from 'react'
import { AlertCircle } from 'lucide-react'
import { createPortal } from 'react-dom'
import {
Expand DownExpand Up@@ -65,7 +65,6 @@ export function ExecutionSnapshot({

const [isMenuOpen, setIsMenuOpen] = useState(false)
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 })
const menuRef = useRef<HTMLDivElement>(null)

function closeMenu() {
setIsMenuOpen(false)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
'use client'

import { useState } from 'react'
import { createLogger } from '@sim/logger'
import { useRouter } from 'next/navigation'
import { Button, Loader } from '@/components/emcn'
import { Button } from '@/components/emcn'
import { Download } from '@/components/emcn/icons'
import { extractWorkspaceIdFromExecutionKey, getViewerUrl } from '@/lib/uploads/utils/file-utils'

Expand DownExpand Up@@ -41,14 +40,9 @@ function formatFileSize(bytes: number): string {
}

function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps) {
const [isDownloading, setIsDownloading] = useState(false)
const router = useRouter()

const handleDownload = () => {
if (isDownloading) return

setIsDownloading(true)

try {
logger.info(`Initiating download for file: ${file.name}`)

Expand DownExpand Up@@ -91,8 +85,6 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
}
} catch (error) {
logger.error(`Failed to download file ${file.name}:`, error)
} finally {
setIsDownloading(false)
}
}

Expand All@@ -113,14 +105,9 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
variant='ghost'
className='!h-[20px] !px-1.5 !py-0 text-xs'
onClick={handleDownload}
disabled={isDownloading}
>
{isDownloading ? (
<Loader className='mr-1 size-[10px]' animate />
) : (
<Download className='mr-1 size-[10px]' />
)}
{isDownloading ? 'Opening...' : 'Download'}
<Download className='mr-1 size-[10px]' />
Download
</Button>
</div>
</div>
Expand DownExpand Up@@ -148,84 +135,3 @@ export function FileCards({ files, isExecutionFile = false, workspaceId }: FileC
</div>
)
}

export function FileDownload({
file,
isExecutionFile = false,
className,
workspaceId,
}: {
file: FileData
isExecutionFile?: boolean
className?: string
workspaceId?: string
}) {
const [isDownloading, setIsDownloading] = useState(false)
const router = useRouter()

const handleDownload = () => {
if (isDownloading) return

setIsDownloading(true)

try {
logger.info(`Initiating download for file: ${file.name}`)

if (file.key.startsWith('url/')) {
if (file.url) {
window.open(file.url, '_blank')
logger.info(`Opened URL-type file directly: ${file.url}`)
return
}
throw new Error('URL is required for URL-type files')
}

let resolvedWorkspaceId = workspaceId
if (!resolvedWorkspaceId && isExecutionFile) {
resolvedWorkspaceId = extractWorkspaceIdFromExecutionKey(file.key) || undefined
} else if (!resolvedWorkspaceId) {
const segments = file.key.split('/')
if (segments.length >= 2 && /^[a-f0-9-]{36}$/.test(segments[0])) {
resolvedWorkspaceId = segments[0]
}
}

if (isExecutionFile) {
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
window.open(serveUrl, '_blank')
logger.info(`Opened execution file serve URL: ${serveUrl}`)
} else {
const viewerUrl = resolvedWorkspaceId ? getViewerUrl(file.key, resolvedWorkspaceId) : null

if (viewerUrl) {
router.push(viewerUrl)
logger.info(`Navigated to viewer URL: ${viewerUrl}`)
} else {
logger.warn(
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`
)
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
window.open(serveUrl, '_blank')
}
}
} catch (error) {
logger.error(`Failed to download file ${file.name}:`, error)
} finally {
setIsDownloading(false)
}
}

return (
<Button
variant='ghost'
className={`h-7 px-2 text-xs ${className}`}
onClick={handleDownload}
disabled={isDownloading}
>
{isDownloading ? <Loader className='size-3' animate /> : <Download className='size-[14px]' />}
{isDownloading ? 'Downloading...' : 'Download'}
</Button>
)
}

export default FileCards
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
export { FileCards, FileDownload } from './file-download'
export { FileCards } from './file-download'
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ import {
parseTime,
} from '@/app/workspace/[workspaceId]/logs/components/log-details/utils'
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'

const DEFAULT_TREE_PANE_WIDTH = 240
const MIN_TREE_PANE_WIDTH = 200
Expand DownExpand Up@@ -428,7 +429,7 @@ function DetailCodeSection({
const [isOpen, setIsOpen] = useState(defaultOpen)
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
const [copied, setCopied] = useState(false)
const { copied, copy } = useCopyToClipboard({ resetMs: 1500 })
const contentRef = useRef<HTMLDivElement>(null)

const {
Expand DownExpand Up@@ -459,9 +460,7 @@ function DetailCodeSection({
}

function handleCopy() {
navigator.clipboard.writeText(jsonString)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
copy(jsonString)
setIsContextMenuOpen(false)
}

Expand DownExpand Up@@ -819,6 +818,7 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa
*/
export const TraceView = memo(function TraceView({ traceSpans, runCostDollars }: TraceViewProps) {
const treeRef = useRef<HTMLDivElement>(null)
const { copied: traceCopied, copy: copyTrace } = useCopyToClipboard()
const [searchQuery, setSearchQuery] = useState('')
const [treePaneWidth, setTreePaneWidth] = useState(DEFAULT_TREE_PANE_WIDTH)
const treePaneWidthRef = useRef(DEFAULT_TREE_PANE_WIDTH)
Expand DownExpand Up@@ -1042,6 +1042,26 @@ export const TraceView = memo(function TraceView({ traceSpans, runCostDollars }:
placeholder='Filter spans'
className='w-[140px]'
/>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
type='button'
variant='ghost'
className='!p-1'
onClick={() => copyTrace(JSON.stringify(traceSpans, null, 2))}
aria-label='Copy raw trace'
>
{traceCopied ? (
<Check className='size-[12px] text-[var(--text-success)]' />
) : (
<Clipboard className='size-[12px]' />
)}
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{traceCopied ? 'Copied' : 'Copy raw trace'}
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ import {
TriggerBadge,
} from '@/app/workspace/[workspaceId]/logs/utils'
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { formatCost } from '@/providers/utils'
import { useLogDetailsUIStore } from '@/stores/logs/store'
Expand All@@ -60,8 +61,7 @@ function creditLabel(credits: number, dollars: number): string {
export const WorkflowOutputSection = memo(
function WorkflowOutputSection({ output }: { output: Record<string, unknown> }) {
const contentRef = useRef<HTMLDivElement>(null)
const [copied, setCopied] = useState(false)
const copyTimerRef = useRef<number | null>(null)
const { copied, copy } = useCopyToClipboard({ resetMs: 1500 })

const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
Expand DownExpand Up@@ -90,19 +90,10 @@ export const WorkflowOutputSection = memo(
}

function handleCopy() {
navigator.clipboard.writeText(jsonString)
setCopied(true)
if (copyTimerRef.current !== null) window.clearTimeout(copyTimerRef.current)
copyTimerRef.current = window.setTimeout(() => setCopied(false), 1500)
copy(jsonString)
setIsContextMenuOpen(false)
}

useEffect(() => {
return () => {
if (copyTimerRef.current !== null) window.clearTimeout(copyTimerRef.current)
}
}, [])

function handleSearch() {
activateSearch()
setIsContextMenuOpen(false)
Expand DownExpand Up@@ -273,22 +264,15 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
const [isExecutionSnapshotOpen, setIsExecutionSnapshotOpen] = useState(false)
const [activeTab, setActiveTab] = useState<LogDetailsTab>('overview')
const [prevLogId, setPrevLogId] = useState(log.id)
const [copiedRunId, setCopiedRunId] = useState(false)
const { copied: copiedRunId, copy: copyRunId } = useCopyToClipboard({ resetMs: 1500 })

if (prevLogId !== log.id) {
setPrevLogId(log.id)
setActiveTab('overview')
}

const copiedRunIdTimerRef = useRef<number | null>(null)
const scrollAreaRef = useRef<HTMLDivElement>(null)

useEffect(() => {
return () => {
if (copiedRunIdTimerRef.current !== null) window.clearTimeout(copiedRunIdTimerRef.current)
}
}, [])

const { config: permissionConfig } = usePermissionConfig()

useEffect(() => {
Expand DownExpand Up@@ -394,11 +378,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
...(showTraceTab ? [{ value: 'trace', label: 'Trace' }] : []),
]}
value={resolvedTab}
onChange={(v) => {
const tab = v as LogDetailsTab
setActiveTab(tab)
onActiveTabChange?.(tab)
}}
onChange={(v) => setActiveTab(v as LogDetailsTab)}
/>

{/* Overview Tab */}
Expand DownExpand Up@@ -442,25 +422,9 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
tabIndex={0}
aria-label='Copy run ID'
className='flex h-10 min-w-0 cursor-pointer items-center justify-between gap-4 px-3 transition-colors hover-hover:bg-[var(--surface-2)]'
onClick={() => {
navigator.clipboard.writeText(log.executionId!)
if (copiedRunIdTimerRef.current) clearTimeout(copiedRunIdTimerRef.current)
setCopiedRunId(true)
copiedRunIdTimerRef.current = window.setTimeout(
() => setCopiedRunId(false),
1500
)
}}
onClick={() => copyRunId(log.executionId!)}
onKeyDown={(event) =>
handleKeyboardActivation(event, () => {
navigator.clipboard.writeText(log.executionId!)
if (copiedRunIdTimerRef.current) clearTimeout(copiedRunIdTimerRef.current)
setCopiedRunId(true)
copiedRunIdTimerRef.current = window.setTimeout(
() => setCopiedRunId(false),
1500
)
})
handleKeyboardActivation(event, () => copyRunId(log.executionId!))
}
>
<span className='flex-shrink-0 font-medium text-[var(--text-tertiary)] text-caption'>
Expand Down
Loading