diff --git a/packages/kbot/dist/win-64/tauri-app.exe b/packages/kbot/dist/win-64/tauri-app.exe index ee420294..8fc4e2b1 100644 Binary files a/packages/kbot/dist/win-64/tauri-app.exe and b/packages/kbot/dist/win-64/tauri-app.exe differ diff --git a/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs b/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs index b9c88956..6fe8d16c 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs +++ b/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ use tauri::{Manager, Emitter}; use serde::{Serialize, Deserialize}; +use dirs; struct Counter(std::sync::Mutex); struct DebugMessages(std::sync::Mutex>); @@ -67,6 +68,23 @@ fn log_error_to_console(error: &str) { eprintln!("[WebView ERROR forwarded from JS]: {}", error); } +#[tauri::command] +fn resolve_path_relative_to_home(absolute_path: String) -> Result { + eprintln!("[RUST LOG]: resolve_path_relative_to_home command called."); + eprintln!("[RUST LOG]: - Received absolute path: {}", absolute_path); + + let home_dir = dirs::home_dir().ok_or_else(|| "Could not find home directory".to_string())?; + + let path_to_resolve = std::path::Path::new(&absolute_path); + + let relative_path = pathdiff::diff_paths(path_to_resolve, home_dir) + .ok_or_else(|| "Failed to calculate relative path from home directory".to_string())?; + + let result = relative_path.to_string_lossy().to_string(); + eprintln!("[RUST LOG]: - Resolved to path relative to home: {}", result); + Ok(result) +} + #[tauri::command] fn increment_counter(state: tauri::State<'_, Counter>) -> Result { eprintln!("[RUST LOG]: increment_counter command called."); @@ -272,6 +290,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ submit_prompt, log_error_to_console, + resolve_path_relative_to_home, increment_counter, get_counter, reset_counter, diff --git a/packages/kbot/gui/tauri-app/src/App.tsx b/packages/kbot/gui/tauri-app/src/App.tsx index 793f7d74..46dcce19 100644 --- a/packages/kbot/gui/tauri-app/src/App.tsx +++ b/packages/kbot/gui/tauri-app/src/App.tsx @@ -87,6 +87,8 @@ interface GeneratedImage { prompt: string; timestamp: number; saved?: boolean; + selectedForNext?: boolean; + timeoutId?: NodeJS.Timeout; } function arrayBufferToBase64(buffer: number[]) { @@ -113,6 +115,7 @@ function App() { const [showDebugPanel, setShowDebugPanel] = useState(true); // Default open for debugging const [ipcInitialized, setIpcInitialized] = useState(false); const [messageToSend, setMessageToSend] = useState(""); + const [fullscreenImage, setFullscreenImage] = useState(null); const generateDefaultDst = (fileCount: number, firstFilePath?: string) => { if (fileCount === 1 && firstFilePath) { @@ -195,6 +198,70 @@ function App() { console.log('Cleared all files'); }; + const toggleGeneratedImageSelection = (imageId: string) => { + setGeneratedImages(prev => + prev.map(img => + img.id === imageId + ? { ...img, selectedForNext: !img.selectedForNext } + : img + ) + ); + }; + + const useSelectedImagesForNext = () => { + const selectedImages = generatedImages.filter(img => img.selectedForNext); + + if (selectedImages.length === 0) { + addDebugMessage('warn', 'No generated images selected'); + return; + } + + // Convert generated images to ImageFile format + const newImageFiles: ImageFile[] = selectedImages.map(img => ({ + path: `generated_${img.id}.png`, + src: img.src + })); + + // Add to current files + setFiles(prevFiles => [...prevFiles, ...newImageFiles]); + + // Clear selections + setGeneratedImages(prev => + prev.map(img => ({ ...img, selectedForNext: false })) + ); + + addDebugMessage('info', `📁 Added ${selectedImages.length} generated images as input`, { + images: selectedImages.map(img => `generated_${img.id}.png`) + }); + }; + + const clearGeneratedImages = () => { + setGeneratedImages([]); + addDebugMessage('info', '🗑️ Cleared generated images gallery'); + }; + + const openFullscreen = (imageSrc: string) => { + setFullscreenImage(imageSrc); + }; + + const closeFullscreen = () => { + setFullscreenImage(null); + }; + + // ESC key handler for fullscreen + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && fullscreenImage) { + closeFullscreen(); + } + }; + + if (fullscreenImage) { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + } + }, [fullscreenImage]); + const generateImage = async (promptText: string, includeImages: ImageFile[] = []) => { if (!apiKey) { addDebugMessage('error', 'No API key available for image generation'); @@ -204,6 +271,18 @@ function App() { setIsGenerating(true); addDebugMessage('info', `🎨 Starting image generation via backend: "${promptText}"`); + // Add placeholder image with spinner + const placeholderImage: GeneratedImage = { + id: `placeholder_${Date.now()}`, + src: '', // Empty src for placeholder + prompt: promptText, + timestamp: Date.now(), + saved: false, + selectedForNext: false + }; + + setGeneratedImages(prev => [...prev, placeholderImage]); + try { // Use the images.ts backend instead of direct API calls const filePaths = includeImages.map(img => img.path); @@ -224,15 +303,24 @@ function App() { addDebugMessage('info', '📤 Generation request sent to backend'); + // Safety timeout to reset generating state (in case something goes wrong) + const timeoutId = setTimeout(() => { + addDebugMessage('warn', '⏰ Generation timeout - resetting state'); + setIsGenerating(false); + setGeneratedImages(prev => prev.filter(img => !img.id.startsWith('placeholder_'))); + }, 30000); // 30 second timeout + + // Store timeout ID so we can clear it when generation completes + placeholderImage.timeoutId = timeoutId; + } catch (error) { addDebugMessage('error', 'Failed to send generation request', { error: error instanceof Error ? error.message : JSON.stringify(error) }); setIsGenerating(false); + // Remove placeholder on error + setGeneratedImages(prev => prev.filter(img => img.id !== placeholderImage.id)); } - - // Note: setIsGenerating(false) will be called when we receive the generated image - // or error response from the backend }; // Theme management @@ -379,22 +467,58 @@ function App() { }; testImg.src = src; - const newImageFile = { path: imageData.filename, src }; - setFiles(prevFiles => { - const exists = prevFiles.some(f => f.path === imageData.filename); - if (!exists) { - addDebugMessage('info', `📁 Adding image to files: ${imageData.filename}`); - return [...prevFiles, newImageFile]; - } - addDebugMessage('warn', `🔄 Image already exists: ${imageData.filename}`); - return prevFiles; - }); + // Check if this is a generated image (output file) or input image + const isGeneratedImage = imageData.filename.includes('_out') || + imageData.filename.includes('generated_') || + isGenerating; - addDebugMessage('info', '📨 Image received from images.ts', { - filename: imageData.filename, - mimeType: imageData.mimeType, - size: `${Math.round(imageData.base64.length/1024)}KB` - }); + if (isGeneratedImage) { + // This is a generated image - replace placeholder or add new + const generatedImage: GeneratedImage = { + id: Date.now().toString(), + src, + prompt: prompt || 'Generated image', + timestamp: Date.now(), + saved: false, + selectedForNext: false + }; + + setGeneratedImages(prev => { + // Find and clear timeout for any placeholder + const placeholder = prev.find(img => img.id.startsWith('placeholder_')); + if (placeholder?.timeoutId) { + clearTimeout(placeholder.timeoutId); + } + + // Remove any placeholder and add the real image + const withoutPlaceholder = prev.filter(img => !img.id.startsWith('placeholder_')); + return [...withoutPlaceholder, generatedImage]; + }); + + setIsGenerating(false); // Complete the generation + addDebugMessage('info', '✅ Generated image added to gallery', { + filename: imageData.filename, + prompt: prompt + }); + } else { + // This is an input image - add to files + const newImageFile = { path: imageData.filename, src }; + setFiles(prevFiles => { + const exists = prevFiles.some(f => f.path === imageData.filename); + if (!exists) { + addDebugMessage('info', `📁 Adding input image: ${imageData.filename}`); + return [...prevFiles, newImageFile]; + } + addDebugMessage('warn', `🔄 Image already exists: ${imageData.filename}`); + return prevFiles; + }); + + addDebugMessage('info', '📨 Input image received from images.ts', { + filename: imageData.filename, + mimeType: imageData.mimeType, + size: `${Math.round(imageData.base64.length/1024)}KB` + }); + } } else { addDebugMessage('error', '❌ Invalid image data received', { hasBase64: !!imageData.base64, @@ -404,32 +528,19 @@ function App() { } }); - // Listen for generation results - await listen('generation-result', (event: any) => { - const resultData = event.payload; - addDebugMessage('info', '🎨 Generation completed', resultData); - - if (resultData.success && resultData.base64) { - const generatedImage: GeneratedImage = { - id: Date.now().toString(), - src: `data:image/png;base64,${resultData.base64}`, - prompt: resultData.prompt || 'Generated image', - timestamp: Date.now(), - saved: false - }; - - setGeneratedImages(prev => [...prev, generatedImage]); - addDebugMessage('info', '✅ Generated image added to gallery'); - } - - setIsGenerating(false); - }); + // Listen for generation results (generated images come as regular image-received events) + // We'll detect generation completion by checking if it's a generated image + + // For now, we'll rely on the image-received event and detect if it's a generated result + // The generated image will be added to the generated images gallery // Listen for generation errors await listen('generation-error', (event: any) => { const errorData = event.payload; addDebugMessage('error', '❌ Generation failed', errorData); setIsGenerating(false); + // Remove any placeholder images + setGeneratedImages(prev => prev.filter(img => !img.id.startsWith('placeholder_'))); }); addDebugMessage('info', 'Tauri event listeners set up'); @@ -512,9 +623,9 @@ function App() { console.log('=================='); if (chatMode && apiKey) { - // Chat mode: generate image directly in frontend + // Chat mode: generate image via backend await generateImage(prompt, files); - setPrompt(''); // Clear prompt after generation + // Don't clear prompt immediately - let user decide if they want to keep it } else { // Simple mode: send to CLI try { @@ -949,7 +1060,13 @@ function App() { {files.map((file) => (
- {file.path} + {file.path} openFullscreen(file.src)} + title="Double-click to view fullscreen" + />
+ +
+
{generatedImages.map((genImage) => ( -
+
!genImage.id.startsWith('placeholder_') && toggleGeneratedImageSelection(genImage.id)} + >
-
-

Prompt:

-

{genImage.prompt}

+
+
+ {genImage.selectedForNext ? ( + + + + ) : ( +
+ )} +
+
+ + {genImage.selectedForNext ? '✅ Selected for next generation' : '👆 Click to select for next prompt'} + +

+ Use this image as input for iterative generation +

+
-

- Generated on {new Date(genImage.timestamp).toLocaleString()} +

+

Prompt:

+

{genImage.prompt}

+
+

+ {genImage.id.startsWith('placeholder_') ? 'Generating...' : `Generated on ${new Date(genImage.timestamp).toLocaleString()}`}

- + {!genImage.id.startsWith('placeholder_') && ( + + )}
- {`Generated: + {genImage.id.startsWith('placeholder_') ? ( + // Placeholder with spinner +
+
+
+

Generating image...

+

"{genImage.prompt}"

+
+
+ ) : ( + // Actual generated image + {`Generated: { + e.stopPropagation(); + openFullscreen(genImage.src); + }} + title="Double-click to view fullscreen" + /> + )}
+ {genImage.selectedForNext && !genImage.id.startsWith('placeholder_') && ( +
+ + + + Selected +
+ )}
@@ -1096,6 +1305,19 @@ function App() { > 📊 Stats +
+ + {/* Fullscreen Image Overlay */} + {fullscreenImage && ( +
+
+ Fullscreen view e.stopPropagation()} + /> + +
+ Press ESC or click outside to close +
+
+
+ )} ); }