UNPKG

use-vibes

Version:

Transform any DOM element into an AI-powered micro-app

264 lines 13.4 kB
import * as React from 'react'; import { ImgFile } from 'use-fireproof'; import { ImgGenError } from './ImgGenError'; import { combineClasses, defaultClasses } from '../../utils/style-utils'; import { getCurrentFileKey, getPromptInfo, getVersionInfo } from './ImgGenDisplayUtils'; import { ImgGenModal } from './ImgGenModal'; import { logDebug } from '../../utils/debug'; // Component for displaying the generated image export function ImgGenDisplay({ document, className, alt, onDelete, onRegen, onPromptEdit, classes = defaultClasses, loading, progress, error, debug, // Add debug flag to props interface }) { // Delete confirmation is now handled within ControlsBar // Use null to indicate not editing, or string for edit mode const [editedPrompt, setEditedPrompt] = React.useState(null); // Track the prompt that's currently being generated const [generatingPrompt, setGeneratingPrompt] = React.useState(null); // --- Fullscreen backdrop state --- const [isFullscreen, setIsFullscreen] = React.useState(false); const openFullscreen = () => setIsFullscreen(true); const closeFullscreen = () => setIsFullscreen(false); // Get version information directly at render time const { versions, currentVersion } = getVersionInfo(document); // Calculate the initial version index based on document state const initialVersionIndex = React.useMemo(() => { return typeof currentVersion === 'number' ? currentVersion : versions?.length ? versions.length - 1 : 0; }, [currentVersion, versions]); // Track previous version count to detect when new versions are added const prevVersionsCountRef = React.useRef(versions.length); // Set flash effect when new version is added React.useEffect(() => { // If we have more versions than before, it means a new version was added if (versions.length > prevVersionsCountRef.current) { // Trigger the flash effect setVersionFlash(true); // Auto-reset flash after animation completes const timer = setTimeout(() => { setVersionFlash(false); }, 2000); // Match the animation duration in CSS return () => clearTimeout(timer); } // Update ref for next comparison prevVersionsCountRef.current = versions.length; }, [versions.length]); // Only track user-selected version index as state const [userSelectedIndex, setUserSelectedIndex] = React.useState(null); // Track when a new version has been added to enable flash effect const [versionFlash, setVersionFlash] = React.useState(false); // Explicitly track regeneration state while waiting for progress to update const [pendingRegeneration, setPendingRegeneration] = React.useState(false); // Keep track of pending regeneration requests const pendingRegenerationRef = React.useRef(false); // We now use progress directly from props // Derive the final version index - use user selection if available, otherwise use the document's current version const versionIndex = userSelectedIndex !== null ? userSelectedIndex : initialVersionIndex; // Custom setter function that manages user selections const setVersionIndex = React.useCallback((index) => { setUserSelectedIndex(index); }, []); const fileKey = getCurrentFileKey(document, versionIndex, versions); const totalVersions = versions ? versions.length : 0; // We now use getPromptInfo directly at render time as a pure function // Navigation handlers function handlePrevVersion() { if (versionIndex > 0) { setVersionIndex(versionIndex - 1); // Exit edit mode when changing versions setEditedPrompt(null); } } function handleNextVersion() { if (versionIndex < totalVersions - 1) { setVersionIndex(versionIndex + 1); // Exit edit mode when changing versions setEditedPrompt(null); } } // ESC handling moved to ImgGenModal component // Determine which file to use - either the versioned file or the legacy 'image' file const currentFile = fileKey && document._files ? document._files[fileKey] : document._files?.image; // Get prompt text early (moved before portal) const promptInfo = getPromptInfo(document, versionIndex); const promptText = promptInfo.currentPrompt || alt || 'Generated image'; // State for delete confirmation is managed directly // Handle delete confirmation function handleDeleteConfirm() { if (debug) { logDebug('[ImgGenDisplay] handleDeleteConfirm called, document ID:', document._id); } if (onDelete && document && document._id) { if (debug) { logDebug('[ImgGenDisplay] Calling onDelete with ID:', document._id); } onDelete(document._id); } else { console.error('[ImgGenDisplay] Cannot delete - missing onDelete handler or document ID'); if (debug) { logDebug('[ImgGenDisplay] Delete details:', { hasOnDelete: !!onDelete, documentId: document?._id, }); } } } // Handle generating a new version function handleRegen() { // Set pending regeneration flag setPendingRegeneration(true); pendingRegenerationRef.current = true; // const { currentPrompt } = getPromptInfo(document, versionIndex); if (editedPrompt !== null) { // User has edited the prompt - always use the edited version // even if it happens to be the same as the current prompt const newPrompt = editedPrompt.trim(); if (newPrompt) { // Always submit the edited prompt as a new prompt onPromptEdit?.(document._id, newPrompt); } else { // Empty prompt, just regenerate with existing prompt onRegen?.(document._id); } } else { // Not in edit mode → regenerate current prompt onRegen?.(document._id); } // Reset user selection when generating a new version // This will make the display automatically switch to the latest version when it returns setUserSelectedIndex(null); // Save the prompt we're generating with let promptToGenerate = ''; if (editedPrompt !== null) { promptToGenerate = editedPrompt.trim(); } else { const { currentPrompt } = getPromptInfo(document, versionIndex); promptToGenerate = currentPrompt; } setGeneratingPrompt(promptToGenerate); // Reset edited prompt since we're no longer in edit mode setEditedPrompt(null); } // Handle prompt editing function handlePromptEdit(newPrompt) { // Get the current prompt for comparison at the exact time of editing const { currentPrompt } = getPromptInfo(document, versionIndex); const trimmedPrompt = newPrompt.trim(); if (trimmedPrompt && trimmedPrompt !== currentPrompt) { // Set the edited prompt to the new trimmed value setEditedPrompt(trimmedPrompt); // Now use handleRegen to handle the regeneration process // This ensures the regeneration logic is consistent handleRegen(); } else { // If the prompt hasn't changed, just exit edit mode setEditedPrompt(null); } } // We're not using document.progress as it's always 100 // Just track the document loading state const documentLoading = document.loading ?? false; // Reset regeneration state when loading state changes React.useEffect(() => { if (!documentLoading && pendingRegenerationRef.current) { pendingRegenerationRef.current = false; setPendingRegeneration(false); // Clear the generating prompt after completion setTimeout(() => { setGeneratingPrompt(null); }, 500); } }, [documentLoading]); // Additional check for document updates to detect version changes const documentIdRef = React.useRef(document?._id); const versionsLengthRef = React.useRef(versions?.length || 0); React.useEffect(() => { // Check if a new version was added (version count increased) if (versions?.length > versionsLengthRef.current) { pendingRegenerationRef.current = false; setPendingRegeneration(false); } // Update refs versionsLengthRef.current = versions?.length || 0; documentIdRef.current = document?._id; }, [document?._id, versions?.length]); // Cleanup no longer needed as we're using real progress // Calculate the effective progress - use real progress during loading, otherwise 100% const effectiveProgress = loading ? progress : 100; // Is regeneration in progress - either from loading state or pending state const isRegenerating = pendingRegeneration || documentLoading === true || loading === true; // Debug logging for render conditions if (debug) { logDebug('[ImgGenDisplay Debug] Render state:', { documentId: document._id, hasFiles: !!document._files, fileKey, versions, versionIndex, hasDefaultImage: !!document._files?.image, isRegenerating, loading, error: error?.message, totalVersions, promptText, }); } if (!document._files || (!fileKey && !document._files.image)) { if (debug) { logDebug('[ImgGenDisplay Debug] Missing image file - showing error', { hasFiles: !!document._files, fileKey, defaultImageExists: !!document._files?.image, loading, }); } return React.createElement(ImgGenError, { message: "Missing image file" }); } // Determine which prompt to display: // 1. If in edit mode, show the edited prompt // 2. If generating, show the generating prompt // 3. Otherwise, show the document's prompt const displayPrompt = editedPrompt !== null ? editedPrompt : pendingRegeneration && generatingPrompt !== null ? generatingPrompt : promptText; return (React.createElement("div", { className: combineClasses('imggen-root', className, classes.root), title: displayPrompt }, React.createElement("div", { className: "imggen-image-container", style: { position: 'relative', width: '100%' } }, React.createElement("button", { className: "imggen-expand-button", onClick: (e) => { e.stopPropagation(); // Prevent click from propagating to parent elements openFullscreen(); }, title: "Expand image", "aria-label": "Expand image" }, React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "24", height: "24", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("polyline", { points: "15,5 20,5 20,10" }), React.createElement("polyline", { points: "9,19 4,19 4,14" }), React.createElement("line", { x1: "20", y1: "5", x2: "14", y2: "11" }), React.createElement("line", { x1: "4", y1: "19", x2: "10", y2: "13" }))), React.createElement(ImgFile, { file: currentFile, className: combineClasses('imggen-image', classes.image), alt: alt || 'Generated image', style: { width: '100%' } }), isRegenerating && (React.createElement("div", { className: "imggen-progress-container", style: { position: 'absolute', top: 0, left: 0, width: '100%', height: '6px', overflow: 'hidden', backgroundColor: 'rgba(0, 0, 0, 0.1)', zIndex: 10, } }, React.createElement("div", { className: combineClasses('imggen-progress', classes.progress), style: { width: `${effectiveProgress}%`, height: '100%', backgroundColor: 'var(--imggen-accent-color, #0074d9)', transition: 'width 0.5s ease-out', }, "aria-hidden": "true" })))), React.createElement(ImgGenModal, { isOpen: isFullscreen, onClose: closeFullscreen, currentFile: currentFile, alt: alt, promptText: displayPrompt, editedPrompt: editedPrompt, setEditedPrompt: setEditedPrompt, handlePromptEdit: handlePromptEdit, handleDeleteConfirm: handleDeleteConfirm, handlePrevVersion: handlePrevVersion, handleNextVersion: handleNextVersion, handleRegen: handleRegen, versionIndex: versionIndex, totalVersions: totalVersions, progress: effectiveProgress, classes: classes, versionFlash: versionFlash, isRegenerating: isRegenerating, error: error }))); } //# sourceMappingURL=ImgGenDisplay.js.map