fix(formatting): consolidate duration formatting into shared utility

This commit is contained in:
waleed
2026-02-02 14:53:14 -08:00
parent a9b7d75d87
commit 7386d227eb
10 changed files with 46 additions and 80 deletions

View File

@@ -6,11 +6,11 @@ import Link from 'next/link'
import { List, type RowComponentProps, useListRef } from 'react-window'
import { Badge, buttonVariants } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { formatDuration } from '@/lib/core/utils/formatting'
import {
DELETED_WORKFLOW_COLOR,
DELETED_WORKFLOW_LABEL,
formatDate,
formatDuration,
getDisplayStatus,
LOG_COLUMNS,
StatusBadge,

View File

@@ -1,6 +1,7 @@
import React from 'react'
import { format } from 'date-fns'
import { Badge } from '@/components/emcn'
import { formatDuration } from '@/lib/core/utils/formatting'
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
import { getBlock } from '@/blocks/registry'
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
@@ -362,47 +363,14 @@ export function mapToExecutionLogAlt(log: RawLogResponse): ExecutionLog {
}
}
/**
* Format duration for display in logs UI
* If duration is under 1 second, displays as milliseconds (e.g., "500ms")
* If duration is 1 second or more, displays as seconds (e.g., "1.23s")
* @param duration - Duration string (e.g., "500ms") or null
* @returns Formatted duration string or null
*/
export function formatDuration(duration: string | null): string | null {
if (!duration) return null
// Extract numeric value from duration string (e.g., "500ms" -> 500)
const ms = Number.parseInt(duration.replace(/[^0-9]/g, ''), 10)
if (!Number.isFinite(ms)) return duration
if (ms < 1000) {
return `${ms}ms`
}
// Convert to seconds with up to 2 decimal places
const seconds = ms / 1000
return `${seconds.toFixed(2).replace(/\.?0+$/, '')}s`
}
/**
* Format latency value for display in dashboard UI
* If latency is under 1 second, displays as milliseconds (e.g., "500ms")
* If latency is 1 second or more, displays as seconds (e.g., "1.23s")
* @param ms - Latency in milliseconds (number)
* @returns Formatted latency string
*/
export function formatLatency(ms: number): string {
if (!Number.isFinite(ms) || ms <= 0) return '—'
if (ms < 1000) {
return `${Math.round(ms)}ms`
}
// Convert to seconds with up to 2 decimal places
const seconds = ms / 1000
return `${seconds.toFixed(2).replace(/\.?0+$/, '')}s`
return formatDuration(ms, { precision: 2 })
}
export const formatDate = (dateString: string) => {

View File

@@ -3,6 +3,7 @@
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import clsx from 'clsx'
import { ChevronUp } from 'lucide-react'
import { formatDuration } from '@/lib/core/utils/formatting'
import { CopilotMarkdownRenderer } from '../markdown-renderer'
/** Removes thinking tags (raw or escaped) and special tags from streamed content */
@@ -241,15 +242,9 @@ export function ThinkingBlock({
return () => window.clearInterval(intervalId)
}, [isStreaming, isExpanded, userHasScrolledAway])
/** Formats duration in milliseconds to seconds (minimum 1s) */
const formatDuration = (ms: number) => {
const seconds = Math.max(1, Math.round(ms / 1000))
return `${seconds}s`
}
const hasContent = cleanContent.length > 0
const isThinkingDone = !isStreaming || hasFollowingContent || hasSpecialTags
const durationText = `${label} for ${formatDuration(duration)}`
const durationText = `${label} for ${formatDuration(Math.max(1000, duration))}`
const getStreamingLabel = (lbl: string) => {
if (lbl === 'Thought') return 'Thinking'

View File

@@ -15,6 +15,7 @@ import {
hasInterrupt as hasInterruptFromConfig,
isSpecialTool as isSpecialToolFromConfig,
} from '@/lib/copilot/tools/client/ui-config'
import { formatDuration } from '@/lib/core/utils/formatting'
import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming'
import { ThinkingBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block'
@@ -848,13 +849,8 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({
(allParsed.options && Object.keys(allParsed.options).length > 0)
)
const formatDuration = (ms: number) => {
const seconds = Math.max(1, Math.round(ms / 1000))
return `${seconds}s`
}
const outerLabel = getSubagentCompletionLabel(toolCall.name)
const durationText = `${outerLabel} for ${formatDuration(duration)}`
const durationText = `${outerLabel} for ${formatDuration(Math.max(1000, duration))}`
const renderCollapsibleContent = () => (
<>

View File

@@ -24,6 +24,7 @@ import {
Tooltip,
} from '@/components/emcn'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { formatDuration } from '@/lib/core/utils/formatting'
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
import {
@@ -43,7 +44,6 @@ import {
type EntryNode,
type ExecutionGroup,
flattenBlockEntriesOnly,
formatDuration,
getBlockColor,
getBlockIcon,
groupEntriesByExecution,

View File

@@ -53,17 +53,6 @@ export function getBlockColor(blockType: string): string {
return '#6b7280'
}
/**
* Formats duration from milliseconds to readable format
*/
export function formatDuration(ms?: number): string {
if (ms === undefined || ms === null) return '-'
if (ms < 1000) {
return `${Math.round(ms)}ms`
}
return `${(ms / 1000).toFixed(2)}s`
}
/**
* Determines if a keyboard event originated from a text-editable element
*/

View File

@@ -30,6 +30,7 @@ import {
Textarea,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { formatDuration } from '@/lib/core/utils/formatting'
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
import { formatEditSequence } from '@/lib/workflows/training/compute-edit-sequence'
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
@@ -575,7 +576,9 @@ export function TrainingModal() {
<span className='text-[var(--text-muted)]'>Duration:</span>{' '}
<span className='text-[var(--text-secondary)]'>
{dataset.metadata?.duration
? `${(dataset.metadata.duration / 1000).toFixed(1)}s`
? formatDuration(dataset.metadata.duration, {
precision: 1,
})
: 'N/A'}
</span>
</div>

View File

@@ -19,6 +19,7 @@ import { checkUsageStatus } from '@/lib/billing/calculations/usage-monitor'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { decryptSecret } from '@/lib/core/security/encryption'
import { formatDuration } from '@/lib/core/utils/formatting'
import { getBaseUrl } from '@/lib/core/utils/urls'
import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
import { sendEmail } from '@/lib/messaging/email/mailer'
@@ -227,12 +228,6 @@ async function deliverWebhook(
}
}
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
return `${(ms / 60000).toFixed(1)}m`
}
function formatCost(cost?: Record<string, unknown>): string {
if (!cost?.total) return 'N/A'
const total = cost.total as number

View File

@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import type { ToolCallGroup, ToolCallState } from '@/lib/copilot/types'
import { cn } from '@/lib/core/utils/cn'
import { formatDuration } from '@/lib/core/utils/formatting'
interface ToolCallProps {
toolCall: ToolCallState
@@ -225,11 +226,6 @@ export function ToolCallCompletion({ toolCall, isCompact = false }: ToolCallProp
const isError = toolCall.state === 'error'
const isAborted = toolCall.state === 'aborted'
const formatDuration = (duration?: number) => {
if (!duration) return ''
return duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(1)}s`
}
return (
<div
className={cn(

View File

@@ -153,20 +153,44 @@ export function formatCompactTimestamp(iso: string): string {
}
/**
* Format a duration in milliseconds to a human-readable format
* @param durationMs - The duration in milliseconds
* Format a duration to a human-readable format
* @param duration - Duration in milliseconds (number) or as string (e.g., "500ms")
* @param options - Optional formatting options
* @param options.precision - Number of decimal places for seconds (default: 0)
* @returns A formatted duration string
*/
export function formatDuration(durationMs: number, options?: { precision?: number }): string {
const precision = options?.precision ?? 0
if (durationMs < 1000) {
return `${durationMs}ms`
export function formatDuration(
duration: number | string | undefined | null,
options?: { precision?: number }
): string {
if (duration === undefined || duration === null) {
return '-'
}
const seconds = durationMs / 1000
// Parse string durations (e.g., "500ms", "1234")
let ms: number
if (typeof duration === 'string') {
ms = Number.parseInt(duration.replace(/[^0-9.-]/g, ''), 10)
if (!Number.isFinite(ms)) {
return duration
}
} else {
ms = duration
}
const precision = options?.precision ?? 0
if (ms < 1) {
// Sub-millisecond: show with 2 decimal places
return `${ms.toFixed(2)}ms`
}
if (ms < 1000) {
// Milliseconds: round to integer
return `${Math.round(ms)}ms`
}
const seconds = ms / 1000
if (seconds < 60) {
return precision > 0 ? `${seconds.toFixed(precision)}s` : `${Math.floor(seconds)}s`
}