Skip to content

Commit 7386d22

Browse files
committed
fix(formatting): consolidate duration formatting into shared utility
1 parent a9b7d75 commit 7386d22

File tree

10 files changed

+44
-78
lines changed

10 files changed

+44
-78
lines changed

apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import Link from 'next/link'
66
import { List, type RowComponentProps, useListRef } from 'react-window'
77
import { Badge, buttonVariants } from '@/components/emcn'
88
import { cn } from '@/lib/core/utils/cn'
9+
import { formatDuration } from '@/lib/core/utils/formatting'
910
import {
1011
DELETED_WORKFLOW_COLOR,
1112
DELETED_WORKFLOW_LABEL,
1213
formatDate,
13-
formatDuration,
1414
getDisplayStatus,
1515
LOG_COLUMNS,
1616
StatusBadge,

apps/sim/app/workspace/[workspaceId]/logs/utils.ts

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react'
22
import { format } from 'date-fns'
33
import { Badge } from '@/components/emcn'
4+
import { formatDuration } from '@/lib/core/utils/formatting'
45
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
56
import { getBlock } from '@/blocks/registry'
67
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
@@ -362,47 +363,14 @@ export function mapToExecutionLogAlt(log: RawLogResponse): ExecutionLog {
362363
}
363364
}
364365

365-
/**
366-
* Format duration for display in logs UI
367-
* If duration is under 1 second, displays as milliseconds (e.g., "500ms")
368-
* If duration is 1 second or more, displays as seconds (e.g., "1.23s")
369-
* @param duration - Duration string (e.g., "500ms") or null
370-
* @returns Formatted duration string or null
371-
*/
372-
export function formatDuration(duration: string | null): string | null {
373-
if (!duration) return null
374-
375-
// Extract numeric value from duration string (e.g., "500ms" -> 500)
376-
const ms = Number.parseInt(duration.replace(/[^0-9]/g, ''), 10)
377-
378-
if (!Number.isFinite(ms)) return duration
379-
380-
if (ms < 1000) {
381-
return `${ms}ms`
382-
}
383-
384-
// Convert to seconds with up to 2 decimal places
385-
const seconds = ms / 1000
386-
return `${seconds.toFixed(2).replace(/\.?0+$/, '')}s`
387-
}
388-
389366
/**
390367
* Format latency value for display in dashboard UI
391-
* If latency is under 1 second, displays as milliseconds (e.g., "500ms")
392-
* If latency is 1 second or more, displays as seconds (e.g., "1.23s")
393368
* @param ms - Latency in milliseconds (number)
394369
* @returns Formatted latency string
395370
*/
396371
export function formatLatency(ms: number): string {
397372
if (!Number.isFinite(ms) || ms <= 0) return '—'
398-
399-
if (ms < 1000) {
400-
return `${Math.round(ms)}ms`
401-
}
402-
403-
// Convert to seconds with up to 2 decimal places
404-
const seconds = ms / 1000
405-
return `${seconds.toFixed(2).replace(/\.?0+$/, '')}s`
373+
return formatDuration(ms, { precision: 2 })
406374
}
407375

