use-vibes
Version:
Transform any DOM element into an AI-powered micro-app
351 lines • 17.7 kB
JavaScript
import * as React from 'react';
import { v4 as uuid } from 'uuid';
import { useImageGen } from '../hooks/image-gen/use-image-gen';
import { useFireproof } from 'use-fireproof';
import { ImgGenPromptWaiting, ImgGenDisplayPlaceholder, ImgGenDisplay, ImgGenError, } from './ImgGenUtils';
// Import from direct file since the main index.ts might not be updated yet
import { ImgGenUploadWaiting } from './ImgGenUtils/ImgGenUploadWaiting';
import { getImgGenMode } from './ImgGenUtils/ImgGenModeUtils';
import { defaultClasses } from '../utils/style-utils';
import { logDebug } from '../utils/debug';
import './ImgGen.css';
/**
* Core implementation of ImgGen component
* This is the component that gets remounted when the document ID or prompt changes
*/
function ImgGenCore(props) {
// Destructure the props for cleaner code
const { prompt, _id, className, alt, images, options, database, onComplete, onError, onDelete, onPromptEdit, onDocumentCreated, classes = defaultClasses, debug, } = props;
// Get access to the Fireproof database directly
const { database: db } = useFireproof(database || 'ImgGen');
// Use a unique generationId to trigger regeneration
// This provides a clearer signal when regeneration is needed
const [generationId, setGenerationId] = React.useState(undefined);
// Track the edited prompt to pass to the image generator and show in UI
const [currentEditedPrompt, setCurrentEditedPrompt] = React.useState(undefined);
// Track the document for image generation - use ImageDocument type or Record
const [imageGenDocument, setImageGenDocument] = React.useState(null);
// Merge options with images into a single options object for the hook
const mergedOptions = React.useMemo(() => (images ? { ...options, images } : options), [options, images]);
// Determine the effective prompt to use - either from form submission or props
const effectivePrompt = currentEditedPrompt || prompt || '';
// Check if we should skip image generation based on whether we have prompt or id
// Use effectivePrompt instead of just props.prompt
const shouldSkipGeneration = !effectivePrompt && !_id;
// Use the custom hook for all the image generation logic
const { imageData, loading, error, progress, document } = useImageGen({
// Use the effective prompt that prioritizes form submission
prompt: effectivePrompt,
_id: _id,
options: {
...mergedOptions,
// Include the document with uploaded files for image generation
...(imageGenDocument ? { document: imageGenDocument } : {}),
},
database,
// Use the generationId to signal when we want a new image
generationId,
// We no longer need editedPrompt since we're using effectivePrompt as the main prompt
// Skip processing if neither prompt nor _id is provided
skip: shouldSkipGeneration,
});
// Determine the current display mode based on document state
const mode = React.useMemo(() => {
return getImgGenMode({
document,
prompt: effectivePrompt, // Use effectivePrompt instead of just props.prompt
loading,
error: error || undefined,
debug,
});
}, [document, effectivePrompt, loading, error, debug]); // Update dependency array
if (debug) {
logDebug('[ImgGenCore] Current mode:', mode, {
document: !!document,
documentId: document?._id,
prompt: !!prompt,
loading,
error: !!error,
});
}
// When document is generated, use its ID for subsequent operations
// This is done through the parent component's remounting logic with uuid()
React.useEffect(() => {
if (onComplete && imageData && !loading && !error) {
onComplete();
}
}, [onComplete, imageData, loading, error]);
// Handle errors from the image generation hook
React.useEffect(() => {
if (onError && error) {
onError(error);
}
}, [onError, error]);
// Handle regeneration
const handleRegen = React.useCallback(() => {
if (document?._id || _id || prompt) {
// Create a new unique ID to trigger regeneration
const newGenId = crypto.randomUUID();
setGenerationId(newGenId);
}
}, [document, _id, prompt]);
// Handle prompt editing
const handlePromptEdit = React.useCallback(async (id, newPrompt) => {
// Update the tracked edited prompt
setCurrentEditedPrompt(newPrompt);
try {
// First, update the document in the database with the new prompt
const doc = await db.get(id);
if (!doc) {
logDebug('Document not found:', id);
return;
}
// Update prompt structure based on existing document pattern
// Support both 'prompt' field (legacy) and new structured 'prompts' pattern
const updatedDoc = { ...doc };
if (updatedDoc.prompts) {
// Handle new structured prompts with versioning
const promptKey = `p${Date.now()}`;
updatedDoc.prompts = {
...updatedDoc.prompts,
[promptKey]: { text: newPrompt },
};
updatedDoc.currentPromptKey = promptKey;
}
else {
// Handle simple legacy prompt field
updatedDoc.prompt = newPrompt;
}
// Save the updated document
await db.put(updatedDoc);
// Notify parent component
if (onPromptEdit) {
onPromptEdit(id, newPrompt);
}
// Store the document to be used for generation
// This ensures that when the regeneration happens, we have access to the document with uploaded images
const refreshedDoc = await db.get(id);
// Set the document in options before triggering regeneration
if (refreshedDoc) {
// Set a local state variable for the document to be used during regeneration
setImageGenDocument(refreshedDoc);
if (debug) {
logDebug('[ImgGen] Setting document for image generation:', refreshedDoc._id, 'with files:', Object.keys(refreshedDoc._files || {}).filter((key) => key.startsWith('in')));
}
}
// Now trigger regeneration with the updated prompt
handleRegen();
}
catch (error) {
logDebug('Error updating prompt:', error);
}
}, [db, handleRegen, onPromptEdit]);
// Handle document deletion
const handleDelete = React.useCallback(async (id) => {
logDebug('[ImgGen] Attempting to delete document:', id);
try {
// Use await to ensure the operation completes
const result = await db.del(id);
if (debug) {
logDebug('[ImgGen] Document deletion result:', result);
}
// Notify parent component about deletion
if (onDelete) {
if (debug) {
logDebug('[ImgGen] Calling onDelete callback with id:', id);
}
onDelete(id);
}
}
catch (error) {
logDebug('Error deleting document:', error);
}
}, [db, onDelete, debug]);
// Handle document creation from file uploads
const handleDocCreated = React.useCallback((docId) => {
if (debug) {
logDebug('[ImgGenCore] Document created:', docId);
}
// Call user's callback if provided
if (onDocumentCreated) {
onDocumentCreated(docId);
}
}, [onDocumentCreated, debug]);
// Render function that determines what to show based on current mode
function renderContent() {
if (debug) {
logDebug('[ImgGen Debug] Render state:', {
mode,
document: document?._id,
loading,
error: error?.message,
currentEditedPrompt: currentEditedPrompt || null,
imageData: !!imageData,
});
}
// Render different components based on the current mode
switch (mode) {
case 'placeholder': {
// Initial state - no document, no prompt
// Use the same ImgGenUploadWaiting component that's used in uploadWaiting mode
// but without a document (this creates a consistent UI for both entry points)
return (React.createElement(ImgGenUploadWaiting, { className: className, classes: classes, debug: debug, database: database, onDocumentCreated: handleDocCreated, onPromptSubmit: (newPrompt) => {
// When a user enters a prompt directly in the initial state
if (debug) {
logDebug('[ImgGenCore] Prompt submitted from initial view:', newPrompt);
}
// Update the edited prompt and generate a new generationId to trigger generation
setCurrentEditedPrompt(newPrompt);
setGenerationId(uuid());
} }));
}
case 'uploadWaiting': {
// We have a document with uploaded files, waiting for prompt input
if (!document || !document._id) {
// This shouldn't happen - go back to placeholder if no document
return React.createElement(ImgGenPromptWaiting, { className: className, classes: classes });
}
// If loading has started, switch to generating view to show progress
if (loading) {
const displayPrompt = currentEditedPrompt || prompt;
return (React.createElement(ImgGenDisplayPlaceholder, { prompt: displayPrompt || '', loading: loading, progress: progress, error: error, className: className, classes: classes }));
}
return (React.createElement(React.Fragment, null,
React.createElement(ImgGenUploadWaiting, { document: document, className: className, classes: classes, debug: debug, database: database, onFilesAdded: () => {
// Just log if new files were added to the same document
if (debug) {
logDebug('[ImgGenCore] Files added to existing document:', document._id);
}
}, onPromptSubmit: (newPrompt, docId) => {
// Use the docId that's passed from the component if available,
// otherwise fall back to the current document._id
const targetDocId = docId || (document && document._id);
if (debug) {
logDebug('[ImgGenCore] Prompt submitted for existing uploads:', newPrompt);
logDebug('[ImgGenCore] Using document ID:', targetDocId);
}
if (targetDocId) {
// Use the document ID to ensure we're using the correct document with the uploaded images
handlePromptEdit(targetDocId, newPrompt);
}
} })));
}
case 'generating': {
// Document with prompt, waiting for generation to complete
// Use the edited prompt during generation if available, or fall back to document prompt
// Look in three places: 1) edited prompt 2) direct prop 3) document's prompt
let displayPrompt = currentEditedPrompt || prompt;
// If we still don't have a prompt but have a document with a prompt, use that
if (!displayPrompt &&
document &&
'prompt' in document &&
typeof document.prompt === 'string') {
displayPrompt = document.prompt;
}
if (debug) {
logDebug('[ImgGen Debug] Generating state prompt sources:', {
currentEditedPrompt: currentEditedPrompt || null,
propPrompt: prompt || null,
documentPrompt: document?.prompt || null,
finalDisplayPrompt: displayPrompt || null,
});
}
return (React.createElement(ImgGenDisplayPlaceholder, { prompt: displayPrompt || '', loading: loading, progress: progress, error: error, className: className, classes: classes }));
}
case 'display': {
// Document with generated images
if (!document || !document._id) {
return React.createElement(ImgGenError, { message: "Missing document" });
}
return (React.createElement(React.Fragment, null,
React.createElement(ImgGenDisplay, { document: document, loading: loading, progress: progress, onPromptEdit: handlePromptEdit, onDelete: handleDelete, onRegen: handleRegen, alt: alt || '', className: className, classes: classes, debug: debug, error: error })));
}
case 'error': {
// Error state
return (React.createElement(ImgGenError, { message: error ? error.message : 'Unknown error', className: className }));
}
default: {
// Fallback for any unexpected state
return React.createElement(ImgGenError, { message: "Unknown state" });
}
}
}
// Always render through the render function - no conditional returns in the main component body
return renderContent();
}
/**
* Main component for generating images with call-ai's imageGen
* Provides automatic caching, reactive updates, and placeholder handling
* Uses a mountKey to ensure clean state when switching documents
*/
export function ImgGen(props) {
// Destructure key props for identity-change tracking
// classes prop is used via the props spread to ImgGenCore
const { _id, prompt, debug, onDocumentCreated } = props;
// Generate a unique mountKey for this instance
const [mountKey, setMountKey] = React.useState(() => uuid());
// Track document creation from uploads for remounting
const [uploadedDocId, setUploadedDocId] = React.useState(undefined);
// Handle document creation callback - combines user callback with internal state
const handleDocCreated = React.useCallback((docId) => {
if (debug)
logDebug('[ImgGen] Document created:', docId);
// Update internal state to trigger remount
setUploadedDocId(docId);
// Call user's callback if provided
if (onDocumentCreated) {
if (debug)
logDebug('[ImgGen] Calling onDocumentCreated callback');
onDocumentCreated(docId);
}
}, [debug, onDocumentCreated]);
// Track previous props/state to detect identity changes
const prevIdRef = React.useRef(_id);
const prevPromptRef = React.useRef(prompt);
const prevUploadedDocIdRef = React.useRef(uploadedDocId);
// Update mountKey when document identity changes
React.useEffect(() => {
const idChanged = _id !== prevIdRef.current;
const promptChanged = prompt && prompt !== prevPromptRef.current;
const uploadedDocIdChanged = uploadedDocId !== prevUploadedDocIdRef.current;
// Reset mountKey if we switched documents, or if we're showing a new prompt
// with no document ID (which means a brand new generation),
// or if we got a new document ID from uploads
if (idChanged || (!_id && promptChanged) || uploadedDocIdChanged) {
if (debug) {
logDebug('[ImgGen] Identity change detected, generating new mountKey:', {
idChanged,
_id,
prevId: prevIdRef.current,
promptChanged: !_id && promptChanged,
prompt,
prevPrompt: prevPromptRef.current,
uploadedDocIdChanged,
uploadedDocId,
prevUploadedDocId: prevUploadedDocIdRef.current,
});
}
setMountKey(uuid()); // Force a remount of ImgGenCore
}
// Update refs for next comparison
prevIdRef.current = _id;
prevPromptRef.current = prompt;
prevUploadedDocIdRef.current = uploadedDocId;
}, [_id, prompt, uploadedDocId, debug]);
// Create a merged props object with the document creation handler
const coreProps = {
...props,
onDocumentCreated: handleDocCreated,
};
// Handle different cases for document identity
if (uploadedDocId && !_id) {
// Always pass the uploadedDocId to ImgGenCore so it can access the document
// This ensures the document with uploaded files is accessible
coreProps._id = uploadedDocId;
}
// Render the core component with a key to force remount when identity changes
return React.createElement(ImgGenCore, { ...coreProps, key: mountKey });
}
// Simple export - no memoization or complex structure
export default ImgGen;
//# sourceMappingURL=ImgGen.js.map