feat(web): drop elapsed timer and duplicate estimate from generation preview

The "usually 2–5 minutes" estimate showed twice (lead footnote + meta row)
and the elapsed counter added little signal, so remove both: delete the
meta row, stop falling back to the estimate footnote in the generating
lead (render the lead only when live narration exists), and drop the now
unused elapsed timer/util.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
chaoxiaoche 2026-05-30 17:13:37 +08:00
parent 4ca4df0cab
commit 876c0de2e6
4 changed files with 3 additions and 53 deletions

View file

@ -257,20 +257,6 @@
} }
} }
.meta {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 12px;
color: var(--text-faint);
}
.metaDivider {
opacity: 0.6;
}
.retry { .retry {
margin-top: 4px; margin-top: 4px;
min-height: 34px; min-height: 34px;

View file

@ -1,7 +1,5 @@
import { useEffect, useState } from 'react';
import { useT } from '../i18n'; import { useT } from '../i18n';
import type { GenerationPreviewModel } from '../runtime/generation-preview'; import type { GenerationPreviewModel } from '../runtime/generation-preview';
import { formatGenerationElapsed } from '../runtime/generation-preview';
import { Icon } from './Icon'; import { Icon } from './Icon';
import styles from './GenerationPreviewStage.module.css'; import styles from './GenerationPreviewStage.module.css';
@ -12,19 +10,9 @@ type Props = {
export function GenerationPreviewStage({ model, onRetry }: Props) { export function GenerationPreviewStage({ model, onRetry }: Props) {
const t = useT(); const t = useT();
const [now, setNow] = useState(() => Date.now());
const generating = model.phase === 'generating'; const generating = model.phase === 'generating';
useEffect(() => {
if (!generating) return undefined;
const id = window.setInterval(() => setNow(Date.now()), 1000);
return () => window.clearInterval(id);
}, [generating, model.startedAt]);
const elapsedSec = Math.max(0, Math.round((now - model.startedAt) / 1000));
const elapsedLabel = formatGenerationElapsed(elapsedSec);
const stepLabels: Record<GenerationPreviewModel['steps'][number]['id'], string> = { const stepLabels: Record<GenerationPreviewModel['steps'][number]['id'], string> = {
understand: t('generationPreview.stepUnderstand'), understand: t('generationPreview.stepUnderstand'),
generate: t('generationPreview.stepGenerate'), generate: t('generationPreview.stepGenerate'),
@ -47,7 +35,7 @@ export function GenerationPreviewStage({ model, onRetry }: Props) {
? t('generationPreview.stoppedLead') ? t('generationPreview.stoppedLead')
: model.phase === 'awaiting-input' : model.phase === 'awaiting-input'
? t('generationPreview.awaitingLead') ? t('generationPreview.awaitingLead')
: model.activityLabel || t('generationPreview.footnote'); : model.activityLabel;
const markIcon = const markIcon =
model.phase === 'failed' ? 'close' : model.phase === 'stopped' ? 'stop' : 'sparkles'; model.phase === 'failed' ? 'close' : model.phase === 'stopped' ? 'stop' : 'sparkles';
@ -69,11 +57,11 @@ export function GenerationPreviewStage({ model, onRetry }: Props) {
<Icon name={markIcon} size={24} /> <Icon name={markIcon} size={24} />
</div> </div>
<h1 className={styles.title}>{title}</h1> <h1 className={styles.title}>{title}</h1>
{showSubstatus ? null : ( {!showSubstatus && lead ? (
<p className={styles.lead} data-live={generating && Boolean(model.activityLabel)}> <p className={styles.lead} data-live={generating && Boolean(model.activityLabel)}>
{lead} {lead}
</p> </p>
)} ) : null}
<div <div
className={styles.progress} className={styles.progress}
data-active={generating} data-active={generating}
@ -118,17 +106,6 @@ export function GenerationPreviewStage({ model, onRetry }: Props) {
) : null} ) : null}
</div> </div>
) : null} ) : null}
{generating ? (
<div className={styles.meta}>
<span data-testid="generation-preview-elapsed">
{t('generationPreview.elapsed', { elapsed: elapsedLabel })}
</span>
<span className={styles.metaDivider} aria-hidden>
·
</span>
<span>{t('generationPreview.estimate')}</span>
</div>
) : null}
{model.phase === 'failed' && onRetry ? ( {model.phase === 'failed' && onRetry ? (
<button <button
type="button" type="button"

View file

@ -249,14 +249,6 @@ export function generationPreviewProgress(steps: GenerationPreviewStep[]): numbe
return Math.max(8, Math.min(steps.some((step) => step.status === 'failed') ? 72 : 92, Math.round(score * 100))); return Math.max(8, Math.min(steps.some((step) => step.status === 'failed') ? 72 : 92, Math.round(score * 100)));
} }
export function formatGenerationElapsed(seconds: number): string {
const safe = Math.max(0, Math.floor(seconds));
if (safe < 60) return `${safe}s`;
const minutes = Math.floor(safe / 60);
const remainder = safe % 60;
return remainder > 0 ? `${minutes}m ${remainder}s` : `${minutes}m`;
}
function isActiveRunStatus(status: ChatMessage['runStatus']): boolean { function isActiveRunStatus(status: ChatMessage['runStatus']): boolean {
return status === 'queued' || status === 'running'; return status === 'queued' || status === 'running';
} }

View file

@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest';
import { import {
buildGenerationPreviewState, buildGenerationPreviewState,
derivePrototypeGenerationSteps, derivePrototypeGenerationSteps,
formatGenerationElapsed,
workspaceHasPreviewSurface, workspaceHasPreviewSurface,
} from '../../src/runtime/generation-preview'; } from '../../src/runtime/generation-preview';
import type { AgentEvent, ChatMessage } from '../../src/types'; import type { AgentEvent, ChatMessage } from '../../src/types';
@ -309,8 +308,4 @@ describe('generation preview helpers', () => {
).toBeNull(); ).toBeNull();
}); });
it('formats elapsed durations for the meta row', () => {
expect(formatGenerationElapsed(42)).toBe('42s');
expect(formatGenerationElapsed(125)).toBe('2m 5s');
});
}); });