408376
export const formatDate = (dateString: string) => {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { memo, useEffect, useMemo, useRef, useState } from 'react'
44
import clsx from 'clsx'
55
import { ChevronUp } from 'lucide-react'
6+
import { formatDuration } from '@/lib/core/utils/formatting'
67
import { CopilotMarkdownRenderer } from '../markdown-renderer'
78

89
/** Removes thinking tags (raw or escaped) and special tags from streamed content */
@@ -241,15 +242,9 @@ export function ThinkingBlock({
241242
return () => window.clearInterval(intervalId)
242243
}, [isStreaming, isExpanded, userHasScrolledAway])
243244

244-
/** Formats duration in milliseconds to seconds (minimum 1s) */
245-
const formatDuration = (ms: number) => {
246-
const seconds = Math.max(1, Math.round(ms / 1000))
247-
return `${seconds}s`
248-
}
249-
250245
const hasContent = cleanContent.length > 0
251246
const isThinkingDone = !isStreaming || hasFollowingContent || hasSpecialTags
252-
const durationText = `${label} for ${formatDuration(duration)}`
247+
const durationText = `${label} for ${formatDuration(Math.max(1000, duration))}`
253248

254249
const getStreamingLabel = (lbl: string) => {
255250
if (lbl === 'Thought') return 'Thinking'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
hasInterrupt as hasInterruptFromConfig,
1616
isSpecialTool as isSpecialToolFromConfig,
1717
} from '@/lib/copilot/tools/client/ui-config'
18+
import { formatDuration } from '@/lib/core/utils/formatting'
1819
import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
1920
import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming'
2021
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({
848849
(allParsed.options && Object.keys(allParsed.options).length > 0)
849850
)
850851

851-
const formatDuration = (ms: number) => {
852-
const seconds = Math.max(1, Math.round(ms / 1000))
853-
return `${seconds}s`
854-
}
855-
856852
const outerLabel = getSubagentCompletionLabel(toolCall.name)
857-
const durationText = `${outerLabel} for ${formatDuration(duration)}`
853+
const durationText = `${outerLabel} for ${formatDuration(Math.max(1000, duration))}`
858854

859855
const renderCollapsibleContent = () => (
860856
<>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Tooltip,
2525
} from '@/components/emcn'
2626
import { getEnv, isTruthy } from '@/lib/core/config/env'
27+
import { formatDuration } from '@/lib/core/utils/formatting'
2728
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
2829
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
2930
import {
@@ -43,7 +44,6 @@ import {
4344
type EntryNode,
4445
type ExecutionGroup,
4546
flattenBlockEntriesOnly,
46-
formatDuration,
4747
getBlockColor,
4848
getBlockIcon,
4949
groupEntriesByExecution,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,6 @@ export function getBlockColor(blockType: string): string {
5353
return '#6b7280'
5454
}
5555

56-
/**
57-
* Formats duration from milliseconds to readable format
58-
*/
59-
export function formatDuration(ms?: number): string {
60-
if (ms === undefined || ms === null) return '-'
61-
if (ms < 1000) {
62-
return `${Math.round(ms)}ms`
63-
}
64-
return `${(ms / 1000).toFixed(2)}s`
65-
}
66-
6756
/**
6857
* Determines if a keyboard event originated from a text-editable element
6958
*/

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/training-modal/training-modal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
Textarea,
3131
} from '@/components/emcn'
3232
import { cn } from '@/lib/core/utils/cn'
33+
import { formatDuration } from '@/lib/core/utils/formatting'
3334
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
3435
import { formatEditSequence } from '@/lib/workflows/training/compute-edit-sequence'
3536
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
@@ -575,7 +576,9 @@ export function TrainingModal() {
575576
<span className='text-[var(--text-muted)]'>Duration:</span>{' '}
576577
<span className='text-[var(--text-secondary)]'>
577578
{dataset.metadata?.duration
578-
? `${(dataset.metadata.duration / 1000).toFixed(1)}s`
579+
? formatDuration(dataset.metadata.duration, {
580+
precision: 1,
581+
})
579582
: 'N/A'}
580583
</span>
581584
</div>

apps/sim/background/workspace-notification-delivery.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { checkUsageStatus } from '@/lib/billing/calculations/usage-monitor'
1919
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
2020
import { RateLimiter } from '@/lib/core/rate-limiter'
2121
import { decryptSecret } from '@/lib/core/security/encryption'
22+
import { formatDuration } from '@/lib/core/utils/formatting'
2223
import { getBaseUrl } from '@/lib/core/utils/urls'
2324
import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
2425
import { sendEmail } from '@/lib/messaging/email/mailer'
@@ -227,12 +228,6 @@ async function deliverWebhook(
227228
}
228229
}
229230

230-
function formatDuration(ms: number): string {
231-
if (ms < 1000) return `${ms}ms`
232-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
233-
return `${(ms / 60000).toFixed(1)}m`
234-
}
235-
236231
function formatCost(cost?: Record<string, unknown>): string {
237232
if (!cost?.total) return 'N/A'
238233
const total = cost.total as number

apps/sim/components/ui/tool-call.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
77
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
88
import type { ToolCallGroup, ToolCallState } from '@/lib/copilot/types'
99
import { cn } from '@/lib/core/utils/cn'
10+
import { formatDuration } from '@/lib/core/utils/formatting'
1011

1112
interface ToolCallProps {
1213
toolCall: ToolCallState
@@ -225,11 +226,6 @@ export function ToolCallCompletion({ toolCall, isCompact = false }: ToolCallProp
225226
const isError = toolCall.state === 'error'
226227
const isAborted = toolCall.state === 'aborted'
227228

228-
const formatDuration = (duration?: number) => {
229-
if (!duration) return ''
230-
return duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(1)}s`
231-
}
232-
233229
return (
234230
<div
235231
className={cn(

apps/sim/lib/core/utils/formatting.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,20 +153,44 @@ export function formatCompactTimestamp(iso: string): string {
153153
}
154154

155155
/**
156-
* Format a duration in milliseconds to a human-readable format
157-
* @param durationMs - The duration in milliseconds
156+
* Format a duration to a human-readable format
157+
* @param duration - Duration in milliseconds (number) or as string (e.g., "500ms")
158158
* @param options - Optional formatting options
159159
* @param options.precision - Number of decimal places for seconds (default: 0)
160160
* @returns A formatted duration string
161161
*/
162-
export function formatDuration(durationMs: number, options?: { precision?: number }): string {
162+
export function formatDuration(
163+
duration: number | string | undefined | null,
164+
options?: { precision?: number }
165+
): string {
166+
if (duration === undefined || duration === null) {
167+
return '-'
168+
}
169+
170+
// Parse string durations (e.g., "500ms", "1234")
171+
let ms: number
172+
if (typeof duration === 'string') {
173+
ms = Number.parseInt(duration.replace(/[^0-9.-]/g, ''), 10)
174+
if (!Number.isFinite(ms)) {
175+
return duration
176+
}
177+
} else {
178+
ms = duration
179+
}
180+
163181
const precision = options?.precision ?? 0
164182

165-
if (durationMs < 1000) {
166-
return `${durationMs}ms`
183+
if (ms < 1) {
184+
// Sub-millisecond: show with 2 decimal places
185+
return `${ms.toFixed(2)}ms`
186+
}
187+
188+
if (ms < 1000) {
189+
// Milliseconds: round to integer
190+
return `${Math.round(ms)}ms`
167191
}
168192

169-
const seconds = durationMs / 1000
193+
const seconds = ms / 1000
170194
if (seconds < 60) {
171195
return precision > 0 ? `${seconds.toFixed(precision)}s` : `${Math.floor(seconds)}s`
172196
}

0 commit comments

Comments
 (0)