kbot image chat :)
This commit is contained in:
parent
b34d17191e
commit
de926c429d
BIN
packages/kbot/dist/win-64/tauri-app.exe
vendored
BIN
packages/kbot/dist/win-64/tauri-app.exe
vendored
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
use tauri::{Manager, Emitter};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use dirs;
|
||||
|
||||
struct Counter(std::sync::Mutex<u32>);
|
||||
struct DebugMessages(std::sync::Mutex<Vec<DebugPayload>>);
|
||||
@ -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<String, String> {
|
||||
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<u32, String> {
|
||||
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,
|
||||
|
||||
@ -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<string | null>(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) => (
|
||||
<div key={file.path} className="group">
|
||||
<div className="relative aspect-square glass-card overflow-hidden shadow-md">
|
||||
<img src={file.src} alt={file.path} className="object-cover w-full h-full" />
|
||||
<img
|
||||
src={file.src}
|
||||
alt={file.path}
|
||||
className="object-cover w-full h-full cursor-pointer"
|
||||
onDoubleClick={() => openFullscreen(file.src)}
|
||||
title="Double-click to view fullscreen"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-200"></div>
|
||||
<button
|
||||
type="button"
|
||||
@ -972,16 +1089,20 @@ function App() {
|
||||
<div className="w-full mt-8">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full status-gradient-connected text-white font-bold py-4 px-8 rounded-xl transition-all duration-300 hover:shadow-xl hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 shadow-lg"
|
||||
className={`w-full font-bold py-4 px-8 rounded-xl transition-all duration-300 shadow-lg ${
|
||||
isGenerating
|
||||
? 'bg-slate-400 dark:bg-slate-600 text-white cursor-not-allowed'
|
||||
: 'status-gradient-connected text-white hover:shadow-xl hover:scale-[1.02]'
|
||||
}`}
|
||||
disabled={isGenerating}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||
Generating...
|
||||
<span className="text-white">Generating...</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<span className="flex items-center justify-center gap-2 text-white">
|
||||
✨ Generate Image
|
||||
</span>
|
||||
)}
|
||||
@ -992,42 +1113,130 @@ function App() {
|
||||
{/* Generated Images Display (Chat Mode Only) */}
|
||||
{chatMode && generatedImages.length > 0 && (
|
||||
<div className="w-full mt-12">
|
||||
<h2 className="text-3xl font-bold mb-6 accent-text text-center">Generated Images</h2>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-3xl font-bold accent-text">Generated Images</h2>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={useSelectedImagesForNext}
|
||||
className="glass-button border-blue-400/50 text-blue-600 hover:bg-blue-500/20 px-4 py-2 rounded-lg text-sm font-semibold"
|
||||
disabled={!generatedImages.some(img => img.selectedForNext)}
|
||||
>
|
||||
📁 Use Selected as Input ({generatedImages.filter(img => img.selectedForNext).length})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearGeneratedImages}
|
||||
className="glass-button border-red-400/50 text-red-600 hover:bg-red-500/20 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
🗑️ Clear All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{generatedImages.map((genImage) => (
|
||||
<div key={genImage.id} className="glass-card p-6 glass-shimmer shadow-xl">
|
||||
<div key={genImage.id} className={`glass-card p-6 glass-shimmer shadow-xl transition-all duration-300 cursor-pointer ${
|
||||
genImage.selectedForNext ? 'ring-4 ring-blue-400 bg-blue-50/30 dark:bg-blue-900/30 border-blue-400' : 'hover:ring-2 hover:ring-slate-300'
|
||||
}`}
|
||||
onClick={() => !genImage.id.startsWith('placeholder_') && toggleGeneratedImageSelection(genImage.id)}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="flex-1">
|
||||
<div className="glass-card p-4 mb-3">
|
||||
<p className="text-sm font-semibold text-slate-700 mb-1">Prompt:</p>
|
||||
<p className="text-sm text-slate-600 leading-relaxed">{genImage.prompt}</p>
|
||||
<div className={`flex items-center gap-4 mb-4 p-4 rounded-lg transition-all duration-200 border-2 ${
|
||||
genImage.selectedForNext
|
||||
? 'bg-blue-50 dark:bg-blue-900/30 border-blue-400 dark:border-blue-500 shadow-md'
|
||||
: 'bg-slate-50 dark:bg-slate-800/50 border-slate-200 dark:border-slate-600 hover:border-blue-300 dark:hover:border-blue-600'
|
||||
}`}>
|
||||
<div className={`w-8 h-8 rounded-full border-3 flex items-center justify-center transition-all duration-200 shadow-lg ${
|
||||
genImage.selectedForNext
|
||||
? 'bg-blue-500 border-blue-500 text-white scale-110'
|
||||
: 'border-slate-400 dark:border-slate-400 bg-white dark:bg-slate-700 hover:border-blue-500 hover:bg-blue-50'
|
||||
}`}>
|
||||
{genImage.selectedForNext ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<div className="w-3 h-3 rounded-full bg-slate-300 dark:bg-slate-500"></div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className={`text-base font-semibold ${
|
||||
genImage.selectedForNext
|
||||
? 'text-blue-700 dark:text-blue-300'
|
||||
: 'text-slate-700 dark:text-slate-300'
|
||||
}`}>
|
||||
{genImage.selectedForNext ? '✅ Selected for next generation' : '👆 Click to select for next prompt'}
|
||||
</span>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||
Use this image as input for iterative generation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 font-medium">
|
||||
Generated on {new Date(genImage.timestamp).toLocaleString()}
|
||||
<div className="glass-card p-4 mb-3">
|
||||
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1">Prompt:</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">{genImage.prompt}</p>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium">
|
||||
{genImage.id.startsWith('placeholder_') ? 'Generating...' : `Generated on ${new Date(genImage.timestamp).toLocaleString()}`}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => saveGeneratedImage(genImage)}
|
||||
className={`ml-6 px-6 py-3 rounded-xl text-sm font-semibold transition-all duration-300 shadow-md ${
|
||||
genImage.saved
|
||||
? 'status-gradient-connected text-white cursor-default'
|
||||
: 'glass-button border-emerald-400/50 text-emerald-600 hover:bg-emerald-500/20 hover:shadow-lg hover:scale-105'
|
||||
}`}
|
||||
disabled={genImage.saved}
|
||||
title={genImage.saved ? 'Already saved' : 'Save image'}
|
||||
>
|
||||
{genImage.saved ? '✅ Saved' : '💾 Save Image'}
|
||||
</button>
|
||||
{!genImage.id.startsWith('placeholder_') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent triggering selection
|
||||
saveGeneratedImage(genImage);
|
||||
}}
|
||||
className={`ml-6 px-6 py-3 rounded-xl text-sm font-semibold transition-all duration-300 shadow-md ${
|
||||
genImage.saved
|
||||
? 'status-gradient-connected text-white cursor-default'
|
||||
: 'glass-button border-emerald-400/50 text-emerald-600 hover:bg-emerald-500/20 hover:shadow-lg hover:scale-105'
|
||||
}`}
|
||||
disabled={genImage.saved}
|
||||
title={genImage.saved ? 'Already saved' : 'Save image'}
|
||||
>
|
||||
{genImage.saved ? '✅ Saved' : '💾 Save Image'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={genImage.src}
|
||||
alt={`Generated: ${genImage.prompt}`}
|
||||
className="max-w-full max-h-[500px] rounded-xl shadow-2xl border-2 border-white/30"
|
||||
/>
|
||||
{genImage.id.startsWith('placeholder_') ? (
|
||||
// Placeholder with spinner
|
||||
<div className="w-96 h-96 bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-700 dark:to-slate-800 rounded-xl shadow-2xl border-2 border-slate-300 dark:border-slate-600 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 border-4 border-blue-500/30 border-t-blue-500 rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-slate-600 dark:text-slate-400 font-medium">Generating image...</p>
|
||||
<p className="text-slate-500 dark:text-slate-500 text-sm mt-1">"{genImage.prompt}"</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Actual generated image
|
||||
<img
|
||||
src={genImage.src}
|
||||
alt={`Generated: ${genImage.prompt}`}
|
||||
className={`max-w-full max-h-[500px] rounded-xl shadow-2xl border-4 transition-all duration-300 cursor-pointer ${
|
||||
genImage.selectedForNext
|
||||
? 'border-blue-400 shadow-blue-400/50 scale-[1.02]'
|
||||
: 'border-white/30 hover:border-slate-300'
|
||||
}`}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openFullscreen(genImage.src);
|
||||
}}
|
||||
title="Double-click to view fullscreen"
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-0 rounded-xl ring-1 ring-black/5"></div>
|
||||
{genImage.selectedForNext && !genImage.id.startsWith('placeholder_') && (
|
||||
<div className="absolute top-3 left-3 bg-blue-500 text-white px-3 py-2 rounded-lg text-sm font-semibold shadow-lg flex items-center gap-1">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Selected
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1096,6 +1305,19 @@ function App() {
|
||||
>
|
||||
📊 Stats
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
addDebugMessage('info', '🔄 Manual state reset', {
|
||||
wasGenerating: isGenerating,
|
||||
placeholderCount: generatedImages.filter(img => img.id.startsWith('placeholder_')).length
|
||||
});
|
||||
setIsGenerating(false);
|
||||
setGeneratedImages(prev => prev.filter(img => !img.id.startsWith('placeholder_')));
|
||||
}}
|
||||
className="glass-button text-sm px-4 py-2 rounded-lg border-orange-400/50 text-orange-600 hover:bg-orange-500/20"
|
||||
>
|
||||
🔄 Reset
|
||||
</button>
|
||||
<button
|
||||
onClick={clearDebugMessages}
|
||||
className="glass-button text-sm px-4 py-2 rounded-lg border-gray-400/50 text-gray-600 hover:bg-gray-500/20"
|
||||
@ -1241,6 +1463,33 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fullscreen Image Overlay */}
|
||||
{fullscreenImage && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/90 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
onClick={closeFullscreen}
|
||||
>
|
||||
<div className="relative max-w-[95vw] max-h-[95vh]">
|
||||
<img
|
||||
src={fullscreenImage}
|
||||
alt="Fullscreen view"
|
||||
className="max-w-full max-h-full object-contain rounded-lg shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<button
|
||||
onClick={closeFullscreen}
|
||||
className="absolute top-4 right-4 bg-black/50 hover:bg-black/70 text-white rounded-full w-10 h-10 flex items-center justify-center text-xl font-bold transition-all duration-200 backdrop-blur-sm"
|
||||
title="Close (ESC)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="absolute bottom-4 left-4 bg-black/50 text-white px-3 py-2 rounded-lg text-sm backdrop-blur-sm">
|
||||
Press ESC or click outside to close
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user