import { useState, useEffect, useRef, useCallback } from 'react'; import { PixelCard } from '@librechat/client'; import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider'; import { ToolIcon, isError } from '~/components/Chat/Messages/Content/ToolOutput'; import Image from '~/components/Chat/Messages/Content/Image'; import { useProgress, useLocalize } from '~/hooks'; import ProgressText from './ProgressText'; import { AGENT_STYLE_TOOLS } from '.'; import { scaleImage } from '~/utils'; function computeCancelled( isSubmitting: boolean | undefined, initialProgress: number, hasError: boolean, ): boolean { if (isSubmitting !== undefined) { return (!isSubmitting && initialProgress < 1) || hasError; } // Legacy path: in-progress (0 < progress < 1) is never cancelled // because legacy image gen lacks a submitting signal. if (initialProgress < 1 && initialProgress > 0) { return false; } return hasError; } export default function OpenAIImageGen({ initialProgress = 0.1, isSubmitting, toolName, args: _args = '', output, attachments, }: { initialProgress: number; isSubmitting?: boolean; toolName?: string; args: string | Record; output?: string | null; attachments?: TAttachment[]; }) { const localize = useLocalize(); const isAgentStyle = toolName != null && AGENT_STYLE_TOOLS.has(toolName); const [agentProgress, setAgentProgress] = useState(initialProgress); const legacyProgress = useProgress(isAgentStyle ? 1 : initialProgress); const progress = isAgentStyle ? agentProgress : legacyProgress; const intervalRef = useRef(null); const hasError = typeof output === 'string' && isError(output); /** * Determines if the image generation was cancelled. * - Agent path (isSubmitting defined): cancelled if not submitting + incomplete, or on error. * - Legacy path (isSubmitting undefined): in-progress (0 < progress < 1) is never cancelled * because legacy image gen lacks a submitting signal — only errors cancel. */ const cancelled = computeCancelled(isSubmitting, initialProgress, hasError); let width: number | undefined; let height: number | undefined; let quality: 'low' | 'medium' | 'high' = 'high'; let parsedArgs: Record = {}; try { parsedArgs = typeof _args === 'string' ? JSON.parse(_args) : _args; } catch { parsedArgs = {}; } try { const argsObj = parsedArgs; if (argsObj && typeof argsObj.size === 'string') { const [w, h] = argsObj.size.split('x').map((v: string) => parseInt(v, 10)); if (!isNaN(w) && !isNaN(h)) { width = w; height = h; } } else if (argsObj && (typeof argsObj.size !== 'string' || !argsObj.size)) { width = undefined; height = undefined; } if (argsObj && typeof argsObj.quality === 'string') { const q = argsObj.quality.toLowerCase(); if (q === 'low' || q === 'medium' || q === 'high') { quality = q; } } } catch { width = undefined; height = undefined; } const attachment = attachments?.[0]; const { width: imageWidth, height: imageHeight, filepath = null, filename = '', } = (attachment as TFile & TAttachmentMetadata) || {}; let origWidth = width ?? imageWidth; let origHeight = height ?? imageHeight; if (origWidth === undefined || origHeight === undefined) { origWidth = 1024; origHeight = 1024; } const [dimensions, setDimensions] = useState({ width: 'auto', height: 'auto' }); const containerRef = useRef(null); const updateDimensions = useCallback(() => { if (origWidth && origHeight && containerRef.current) { const scaled = scaleImage({ originalWidth: origWidth, originalHeight: origHeight, containerRef, }); setDimensions(scaled); } }, [origWidth, origHeight]); useEffect(() => { if (!isAgentStyle) { return; } if (isSubmitting) { setAgentProgress(initialProgress); if (intervalRef.current) { clearInterval(intervalRef.current); } let baseDuration = 20000; if (quality === 'low') { baseDuration = 10000; } else if (quality === 'high') { baseDuration = 50000; } const jitter = Math.floor(baseDuration * 0.3); const totalDuration = Math.floor(Math.random() * jitter) + baseDuration; const updateInterval = 200; const totalSteps = totalDuration / updateInterval; let currentStep = 0; intervalRef.current = setInterval(() => { currentStep++; if (currentStep >= totalSteps) { clearInterval(intervalRef.current as NodeJS.Timeout); setAgentProgress(0.9); } else { const progressRatio = currentStep / totalSteps; let mapRatio: number; if (progressRatio < 0.8) { mapRatio = Math.pow(progressRatio, 1.1); } else { const sub = (progressRatio - 0.8) / 0.2; mapRatio = 0.8 + (1 - Math.pow(1 - sub, 2)) * 0.2; } const scaledProgress = 0.1 + mapRatio * 0.8; setAgentProgress(scaledProgress); } }, updateInterval); } return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, [isSubmitting, initialProgress, quality, isAgentStyle]); useEffect(() => { if (!isAgentStyle) { return; } if (initialProgress >= 1 || cancelled) { setAgentProgress(initialProgress); if (intervalRef.current) { clearInterval(intervalRef.current); } } }, [initialProgress, cancelled, isAgentStyle]); useEffect(() => { updateDimensions(); const resizeObserver = new ResizeObserver(() => { updateDimensions(); }); if (containerRef.current) { resizeObserver.observe(containerRef.current); } return () => { resizeObserver.disconnect(); }; }, [updateDimensions]); const isInProgress = progress < 1 && !cancelled; return ( <> {(() => { if (progress < 1 && !cancelled) { return ''; } if (cancelled && hasError) { return localize('com_ui_image_gen_failed'); } if (cancelled) { return localize('com_ui_cancelled'); } return localize('com_ui_image_created'); })()}
{isAgentStyle && (
{dimensions.width !== 'auto' && progress < 1 && ( )}
)} ); }