diff --git a/src/api/aiGenerationClient.ts b/src/api/aiGenerationClient.ts index 6fdfe29..090977d 100644 --- a/src/api/aiGenerationClient.ts +++ b/src/api/aiGenerationClient.ts @@ -150,6 +150,10 @@ export interface AiTaskStatus { type: "image" | "video"; status: "pending" | "running" | "completed" | "failed" | "cancelled"; progress: number; + progressSource?: "real" | "estimated" | string | null; + stage?: string | null; + startedAt?: string | null; + expectedDurationMs?: number | null; resultUrl: string | null; error: string | null; params?: Record; @@ -514,7 +518,20 @@ export const aiGenerationClient = { subscribeTaskStatus( taskId: string, - onUpdate: (task: Pick) => void, + onUpdate: ( + task: Pick< + AiTaskStatus, + | "taskId" + | "status" + | "progress" + | "progressSource" + | "stage" + | "startedAt" + | "expectedDurationMs" + | "resultUrl" + | "error" + >, + ) => void, ): () => void { const url = buildApiUrl(`ai/tasks/${taskId}/stream`); const controller = new AbortController(); diff --git a/src/api/taskSubscription.ts b/src/api/taskSubscription.ts index 3084581..c04d17f 100644 --- a/src/api/taskSubscription.ts +++ b/src/api/taskSubscription.ts @@ -9,6 +9,10 @@ export interface TaskProgressEvent { taskId: string; status: string; progress: number; + progressSource?: "real" | "estimated" | string | null; + stage?: string | null; + startedAt?: string | null; + expectedDurationMs?: number | null; resultUrl?: string | null; error?: string | null; } @@ -37,7 +41,8 @@ export function waitForTask( operation: options.operation, }); const timeoutMs = options.timeoutMs ?? timeoutPolicy.maxRuntimeMs; - const noProgressTimeoutMs = options.noProgressTimeoutMs ?? timeoutPolicy.noProgressTimeoutMs; + const noProgressTimeoutMs = + options.noProgressTimeoutMs ?? timeoutPolicy.noProgressTimeoutMs; const startedAt = options.startedAt ?? Date.now(); return new Promise((resolve, reject) => { @@ -58,7 +63,10 @@ export function waitForTask( }; timeoutId = setTimeout( - () => settle(() => reject(new Error(buildLocalTimeoutMessage(options.kind || "video")))), + () => + settle(() => + reject(new Error(buildLocalTimeoutMessage(options.kind || "video"))), + ), timeoutMs, ); @@ -105,7 +113,11 @@ export function waitForTask( policy: { ...timeoutPolicy, noProgressTimeoutMs }, }); if (timeoutReason) { - settle(() => reject(new Error(buildLocalTimeoutMessage(options.kind || "video")))); + settle(() => + reject( + new Error(buildLocalTimeoutMessage(options.kind || "video")), + ), + ); return; } try { @@ -114,6 +126,10 @@ export function waitForTask( taskId, status: task.status, progress: task.progress || 0, + progressSource: task.progressSource, + stage: task.stage, + startedAt: task.startedAt, + expectedDurationMs: task.expectedDurationMs, resultUrl: task.resultUrl, error: task.error, }); diff --git a/src/features/canvas/CanvasPage.tsx b/src/features/canvas/CanvasPage.tsx index 8ba9351..18ee3d0 100644 --- a/src/features/canvas/CanvasPage.tsx +++ b/src/features/canvas/CanvasPage.tsx @@ -33,6 +33,10 @@ import { communityClient } from "../../api/communityClient"; import { modelCapabilitiesClient } from "../../api/modelCapabilitiesClient"; import type { CreatePreviewTaskInput } from "../../api/webGenerationGateway"; import WorkspacePageShell from "../../components/WorkspacePageShell"; +import { + DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS, + DEFAULT_VIDEO_GENERATION_EXPECTED_DURATION_MS, +} from "../../hooks/useSmoothedProgress"; import type { WebCanvasWorkflow } from "../../types"; import type { AssetLibraryCategory } from "../assets/localAssetStore"; import { @@ -4492,6 +4496,7 @@ function CanvasPage({ progress={imageNodeProgress} status={imageTaskState?.status || "running"} message={imageTaskState?.message || "图片生成中"} + expectedDurationMs={DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS} /> ) : null} {imageNodeFocusActive && imageFocusSelectionReady ? ( @@ -4866,6 +4871,7 @@ function CanvasPage({ progress={videoNodeProgress} status={videoTaskState?.status || "running"} message={videoTaskState?.message || "视频生成中"} + expectedDurationMs={DEFAULT_VIDEO_GENERATION_EXPECTED_DURATION_MS} /> ) : null} {renderConnectorButton({ kind: "video", nodeId: videoNode.id, side: "left", slot: "center" }, "studio-canvas-video-node__connector")} diff --git a/src/features/canvas/CanvasSmoothedProgressRing.tsx b/src/features/canvas/CanvasSmoothedProgressRing.tsx index a80eefc..8ee0d7c 100644 --- a/src/features/canvas/CanvasSmoothedProgressRing.tsx +++ b/src/features/canvas/CanvasSmoothedProgressRing.tsx @@ -1,4 +1,8 @@ -import { useSmoothedProgress } from "../../hooks/useSmoothedProgress"; +import { + DEFAULT_GENERATION_EXPECTED_DURATION_MS, + useSmoothedProgress, + type ProgressSource, +} from "../../hooks/useSmoothedProgress"; import { canvasGenerationProgressStyle } from "./canvasUtils"; type NodeGenStatus = "submitting" | "running" | "success" | "error"; @@ -7,10 +11,24 @@ interface CanvasSmoothedProgressRingProps { progress: number; status: NodeGenStatus; message?: string; + progressSource?: ProgressSource; + startedAt?: number | string | Date | null; + expectedDurationMs?: number | null; } -export function CanvasSmoothedProgressRing({ progress, status, message }: CanvasSmoothedProgressRingProps) { - const smoothed = useSmoothedProgress(progress, status); +export function CanvasSmoothedProgressRing({ + progress, + status, + message, + progressSource = "estimated", + startedAt, + expectedDurationMs = DEFAULT_GENERATION_EXPECTED_DURATION_MS, +}: CanvasSmoothedProgressRingProps) { + const smoothed = useSmoothedProgress(progress, status, { + progressSource, + startedAt, + expectedDurationMs, + }); return (
-
diff --git a/src/features/canvas/canvasUtils.ts b/src/features/canvas/canvasUtils.ts index 72fc37c..d46d2b5 100644 --- a/src/features/canvas/canvasUtils.ts +++ b/src/features/canvas/canvasUtils.ts @@ -262,7 +262,17 @@ export async function waitForImageTaskResult( abortRef, onProgress: (e) => { if (onStatus) { - onStatus({ taskId, status: e.status, progress: e.progress, resultUrl: e.resultUrl ?? undefined, error: e.error ?? undefined } as AiTaskStatus); + onStatus({ + taskId, + status: e.status, + progress: e.progress, + progressSource: e.progressSource, + stage: e.stage, + startedAt: e.startedAt, + expectedDurationMs: e.expectedDurationMs, + resultUrl: e.resultUrl ?? undefined, + error: e.error ?? undefined, + } as AiTaskStatus); } }, }); @@ -281,7 +291,17 @@ export async function waitForVideoTaskResult( abortRef, onProgress: (e) => { if (onStatus) { - onStatus({ taskId, status: e.status, progress: e.progress, resultUrl: e.resultUrl ?? undefined, error: e.error ?? undefined } as AiTaskStatus); + onStatus({ + taskId, + status: e.status, + progress: e.progress, + progressSource: e.progressSource, + stage: e.stage, + startedAt: e.startedAt, + expectedDurationMs: e.expectedDurationMs, + resultUrl: e.resultUrl ?? undefined, + error: e.error ?? undefined, + } as AiTaskStatus); } }, }); diff --git a/src/features/ecommerce/EcommerceProgressBar.tsx b/src/features/ecommerce/EcommerceProgressBar.tsx index f8cca03..53da5eb 100644 --- a/src/features/ecommerce/EcommerceProgressBar.tsx +++ b/src/features/ecommerce/EcommerceProgressBar.tsx @@ -1,4 +1,9 @@ -import { useSmoothedProgress } from "../../hooks/useSmoothedProgress"; +import { useRef } from "react"; +import { + DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS, + formatEstimatedRemainingLabel, + useSmoothedProgress, +} from "../../hooks/useSmoothedProgress"; interface EcommerceProgressBarProps { status: "idle" | "generating" | "done" | "failed" | string; @@ -12,19 +17,42 @@ function mapStatus(status: string): "running" | "completed" | "failed" { return "running"; } -export function EcommerceProgressBar({ status, label }: EcommerceProgressBarProps) { - const progress = mapStatus(status) === "running" ? 50 : 100; - const smoothed = useSmoothedProgress(progress, mapStatus(status)); +export function EcommerceProgressBar({ + status, + label, +}: EcommerceProgressBarProps) { + const startedAtRef = useRef(Date.now()); + const mappedStatus = mapStatus(status); + const progress = mappedStatus === "running" ? 50 : 100; + const smoothed = useSmoothedProgress(progress, mappedStatus, { + progressSource: mappedStatus === "running" ? "estimated" : "real", + startedAt: startedAtRef.current, + expectedDurationMs: DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS, + }); + const remainingLabel = + mappedStatus === "running" + ? formatEstimatedRemainingLabel({ + nowMs: Date.now(), + startedAt: startedAtRef.current, + expectedDurationMs: DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS, + }) + : null; if (status === "idle") return null; return (
- {label || "AI 正在生成"} + + {label || "AI 正在生成"} + {remainingLabel ? ` / ${remainingLabel}` : ""} +
-
+
{smoothed}%
); -} \ No newline at end of file +} diff --git a/src/features/workbench/SmoothedProgressBar.tsx b/src/features/workbench/SmoothedProgressBar.tsx index 2d2d809..1ed8202 100644 --- a/src/features/workbench/SmoothedProgressBar.tsx +++ b/src/features/workbench/SmoothedProgressBar.tsx @@ -1,4 +1,8 @@ -import { useSmoothedProgress } from "../../hooks/useSmoothedProgress"; +import { + DEFAULT_SUPER_RESOLUTION_EXPECTED_DURATION_MS, + useSmoothedProgress, + type ProgressSource, +} from "../../hooks/useSmoothedProgress"; type MessageStatus = "thinking" | "completed" | "failed" | string; @@ -6,6 +10,9 @@ interface SmoothedProgressBarProps { progress: number; status: MessageStatus; label?: string; + progressSource?: ProgressSource; + startedAt?: number | string | Date | null; + expectedDurationMs?: number | null; } function mapMessageStatus(status: MessageStatus) { @@ -15,8 +22,19 @@ function mapMessageStatus(status: MessageStatus) { return "running" as const; } -export function SmoothedProgressBar({ progress, status, label }: SmoothedProgressBarProps) { - const smoothed = useSmoothedProgress(progress, mapMessageStatus(status)); +export function SmoothedProgressBar({ + progress, + status, + label, + progressSource = "estimated", + startedAt, + expectedDurationMs = DEFAULT_SUPER_RESOLUTION_EXPECTED_DURATION_MS, +}: SmoothedProgressBarProps) { + const smoothed = useSmoothedProgress(progress, mapMessageStatus(status), { + progressSource, + startedAt, + expectedDurationMs, + }); return ( <> {label || "超分处理中..."} diff --git a/src/features/workbench/components/WorkbenchChatCards.tsx b/src/features/workbench/components/WorkbenchChatCards.tsx index ace9f58..4d73210 100644 --- a/src/features/workbench/components/WorkbenchChatCards.tsx +++ b/src/features/workbench/components/WorkbenchChatCards.tsx @@ -19,7 +19,13 @@ import { assetClient } from "../../../api/assetClient"; import { communityClient } from "../../../api/communityClient"; import { saveAssetToLocalLibrary } from "../../assets/localAssetStore"; import { SmoothedProgressBar } from "../SmoothedProgressBar"; -import { useSmoothedProgress } from "../../../hooks/useSmoothedProgress"; +import { + DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS, + DEFAULT_SUPER_RESOLUTION_EXPECTED_DURATION_MS, + DEFAULT_VIDEO_GENERATION_EXPECTED_DURATION_MS, + formatEstimatedRemainingLabel, + useSmoothedProgress, +} from "../../../hooks/useSmoothedProgress"; import { renderMarkdownBlocks } from "../markdownRenderer"; import { downloadResultAsset } from "../workbenchDownload"; import type { WorkbenchChatAttachment, WorkbenchChatMessage, WorkbenchResultActionPayload } from "../workbenchChatTypes"; @@ -456,6 +462,8 @@ export const ResultCard = memo(function ResultCard({ progress={message.taskProgress ?? 18} status={message.status || "thinking"} label={message.taskStatusLabel || "超分处理中..."} + progressSource="estimated" + expectedDurationMs={DEFAULT_SUPER_RESOLUTION_EXPECTED_DURATION_MS} />
) : null} @@ -575,7 +583,23 @@ export const GenerationPendingCard = memo(function GenerationPendingCard({ const specs = message.result?.specs || []; const prompt = message.prompt || message.body; const isVideo = message.mode === "video"; - const smoothed = useSmoothedProgress(message.taskProgress ?? 5, message.status === "thinking" ? "running" : "completed"); + const expectedDurationMs = isVideo + ? DEFAULT_VIDEO_GENERATION_EXPECTED_DURATION_MS + : DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS; + const progressStatus = message.status === "thinking" ? "running" : "completed"; + const smoothed = useSmoothedProgress(message.taskProgress ?? 5, progressStatus, { + progressSource: progressStatus === "running" ? "estimated" : "real", + startedAt: message.createdAt, + expectedDurationMs, + }); + const remainingLabel = progressStatus === "running" + ? formatEstimatedRemainingLabel({ + nowMs: Date.now(), + startedAt: message.createdAt, + expectedDurationMs, + }) + : null; + const statusLabel = message.taskStatusLabel || (isVideo ? "视频生成中" : "图片生成中"); return (
@@ -590,7 +614,7 @@ export const GenerationPendingCard = memo(function GenerationPendingCard({
- {message.taskStatusLabel || "Generating..."} + {remainingLabel ? `${statusLabel} / ${remainingLabel}` : statusLabel} {prompt}
{specs.length > 0 && ( diff --git a/src/hooks/useSmoothedProgress.test.ts b/src/hooks/useSmoothedProgress.test.ts new file mode 100644 index 0000000..c023c28 --- /dev/null +++ b/src/hooks/useSmoothedProgress.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "../test/testHarness"; + +import { + calculateEstimatedProgress, + formatEstimatedRemainingLabel, + resolveProgressStartedAt, +} from "./useSmoothedProgress"; + +describe("useSmoothedProgress helpers", () => { + it("calculates estimated progress from elapsed time with an easing curve", () => { + expect( + Math.round( + calculateEstimatedProgress({ + nowMs: 220_000, + startedAtMs: 100_000, + expectedDurationMs: 120_000, + }), + ), + ).toBe(85); + }); + + it("caps estimated progress below completion", () => { + expect( + Math.round( + calculateEstimatedProgress({ + nowMs: 900_000, + startedAtMs: 0, + expectedDurationMs: 120_000, + }), + ), + ).toBe(92); + }); + + it("parses local workbench timestamps", () => { + expect(resolveProgressStartedAt("2026-06-10 09:30")).toBe( + new Date(2026, 5, 10, 9, 30).getTime(), + ); + }); + + it("formats remaining time for estimated tasks", () => { + expect( + formatEstimatedRemainingLabel({ + nowMs: new Date(2026, 5, 10, 9, 30).getTime(), + startedAt: "2026-06-10 09:29", + expectedDurationMs: 120_000, + }), + ).toBe("预计还需 1 分钟"); + }); +}); diff --git a/src/hooks/useSmoothedProgress.ts b/src/hooks/useSmoothedProgress.ts index 15309dd..234dddd 100644 --- a/src/hooks/useSmoothedProgress.ts +++ b/src/hooks/useSmoothedProgress.ts @@ -1,6 +1,14 @@ import { useEffect, useRef, useState } from "react"; -type ProgressStatus = "queued" | "running" | "submitting" | "completed" | "failed" | "success" | "error"; +type ProgressStatus = + | "queued" + | "running" + | "submitting" + | "completed" + | "failed" + | "success" + | "error"; +export type ProgressSource = "real" | "estimated"; interface SmoothedProgressOptions { creepSpeed?: number; @@ -8,6 +16,11 @@ interface SmoothedProgressOptions { creepCeiling?: number; creepAhead?: number; completionDuration?: number; + progressSource?: ProgressSource; + startedAt?: number | string | Date | null; + expectedDurationMs?: number | null; + estimatedFloor?: number; + estimatedCeiling?: number; } const DEFAULT_CREEP_SPEED = 0.5; @@ -15,15 +28,109 @@ const DEFAULT_CHASE_RATE = 0.09; const DEFAULT_CREEP_CEILING = 97; const DEFAULT_CREEP_AHEAD = 25; const DEFAULT_COMPLETION_DURATION = 450; +export const DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS = 120_000; +export const DEFAULT_VIDEO_GENERATION_EXPECTED_DURATION_MS = 240_000; +export const DEFAULT_SUPER_RESOLUTION_EXPECTED_DURATION_MS = 180_000; +export const DEFAULT_GENERATION_EXPECTED_DURATION_MS = + DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS; +const DEFAULT_ESTIMATED_FLOOR = 12; +const DEFAULT_ESTIMATED_CEILING = 92; +const ESTIMATED_PROGRESS_CURVE = 2.4; +const MIN_EXPECTED_DURATION_MS = 1_000; function isTerminal(status: ProgressStatus): boolean { - return status === "completed" || status === "success" || status === "failed" || status === "error"; + return ( + status === "completed" || + status === "success" || + status === "failed" || + status === "error" + ); } function isSuccess(status: ProgressStatus): boolean { return status === "completed" || status === "success"; } +export function resolveProgressStartedAt( + value: number | string | Date | null | undefined, +): number | null { + if (value instanceof Date) { + const time = value.getTime(); + return Number.isFinite(time) ? time : null; + } + + if (typeof value === "number") { + return Number.isFinite(value) && value > 0 ? value : null; + } + + if (typeof value !== "string" || !value.trim()) return null; + + const normalized = value.trim(); + const localMatch = normalized.match( + /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})(?::(\d{2}))?/, + ); + if (localMatch) { + const [, year, month, day, hours, minutes, seconds] = localMatch; + const time = new Date( + Number(year), + Number(month) - 1, + Number(day), + Number(hours), + Number(minutes), + Number(seconds || 0), + ).getTime(); + return Number.isFinite(time) ? time : null; + } + + const parsed = Date.parse(normalized); + return Number.isFinite(parsed) ? parsed : null; +} + +export function calculateEstimatedProgress(input: { + nowMs: number; + startedAtMs: number; + expectedDurationMs?: number | null; + floor?: number; + ceiling?: number; +}): number { + const floor = Math.max( + 0, + Math.min(99, input.floor ?? DEFAULT_ESTIMATED_FLOOR), + ); + const ceiling = Math.max( + floor, + Math.min(99, input.ceiling ?? DEFAULT_ESTIMATED_CEILING), + ); + const duration = Math.max( + MIN_EXPECTED_DURATION_MS, + Number(input.expectedDurationMs) || DEFAULT_GENERATION_EXPECTED_DURATION_MS, + ); + const elapsed = Math.max(0, input.nowMs - input.startedAtMs); + const ratio = elapsed / duration; + const eased = 1 - Math.exp(-ratio * ESTIMATED_PROGRESS_CURVE); + return Math.min(ceiling, floor + eased * (ceiling - floor)); +} + +export function formatEstimatedRemainingLabel(input: { + nowMs: number; + startedAt: number | string | Date | null | undefined; + expectedDurationMs?: number | null; +}): string | null { + const startedAtMs = resolveProgressStartedAt(input.startedAt); + if (!startedAtMs) return null; + const duration = Math.max( + MIN_EXPECTED_DURATION_MS, + Number(input.expectedDurationMs) || DEFAULT_GENERATION_EXPECTED_DURATION_MS, + ); + const remainingSeconds = Math.max( + 0, + Math.ceil((startedAtMs + duration - input.nowMs) / 1000), + ); + if (remainingSeconds <= 0) return "即将完成"; + if (remainingSeconds < 60) return `预计还需 ${remainingSeconds} 秒`; + return `预计还需 ${Math.ceil(remainingSeconds / 60)} 分钟`; +} + /** * Smoothly interpolates between coarse server-reported progress values. * On completion, animates quickly to 100% instead of jumping. @@ -37,24 +144,59 @@ export function useSmoothedProgress( const chaseRate = options?.chaseRate ?? DEFAULT_CHASE_RATE; const creepCeiling = options?.creepCeiling ?? DEFAULT_CREEP_CEILING; const creepAhead = options?.creepAhead ?? DEFAULT_CREEP_AHEAD; - const completionDuration = options?.completionDuration ?? DEFAULT_COMPLETION_DURATION; + const completionDuration = + options?.completionDuration ?? DEFAULT_COMPLETION_DURATION; + const progressSource = options?.progressSource ?? "real"; + const startedAt = options?.startedAt; + const expectedDurationMs = + options?.expectedDurationMs ?? DEFAULT_GENERATION_EXPECTED_DURATION_MS; + const estimatedFloor = options?.estimatedFloor ?? DEFAULT_ESTIMATED_FLOOR; + const estimatedCeiling = + options?.estimatedCeiling ?? DEFAULT_ESTIMATED_CEILING; const [displayed, setDisplayed] = useState(0); const rafRef = useRef(0); const targetRef = useRef(realProgress); const statusRef = useRef(status); + const progressSourceRef = useRef(progressSource); + const expectedDurationMsRef = useRef(expectedDurationMs); + const estimatedFloorRef = useRef(estimatedFloor); + const estimatedCeilingRef = useRef(estimatedCeiling); + const estimatedStartedAtRef = useRef(null); const completionStartRef = useRef(null); const completionBaseRef = useRef(0); useEffect(() => { targetRef.current = realProgress; statusRef.current = status; + progressSourceRef.current = progressSource; + expectedDurationMsRef.current = expectedDurationMs; + estimatedFloorRef.current = estimatedFloor; + estimatedCeilingRef.current = estimatedCeiling; + + if (progressSource === "estimated" && !isTerminal(status)) { + estimatedStartedAtRef.current = + resolveProgressStartedAt(startedAt) ?? + estimatedStartedAtRef.current ?? + Date.now(); + } if (isSuccess(status) && completionStartRef.current === null) { completionStartRef.current = performance.now(); completionBaseRef.current = displayed; + } else if (!isSuccess(status)) { + completionStartRef.current = null; } - }, [realProgress, status]); + }, [ + displayed, + estimatedCeiling, + estimatedFloor, + expectedDurationMs, + progressSource, + realProgress, + startedAt, + status, + ]); useEffect(() => { if (status === "failed" || status === "error") { @@ -82,6 +224,20 @@ export function useSmoothedProgress( if (isTerminal(currentStatus)) return current; + if (progressSourceRef.current === "estimated") { + const target = calculateEstimatedProgress({ + nowMs: Date.now(), + startedAtMs: estimatedStartedAtRef.current ?? Date.now(), + expectedDurationMs: expectedDurationMsRef.current, + floor: estimatedFloorRef.current, + ceiling: estimatedCeilingRef.current, + }); + if (current >= target) return current; + const gap = target - current; + const step = (gap * chaseRate + 0.2) * dt * 60; + return Math.min(current + step, target); + } + const target = targetRef.current; if (current >= target) { @@ -100,7 +256,14 @@ export function useSmoothedProgress( rafRef.current = requestAnimationFrame(tick); return () => cancelAnimationFrame(rafRef.current); - }, [status, chaseRate, creepSpeed, creepCeiling, creepAhead, completionDuration]); + }, [ + status, + chaseRate, + creepSpeed, + creepCeiling, + creepAhead, + completionDuration, + ]); if (status === "failed" || status === "error") return Math.round(displayed); if (isSuccess(status) && displayed >= 99.5) return 100;