Skip to content

Commit 5488e07

Browse files
committed
feat: add image editing support with debug logging capabilities
1 parent 6d2cada commit 5488e07

File tree

6 files changed

+195
-10
lines changed

6 files changed

+195
-10
lines changed

examples/react-example/src/App.tsx

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useState, useRef } from 'react';
22
import { ImgGen } from 'use-vibes';
33
import { useFireproof } from 'use-fireproof';
44
import type { DocBase, DocFileMeta } from 'use-fireproof';
@@ -18,6 +18,9 @@ function App() {
1818
const [isGenerating, setIsGenerating] = useState(false);
1919
const [selectedImageId, setSelectedImageId] = useState<string | undefined>();
2020
const [quality, setQuality] = useState<'low' | 'medium' | 'high' | 'auto'>('low');
21+
const [uploadedImage, setUploadedImage] = useState<File | null>(null);
22+
const [imagePreview, setImagePreview] = useState<string | null>(null);
23+
const fileInputRef = useRef<HTMLInputElement>(null);
2124

2225
// Use Fireproof to query all images
2326
const { useLiveQuery } = useFireproof('ImgGen');
@@ -57,6 +60,28 @@ function App() {
5760
setIsGenerating(false);
5861
};
5962

63+
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
64+
if (e.target.files && e.target.files[0]) {
65+
const file = e.target.files[0];
66+
setUploadedImage(file);
67+
68+
// Create preview URL for the uploaded image
69+
const reader = new FileReader();
70+
reader.onload = (event) => {
71+
setImagePreview(event.target?.result as string);
72+
};
73+
reader.readAsDataURL(file);
74+
}
75+
};
76+
77+
const handleClearImage = () => {
78+
setUploadedImage(null);
79+
setImagePreview(null);
80+
if (fileInputRef.current) {
81+
fileInputRef.current.value = '';
82+
}
83+
};
84+
6085
// Get all documents with type: 'image'
6186
const { docs: imageDocuments } = useLiveQuery<ImageDocument>('type', {
6287
key: 'image',
@@ -118,16 +143,55 @@ function App() {
118143
className="generate-button"
119144
disabled={isGenerating || !inputPrompt.trim()}
120145
>
121-
{isGenerating ? 'Generating...' : 'Generate Image'}
146+
{isGenerating ? 'Generating...' : uploadedImage ? 'Edit Image' : 'Generate Image'}
122147
</button>
123148
</form>
124149

150+
<div className="image-upload-container" style={{ marginTop: '20px', marginBottom: '20px' }}>
151+
<h3>Upload an image to edit</h3>
152+
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
153+
<input
154+
type="file"
155+
accept="image/*"
156+
onChange={handleImageUpload}
157+
ref={fileInputRef}
158+
style={{ flexGrow: 1 }}
159+
/>
160+
{uploadedImage && (
161+
<button
162+
onClick={handleClearImage}
163+
style={{
164+
padding: '5px 10px',
165+
backgroundColor: '#f44336',
166+
color: 'white',
167+
border: 'none',
168+
borderRadius: '4px',
169+
cursor: 'pointer',
170+
}}
171+
>
172+
Clear
173+
</button>
174+
)}
175+
</div>
176+
{imagePreview && (
177+
<div style={{ marginTop: '10px', maxWidth: '300px' }}>
178+
<img
179+
src={imagePreview}
180+
alt="Preview"
181+
style={{ width: '100%', borderRadius: '8px', border: '2px solid #ddd' }}
182+
/>
183+
</div>
184+
)}
185+
</div>
186+
125187
<div className="img-wrapper">
126188
<ImgGen
127189
prompt={activePrompt}
128190
_id={selectedImageId}
191+
images={uploadedImage ? [uploadedImage] : undefined}
129192
options={{
130-
quality: quality,
193+
debug: true,
194+
quality,
131195
imgUrl: 'https://vibecode.garden',
132196
size: '1024x1024',
133197
}}

src/components/ImgGen.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { v4 as uuid } from 'uuid';
33
import type { ImageGenOptions } from 'call-ai';
44
import { useImageGen } from '../hooks/image-gen/use-image-gen';
55
import { useFireproof, Database } from 'use-fireproof';
6+
import { setDebugMode } from '../hooks/image-gen/types';
67
import { ImageDocument } from '../hooks/image-gen/types';
78
import {
89
ImgGenPromptWaiting,
@@ -32,6 +33,9 @@ export interface ImgGenProps {
3233
/** Image generation options */
3334
options?: ImageGenOptions;
3435

36+
/** Enable debug logging */
37+
debug?: boolean;
38+
3539
/** Database name or instance to use for storing images */
3640
database?: string | Database;
3741

@@ -79,8 +83,16 @@ function ImgGenCore(props: ImgGenProps): React.ReactElement {
7983
onDelete,
8084
onPromptEdit,
8185
classes = defaultClasses,
86+
debug,
8287
} = props;
8388

89+
// Set debug mode based on prop
90+
React.useEffect(() => {
91+
if (debug) {
92+
setDebugMode(true);
93+
}
94+
}, [debug]);
95+
8496
// Get access to the Fireproof database directly
8597
const { database: db } = useFireproof(database || 'ImgGen');
8698

@@ -348,7 +360,15 @@ function ImgGenCore(props: ImgGenProps): React.ReactElement {
348360
export function ImgGen(props: ImgGenProps): React.ReactElement {
349361
// Destructure key props for identity-change tracking
350362
// classes prop is used via the props spread to ImgGenCore
351-
const { _id, prompt } = props;
363+
const { _id, prompt, debug } = props;
364+
365+
// Enable debug mode if requested
366+
React.useEffect(() => {
367+
if (debug) {
368+
setDebugMode(true);
369+
console.log('[ImgGen] Debug mode enabled, images prop:', props.images);
370+
}
371+
}, [debug, props.images]);
352372

353373
// Generate a unique mountKey for this instance
354374
const [mountKey, setMountKey] = React.useState(() => uuid());

src/hooks/image-gen/image-generator.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
imageGen as originalImageGen,
55
} from 'call-ai';
66
import { MODULE_STATE, getRelevantOptions } from './utils';
7+
import { DEBUG_MODE } from './types';
78

89
// Extend the ImageGenOptions type to include our regeneration ID
910
interface ImageGenOptions extends BaseImageGenOptions {
@@ -15,6 +16,24 @@ interface ImageGenOptions extends BaseImageGenOptions {
1516
* This function maintains a module-level cache to prevent duplicate API calls
1617
*/
1718
export function imageGen(prompt: string, options?: ImageGenOptions): Promise<ImageResponse> {
19+
// Debug: Log options if debug mode is enabled
20+
if (DEBUG_MODE) {
21+
console.log('[ImgGen Debug] imageGen called with prompt:', prompt);
22+
console.log('[ImgGen Debug] imageGen options:', options);
23+
if (options?.images) {
24+
console.log('[ImgGen Debug] imageGen images:', {
25+
count: options.images.length,
26+
fileInfo: options.images.map((img) => ({
27+
name: img.name,
28+
type: img.type,
29+
size: img.size,
30+
})),
31+
});
32+
} else {
33+
console.log('[ImgGen Debug] No images provided in options');
34+
}
35+
}
36+
1837
// Get the relevant options to form a stable key
1938
const relevantOptions = getRelevantOptions(options);
2039

@@ -47,6 +66,9 @@ export function imageGen(prompt: string, options?: ImageGenOptions): Promise<Ima
4766

4867
try {
4968
// Direct import from call-ai - this works consistently with test mocks
69+
if (DEBUG_MODE) {
70+
console.log(`[ImgGen Debug] Calling originalImageGen for request #${requestId}`);
71+
}
5072
promise = originalImageGen(prompt, options);
5173
} catch (e) {
5274
console.error(`[ImgGen Debug] Error with imageGen for request #${requestId}:`, e);
@@ -85,6 +107,13 @@ export function createImageGenerator(requestHash: string) {
85107
// Options key no longer used for logging
86108
JSON.stringify(getRelevantOptions(genOptions)); // Still generate to maintain behavior
87109

110+
// Debug: Log options if debug mode is enabled
111+
if (DEBUG_MODE) {
112+
console.log(`[ImgGen Debug] createImageGenerator wrapper called [ID:${requestHash}]`);
113+
console.log('[ImgGen Debug] Prompt:', promptText);
114+
console.log('[ImgGen Debug] Full genOptions:', genOptions);
115+
}
116+
88117
// Log detailed information about this request - including request hash and options
89118

90119
try {

src/hooks/image-gen/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import type { DocFileMeta, Database } from 'use-fireproof';
22
import { ImageGenOptions, ImageResponse } from 'call-ai';
33

4+
// Global debug mode flag
5+
export let DEBUG_MODE = false;
6+
7+
// Function to set debug mode
8+
export function setDebugMode(enabled: boolean): void {
9+
DEBUG_MODE = enabled;
10+
if (DEBUG_MODE) {
11+
console.log('[ImgGen Debug] Debug mode enabled');
12+
}
13+
}
14+
415
// Interface for our image documents in Fireproof
516
// Interface for prompt entry
617
export interface PromptEntry {

src/hooks/image-gen/use-image-gen.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useState, useEffect, useMemo, useRef } from 'react';
22
import { useFireproof } from 'use-fireproof';
33
import { ImageResponse } from 'call-ai';
4-
import { UseImageGenOptions, UseImageGenResult, ImageDocument } from './types';
4+
import { UseImageGenOptions, UseImageGenResult, ImageDocument, DEBUG_MODE } from './types';
55

66
import {
77
hashInput,
@@ -48,16 +48,44 @@ export function useImageGen({
4848
const size = options?.size || '1024x1024';
4949
const [width, height] = size.split('x').map(Number);
5050

51+
// Debug logging for options and images prop
52+
useEffect(() => {
53+
if (DEBUG_MODE) {
54+
console.log('[useImageGen Debug] Original options:', options);
55+
if (options.images) {
56+
console.log('[useImageGen Debug] Images in options:', options.images);
57+
console.log(
58+
'[useImageGen Debug] Images info:',
59+
options.images.map((img) => ({
60+
name: img.name,
61+
type: img.type,
62+
size: img.size,
63+
}))
64+
);
65+
} else {
66+
console.log('[useImageGen Debug] No images in options');
67+
}
68+
}
69+
}, [options]);
70+
5171
// Memoize options to prevent unnecessary re-renders and regeneration
52-
const memoizedOptions = useMemo(
53-
() => ({
72+
const memoizedOptions = useMemo(() => {
73+
// Include all options including images
74+
const finalOptions = {
5475
size: options?.size,
5576
quality: options?.quality,
5677
model: options?.model,
5778
style: options?.style,
58-
}),
59-
[options?.size, options?.quality, options?.model, options?.style]
60-
);
79+
// Explicitly pass through the images array
80+
images: options?.images,
81+
};
82+
83+
if (DEBUG_MODE) {
84+
console.log('[useImageGen Debug] Memoized options:', finalOptions);
85+
}
86+
87+
return finalOptions;
88+
}, [options?.size, options?.quality, options?.model, options?.style, options?.images]);
6189

6290
// Store reference to previous options to detect changes
6391
const prevOptionsRef = useRef<ReturnType<typeof getRelevantOptions>>(getRelevantOptions({}));

tests/ImgGenImageEdit.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,37 @@ describe('ImgGen Component - Image Editing', () => {
196196
const callArgs = mockImageGen.mock.calls[0][1];
197197
expect(callArgs.images.length).toBe(2);
198198
});
199+
200+
it('should log debug information when debug prop is true', async () => {
201+
// Create mock files
202+
const mockImageFile = createMockFile('debug-test.png');
203+
204+
// Spy on console.log
205+
const consoleSpy = vi.spyOn(console, 'log');
206+
207+
// Reset the mock
208+
mockImageGen.mockClear();
209+
210+
// Render ImgGen with debug prop
211+
render(<ImgGen prompt="debug image edit" images={[mockImageFile]} debug={true} />);
212+
213+
// Wait for component to render and process
214+
await act(async () => {
215+
await new Promise((resolve) => setTimeout(resolve, 50));
216+
});
217+
218+
// Verify debug logs were called
219+
expect(consoleSpy).toHaveBeenCalled();
220+
221+
// Verify imageGen was called with the image
222+
expect(mockImageGen).toHaveBeenCalledWith(
223+
'debug image edit',
224+
expect.objectContaining({
225+
images: [mockImageFile],
226+
})
227+
);
228+
229+
// Clean up
230+
consoleSpy.mockRestore();
231+
});
199232
});

0 commit comments

Comments
 (0)