tauri fixes
|
Before Width: | Height: | Size: 1.5 MiB |
BIN
packages/kbot/dist/win-64/tauri-app.exe
vendored
|
Before Width: | Height: | Size: 2.4 MiB |
@ -278,6 +278,22 @@ fn forward_image_to_frontend(base64: String, mime_type: String, filename: String
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn request_file_deletion(path: String) -> Result<(), String> {
|
||||
eprintln!("[RUST LOG]: request_file_deletion command called.");
|
||||
eprintln!("[RUST LOG]: - Path: {}", path);
|
||||
|
||||
let request = serde_json::json!({
|
||||
"type": "delete_request",
|
||||
"path": path,
|
||||
});
|
||||
|
||||
println!("{}", serde_json::to_string(&request).unwrap());
|
||||
eprintln!("[RUST LOG]: Deletion request sent to images.ts");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let app = tauri::Builder::default()
|
||||
@ -302,7 +318,8 @@ pub fn run() {
|
||||
request_config_from_images,
|
||||
forward_config_to_frontend,
|
||||
forward_image_to_frontend,
|
||||
generate_image_via_backend
|
||||
generate_image_via_backend,
|
||||
request_file_deletion
|
||||
])
|
||||
.setup(|app| {
|
||||
#[cfg(debug_assertions)] // only include this code on debug builds
|
||||
@ -405,6 +422,25 @@ pub fn run() {
|
||||
eprintln!("[RUST LOG]: Generation complete emitted successfully");
|
||||
}
|
||||
}
|
||||
"file_deleted_successfully" => {
|
||||
if let Some(path) = command.get("path").and_then(|v| v.as_str()) {
|
||||
eprintln!("[RUST LOG]: Received confirmation of file deletion: {}", path);
|
||||
if let Err(e) = app_handle.emit("file-deleted-successfully", &serde_json::json!({ "path": path })) {
|
||||
eprintln!("[RUST LOG]: Failed to emit file-deleted-successfully: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
"file_deletion_error" => {
|
||||
if let (Some(path), Some(error)) = (
|
||||
command.get("path").and_then(|v| v.as_str()),
|
||||
command.get("error").and_then(|v| v.as_str())
|
||||
) {
|
||||
eprintln!("[RUST LOG]: Received file deletion error for {}: {}", path, error);
|
||||
if let Err(e) = app_handle.emit("file-deletion-error", &serde_json::json!({ "path": path, "error": error })) {
|
||||
eprintln!("[RUST LOG]: Failed to emit file-deletion-error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!("[RUST LOG]: Unknown command: {}", cmd);
|
||||
}
|
||||
|
||||
@ -29,6 +29,12 @@ function App() {
|
||||
const [messageToSend, setMessageToSend] = useState("");
|
||||
const [generationTimeoutId, setGenerationTimeoutId] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
const deleteFilePermanently = async (pathToDelete: string) => {
|
||||
addDebugMessage('info', `Requesting deletion of file: ${pathToDelete}`);
|
||||
// This will be the new tauri command
|
||||
await tauriApi.requestFileDeletion({ path: pathToDelete });
|
||||
};
|
||||
|
||||
const generateDefaultDst = (fileCount: number, firstFilePath?: string) => {
|
||||
if (fileCount === 1 && firstFilePath) {
|
||||
const parsedPath = firstFilePath.split(/[/\\]/).pop() || 'image';
|
||||
@ -91,23 +97,17 @@ function App() {
|
||||
const addFiles = async (newPaths: string[]) => {
|
||||
const uniqueNewPaths = newPaths.filter(newPath => !files.some(f => f.path === newPath));
|
||||
const newImageFiles: ImageFile[] = [];
|
||||
|
||||
|
||||
for (const path of uniqueNewPaths) {
|
||||
try {
|
||||
const relativePath = await tauriApi.resolvePathRelativeToHome(path);
|
||||
if (!relativePath) {
|
||||
console.warn(`Could not resolve relative path for: ${path}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const buffer = await tauriApi.fs.readFile(relativePath, { baseDir: tauriApi.fs.BaseDirectory().Home });
|
||||
|
||||
const buffer = await tauriApi.fs.readFile(path);
|
||||
|
||||
const base64 = arrayBufferToBase64(Array.from(buffer));
|
||||
const mimeType = path.toLowerCase().endsWith('.png') ? 'image/png' :
|
||||
path.toLowerCase().endsWith('.jpg') || path.toLowerCase().endsWith('.jpeg') ? 'image/jpeg' :
|
||||
const mimeType = path.toLowerCase().endsWith('.png') ? 'image/png' :
|
||||
path.toLowerCase().endsWith('.jpg') || path.toLowerCase().endsWith('.jpeg') ? 'image/jpeg' :
|
||||
'image/png';
|
||||
const src = `data:${mimeType};base64,${base64}`;
|
||||
|
||||
|
||||
newImageFiles.push({ path, src });
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : JSON.stringify(e);
|
||||
@ -115,7 +115,7 @@ function App() {
|
||||
tauriApi.logErrorToConsole(`[Frontend Error] Failed to read file ${path}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setFiles(prevFiles => [...prevFiles, ...newImageFiles]);
|
||||
};
|
||||
|
||||
@ -128,9 +128,9 @@ function App() {
|
||||
};
|
||||
|
||||
const toggleImageSelection = (imagePath: string) => {
|
||||
setFiles(prev =>
|
||||
prev.map(file =>
|
||||
file.path === imagePath
|
||||
setFiles(prev =>
|
||||
prev.map(file =>
|
||||
file.path === imagePath
|
||||
? { ...file, selected: !file.selected }
|
||||
: file
|
||||
)
|
||||
@ -183,7 +183,7 @@ function App() {
|
||||
|
||||
setIsGenerating(true);
|
||||
addDebugMessage('info', `🎨 Starting image generation via backend: "${promptText}"`);
|
||||
|
||||
|
||||
// Add placeholder image with spinner to the files grid
|
||||
const placeholderFile: ImageFile = {
|
||||
path: `generating_${Date.now()}`,
|
||||
@ -198,43 +198,43 @@ function App() {
|
||||
</svg>
|
||||
`)
|
||||
};
|
||||
|
||||
|
||||
setFiles(prev => [...prev, placeholderFile]);
|
||||
|
||||
|
||||
try {
|
||||
// Use the images.ts backend instead of direct API calls
|
||||
const filePaths = includeImages.map(img => img.path);
|
||||
const genDst = dst || `generated_${Date.now()}.png`;
|
||||
|
||||
|
||||
addDebugMessage('info', 'Sending generation request to images.ts backend', {
|
||||
prompt: promptText,
|
||||
files: filePaths,
|
||||
dst: genDst
|
||||
});
|
||||
|
||||
|
||||
// Send generation request via Tauri command
|
||||
await tauriApi.generateImageViaBackend({
|
||||
prompt: promptText,
|
||||
prompt: promptText,
|
||||
files: filePaths,
|
||||
dst: genDst
|
||||
});
|
||||
|
||||
|
||||
addDebugMessage('info', '📤 Generation request sent to backend');
|
||||
|
||||
|
||||
// Clear any existing timeout
|
||||
if (generationTimeoutId) {
|
||||
clearTimeout(generationTimeoutId);
|
||||
}
|
||||
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
addDebugMessage('warn', '⏰ Generation timeout - resetting state');
|
||||
setIsGenerating(false);
|
||||
setFiles(prev => prev.filter(file => !file.path.startsWith('generating_')));
|
||||
setGenerationTimeoutId(null);
|
||||
}, 30000);
|
||||
|
||||
|
||||
setGenerationTimeoutId(timeoutId);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
addDebugMessage('error', 'Failed to send generation request', {
|
||||
error: error instanceof Error ? error.message : JSON.stringify(error)
|
||||
@ -267,7 +267,7 @@ function App() {
|
||||
const toggleTheme = () => {
|
||||
const newDarkMode = !isDarkMode;
|
||||
setIsDarkMode(newDarkMode);
|
||||
|
||||
|
||||
if (newDarkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
@ -292,13 +292,13 @@ function App() {
|
||||
}, []);
|
||||
|
||||
async function submit() {
|
||||
|
||||
|
||||
if (apiKey) {
|
||||
// Generate image via backend (always chat mode now)
|
||||
// Use selected images if any, otherwise use all files
|
||||
const selectedImages = getSelectedImages();
|
||||
const imagesToUse = selectedImages.length > 0 ? selectedImages : files.filter(f => !f.path.startsWith('generating_'));
|
||||
|
||||
|
||||
await generateImage(prompt, imagesToUse);
|
||||
// Don't clear prompt - let user iterate
|
||||
} else {
|
||||
@ -309,17 +309,17 @@ function App() {
|
||||
const clearDebugMessages = async () => {
|
||||
setDebugMessages([]);
|
||||
await tauriApi.clearDebugMessages();
|
||||
addDebugMessage('info', 'Debug messages cleared');
|
||||
addDebugMessage('info', 'Debug messages cleared');
|
||||
};
|
||||
|
||||
const sendIPCMessage = async (messageType: string, data: any) => {
|
||||
await tauriApi.sendIPCMessage(messageType, data);
|
||||
addDebugMessage('info', `IPC message sent: ${messageType}`, data);
|
||||
addDebugMessage('info', `IPC message sent: ${messageType}`, data);
|
||||
};
|
||||
|
||||
const sendMessageToImages = async () => {
|
||||
if (!messageToSend.trim()) return;
|
||||
|
||||
|
||||
const message = {
|
||||
message: messageToSend,
|
||||
timestamp: Date.now(),
|
||||
@ -333,7 +333,7 @@ function App() {
|
||||
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
addDebugMessage('error', `Failed to send message: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
||||
// Clear the input
|
||||
setMessageToSend('');
|
||||
};
|
||||
@ -351,7 +351,7 @@ function App() {
|
||||
const fileArray = Array.from(target.files);
|
||||
const newImageFiles: ImageFile[] = [];
|
||||
let loadedCount = 0;
|
||||
|
||||
|
||||
fileArray.forEach(file => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
@ -373,13 +373,13 @@ function App() {
|
||||
input.click();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
if (!tauriApi.dialog.open) {
|
||||
console.error('Open function not available');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const selected = await tauriApi.dialog.open({
|
||||
multiple: true,
|
||||
filters: [{
|
||||
@ -391,7 +391,7 @@ function App() {
|
||||
addFiles(selected);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('File picker error:', e);
|
||||
console.error('File picker error:', e);
|
||||
tauriApi.logErrorToConsole(`[Frontend Error] File picker error: ${JSON.stringify(e)}`);
|
||||
}
|
||||
}
|
||||
@ -400,7 +400,7 @@ function App() {
|
||||
try {
|
||||
// Extract current filename from dst for default, or use smart default
|
||||
const currentFilename = dst.split(/[/\\]/).pop() || generateDefaultDst(files.length, files[0]?.path);
|
||||
|
||||
|
||||
const selected = await tauriApi.dialog.save({
|
||||
defaultPath: currentFilename,
|
||||
filters: [{
|
||||
@ -408,12 +408,12 @@ function App() {
|
||||
extensions: ['png', 'jpg']
|
||||
}]
|
||||
});
|
||||
|
||||
|
||||
if (selected) {
|
||||
setDst(selected);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Save dialog error:', e);
|
||||
console.error('Save dialog error:', e);
|
||||
tauriApi.logErrorToConsole(`[Frontend Error] Save dialog error: ${JSON.stringify(e)}`);
|
||||
}
|
||||
}
|
||||
@ -425,7 +425,7 @@ function App() {
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-gradient-to-br from-indigo-200/30 to-purple-200/30 dark:from-indigo-500/20 dark:to-purple-500/20 rounded-full blur-3xl"></div>
|
||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-gradient-to-tr from-cyan-200/30 to-blue-200/30 dark:from-cyan-500/20 dark:to-blue-500/20 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="w-full max-w-4xl relative z-10 mt-8">
|
||||
<Header
|
||||
showDebugPanel={showDebugPanel}
|
||||
@ -450,6 +450,7 @@ function App() {
|
||||
saveAndClose={saveAndClose}
|
||||
submit={submit}
|
||||
addImageFromUrl={addImageFromUrl}
|
||||
onImageDelete={deleteFilePermanently}
|
||||
/>
|
||||
|
||||
{/* Debug Panel */}
|
||||
|
||||
@ -5,6 +5,7 @@ interface ImageGalleryProps {
|
||||
images: ImageFile[];
|
||||
onImageSelect?: (imagePath: string) => void;
|
||||
onImageRemove?: (imagePath: string) => void;
|
||||
onImageDelete?: (imagePath: string) => void;
|
||||
showSelection?: boolean;
|
||||
}
|
||||
|
||||
@ -12,6 +13,7 @@ export default function ImageGallery({
|
||||
images,
|
||||
onImageSelect,
|
||||
onImageRemove,
|
||||
onImageDelete,
|
||||
showSelection = false
|
||||
}: ImageGalleryProps) {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
@ -74,10 +76,12 @@ export default function ImageGallery({
|
||||
setCurrentIndex(index);
|
||||
|
||||
// If it's a generated image and selection is enabled, also toggle selection
|
||||
/*
|
||||
const isGenerated = imagePath.startsWith('generated_');
|
||||
if (showSelection && isGenerated && onImageSelect) {
|
||||
if (showSelection && onImageSelect) {
|
||||
onImageSelect(imagePath);
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
if (images.length === 0) {
|
||||
@ -202,11 +206,29 @@ export default function ImageGallery({
|
||||
onImageRemove(image.path);
|
||||
}}
|
||||
className="absolute top-1 right-1 bg-red-500/90 hover:bg-red-600 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-all duration-200"
|
||||
title="Remove"
|
||||
title="Remove from view"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Delete button (permanent) */}
|
||||
{!thumbIsGenerating && onImageDelete && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Are you sure you want to permanently delete this file from your disk?')) {
|
||||
onImageDelete(image.path);
|
||||
}
|
||||
}}
|
||||
className="absolute bottom-1 right-1 bg-red-500/80 hover:bg-red-600 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-all duration-200"
|
||||
title="Delete File Permanently"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -18,6 +18,7 @@ interface PromptFormProps {
|
||||
saveAndClose: () => void;
|
||||
submit: () => void;
|
||||
addImageFromUrl: (url: string) => void;
|
||||
onImageDelete?: (path: string) => void;
|
||||
}
|
||||
|
||||
const PromptForm: React.FC<PromptFormProps> = ({
|
||||
@ -36,7 +37,10 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
saveAndClose,
|
||||
submit,
|
||||
addImageFromUrl,
|
||||
onImageDelete
|
||||
}) => {
|
||||
const selectedCount = getSelectedImages().length;
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col items-center glass-card p-8 glass-shimmer shadow-2xl"
|
||||
@ -115,9 +119,9 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">
|
||||
Images ({files.length})
|
||||
{getSelectedImages().length > 0 && (
|
||||
{selectedCount > 0 && (
|
||||
<span className="ml-2 text-blue-600 dark:text-blue-400">
|
||||
• {getSelectedImages().length} selected
|
||||
• {selectedCount} selected
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
@ -137,6 +141,7 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
onImageSelect={toggleImageSelection}
|
||||
onImageRemove={removeFile}
|
||||
showSelection={true}
|
||||
onImageDelete={onImageDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -11,6 +11,7 @@ export enum TauriCommand {
|
||||
CLEAR_DEBUG_MESSAGES = 'clear_debug_messages',
|
||||
SEND_IPC_MESSAGE = 'send_ipc_message',
|
||||
SEND_MESSAGE_TO_STDOUT = 'send_message_to_stdout',
|
||||
REQUEST_FILE_DELETION = 'request_file_deletion',
|
||||
}
|
||||
|
||||
export enum TauriEvent {
|
||||
@ -18,4 +19,6 @@ export enum TauriEvent {
|
||||
IMAGE_RECEIVED = 'image-received',
|
||||
GENERATION_ERROR = 'generation-error',
|
||||
GENERATION_COMPLETE = 'generation-complete',
|
||||
FILE_DELETED_SUCCESSFULLY = 'file-deleted-successfully',
|
||||
FILE_DELETION_ERROR = 'file-deletion-error',
|
||||
}
|
||||
|
||||
@ -31,114 +31,126 @@ export function useTauriListeners({
|
||||
prompt
|
||||
}: TauriListenersProps) {
|
||||
useEffect(() => {
|
||||
const initializeApp = async () => {
|
||||
let unlistenConfig: (() => void) | undefined;
|
||||
let unlistenImage: (() => void) | undefined;
|
||||
let unlistenError: (() => void) | undefined;
|
||||
let unlistenComplete: (() => void) | undefined;
|
||||
let unlistenDeleted: (() => void) | undefined;
|
||||
let unlistenDeleteError: (() => void) | undefined;
|
||||
|
||||
const setupListeners = async () => {
|
||||
await tauriApi.ensureTauriApi();
|
||||
|
||||
if (tauriApi.isTauri()) {
|
||||
addDebugMessage('info', 'IPC system initialized successfully');
|
||||
}
|
||||
|
||||
const setupTauriEventListeners = async () => {
|
||||
if (tauriApi.isTauri()) {
|
||||
try {
|
||||
await tauriApi.listen(TauriEvent.CONFIG_RECEIVED, (event: any) => {
|
||||
const data = event.payload;
|
||||
if (data.prompt) setPrompt(data.prompt);
|
||||
if (data.dst) setDst(data.dst);
|
||||
if (data.apiKey) setApiKey(data.apiKey);
|
||||
setIpcInitialized(true);
|
||||
addDebugMessage('info', '📨 Config received from images.ts', {
|
||||
hasPrompt: !!data.prompt,
|
||||
hasDst: !!data.dst,
|
||||
hasApiKey: !!data.apiKey,
|
||||
fileCount: data.files?.length || 0,
|
||||
});
|
||||
});
|
||||
const listeners = await Promise.all([
|
||||
tauriApi.listen(TauriEvent.CONFIG_RECEIVED, (event: any) => {
|
||||
const data = event.payload;
|
||||
if (data.prompt) setPrompt(data.prompt);
|
||||
if (data.dst) setDst(data.dst);
|
||||
if (data.apiKey) setApiKey(data.apiKey);
|
||||
setIpcInitialized(true);
|
||||
addDebugMessage('info', '📨 Config received from images.ts', {
|
||||
hasPrompt: !!data.prompt,
|
||||
hasDst: !!data.dst,
|
||||
hasApiKey: !!data.apiKey,
|
||||
fileCount: data.files?.length || 0,
|
||||
});
|
||||
}),
|
||||
tauriApi.listen(TauriEvent.IMAGE_RECEIVED, (event: any) => {
|
||||
const imageData = event.payload;
|
||||
addDebugMessage('debug', '🖼️ Processing image data', {
|
||||
filename: imageData.filename,
|
||||
mimeType: imageData.mimeType,
|
||||
base64Length: imageData.base64?.length,
|
||||
hasValidData: !!(imageData.base64 && imageData.mimeType && imageData.filename),
|
||||
});
|
||||
|
||||
await tauriApi.listen(TauriEvent.IMAGE_RECEIVED, (event: any) => {
|
||||
const imageData = event.payload;
|
||||
addDebugMessage('debug', '🖼️ Processing image data', {
|
||||
filename: imageData.filename,
|
||||
mimeType: imageData.mimeType,
|
||||
base64Length: imageData.base64?.length,
|
||||
hasValidData: !!(imageData.base64 && imageData.mimeType && imageData.filename),
|
||||
if (imageData.base64 && imageData.mimeType && imageData.filename) {
|
||||
const src = `data:${imageData.mimeType};base64,${imageData.base64}`;
|
||||
const hasGeneratingPlaceholder = document.querySelector('[src^="data:image/svg+xml"]'); // A bit hacky, but avoids depending on files state
|
||||
const isGeneratedImage = isGenerating || hasGeneratingPlaceholder || imageData.filename.includes('_out') || imageData.filename.includes('generated_');
|
||||
|
||||
if (isGeneratedImage) {
|
||||
const generatedImageFile: ImageFile = { path: `generated_${imageData.filename}`, src };
|
||||
setFiles(prev => {
|
||||
const withoutPlaceholder = prev.filter(file => !file.path.startsWith('generating_') && !file.path.endsWith(imageData.filename) && file.path !== `generated_${imageData.filename}`);
|
||||
return [...withoutPlaceholder, generatedImageFile];
|
||||
});
|
||||
|
||||
if (imageData.base64 && imageData.mimeType && imageData.filename) {
|
||||
const src = `data:${imageData.mimeType};base64,${imageData.base64}`;
|
||||
const hasGeneratingPlaceholder = document.querySelector('[src^="data:image/svg+xml"]'); // A bit hacky, but avoids depending on files state
|
||||
const isGeneratedImage = isGenerating || hasGeneratingPlaceholder || imageData.filename.includes('_out') || imageData.filename.includes('generated_');
|
||||
|
||||
if (isGeneratedImage) {
|
||||
const generatedImageFile: ImageFile = { path: `generated_${imageData.filename}`, src };
|
||||
setFiles(prev => {
|
||||
const withoutPlaceholder = prev.filter(file => !file.path.startsWith('generating_') && !file.path.endsWith(imageData.filename) && file.path !== `generated_${imageData.filename}`);
|
||||
return [...withoutPlaceholder, generatedImageFile];
|
||||
});
|
||||
|
||||
if (generationTimeoutId) {
|
||||
clearTimeout(generationTimeoutId);
|
||||
setGenerationTimeoutId(null);
|
||||
}
|
||||
setIsGenerating(false);
|
||||
addDebugMessage('info', '✅ Generated image added to files', { filename: imageData.filename, prompt });
|
||||
} else {
|
||||
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;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
addDebugMessage('error', '❌ Invalid image data received', {
|
||||
hasBase64: !!imageData.base64,
|
||||
hasMimeType: !!imageData.mimeType,
|
||||
hasFilename: !!imageData.filename,
|
||||
});
|
||||
if (generationTimeoutId) {
|
||||
clearTimeout(generationTimeoutId);
|
||||
setGenerationTimeoutId(null);
|
||||
}
|
||||
});
|
||||
|
||||
await tauriApi.listen(TauriEvent.GENERATION_ERROR, (event: any) => {
|
||||
const errorData = event.payload;
|
||||
addDebugMessage('error', '❌ Generation failed', errorData);
|
||||
setIsGenerating(false);
|
||||
setFiles(prev => prev.filter(file => !file.path.startsWith('generating_')));
|
||||
});
|
||||
|
||||
await tauriApi.listen(TauriEvent.GENERATION_COMPLETE, (event: any) => {
|
||||
const completionData = event.payload;
|
||||
addDebugMessage('info', '✅ Simple mode: Image generation completed', {
|
||||
dst: completionData.dst,
|
||||
prompt: completionData.prompt
|
||||
});
|
||||
setIsGenerating(false);
|
||||
setFiles(prev => prev.filter(file => !file.path.startsWith('generating_')));
|
||||
});
|
||||
|
||||
addDebugMessage('info', 'Tauri event listeners set up');
|
||||
|
||||
try {
|
||||
await tauriApi.requestConfigFromImages();
|
||||
addDebugMessage('info', 'Config request sent to images.ts');
|
||||
} catch (e) {
|
||||
addDebugMessage('error', `Failed to request config: ${e}`);
|
||||
addDebugMessage('info', '✅ Generated image added to files', { filename: imageData.filename, prompt });
|
||||
} else {
|
||||
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;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
addDebugMessage('error', `Failed to set up event listeners: ${error}`);
|
||||
} else {
|
||||
addDebugMessage('error', '❌ Invalid image data received', {
|
||||
hasBase64: !!imageData.base64,
|
||||
hasMimeType: !!imageData.mimeType,
|
||||
hasFilename: !!imageData.filename,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
addDebugMessage('warn', 'Tauri event listeners not available - running in browser mode');
|
||||
}
|
||||
};
|
||||
}),
|
||||
tauriApi.listen(TauriEvent.GENERATION_ERROR, (event: any) => {
|
||||
const errorData = event.payload;
|
||||
addDebugMessage('error', '❌ Generation failed', errorData);
|
||||
setIsGenerating(false);
|
||||
setFiles(prev => prev.filter(file => !file.path.startsWith('generating_')));
|
||||
}),
|
||||
tauriApi.listen(TauriEvent.GENERATION_COMPLETE, (event: any) => {
|
||||
const completionData = event.payload;
|
||||
addDebugMessage('info', '✅ Simple mode: Image generation completed', {
|
||||
dst: completionData.dst,
|
||||
prompt: completionData.prompt
|
||||
});
|
||||
setIsGenerating(false);
|
||||
setFiles(prev => prev.filter(file => !file.path.startsWith('generating_')));
|
||||
}),
|
||||
tauriApi.listen(TauriEvent.FILE_DELETED_SUCCESSFULLY, (event: any) => {
|
||||
const deletedPath = event.payload.path;
|
||||
addDebugMessage('info', `✅ File deleted successfully: ${deletedPath}`);
|
||||
setFiles(prevFiles => prevFiles.filter(file => file.path !== deletedPath));
|
||||
}),
|
||||
tauriApi.listen(TauriEvent.FILE_DELETION_ERROR, (event: any) => {
|
||||
const { path, error } = event.payload;
|
||||
addDebugMessage('error', `Failed to delete file: ${path}`, { error });
|
||||
})
|
||||
]);
|
||||
|
||||
setTimeout(setupTauriEventListeners, 500);
|
||||
[unlistenConfig, unlistenImage, unlistenError, unlistenComplete, unlistenDeleted, unlistenDeleteError] = listeners;
|
||||
|
||||
try {
|
||||
await tauriApi.requestConfigFromImages();
|
||||
addDebugMessage('info', 'Config request sent to images.ts');
|
||||
} catch (e) {
|
||||
addDebugMessage('error', `Failed to request config: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(initializeApp, 200);
|
||||
setupListeners();
|
||||
|
||||
return () => {
|
||||
unlistenConfig?.();
|
||||
unlistenImage?.();
|
||||
unlistenError?.();
|
||||
unlistenComplete?.();
|
||||
unlistenDeleted?.();
|
||||
unlistenDeleteError?.();
|
||||
};
|
||||
}, []); // Empty dependency array to run only once on mount
|
||||
}
|
||||
|
||||
@ -141,4 +141,6 @@ export const tauriApi = {
|
||||
|
||||
sendIPCMessage: (messageType: string, data: any) =>
|
||||
safeInvoke(TauriCommand.SEND_IPC_MESSAGE, { messageType, data }),
|
||||
requestFileDeletion: (data: { path: string }) =>
|
||||
safeInvoke(TauriCommand.REQUEST_FILE_DELETION, data),
|
||||
};
|
||||
|
||||
@ -16,8 +16,8 @@
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
|
||||
@ -2,7 +2,11 @@ import { z } from 'zod';
|
||||
import * as path from 'node:path';
|
||||
import { sync as write } from '@polymech/fs/write';
|
||||
import { sync as exists } from '@polymech/fs/exists';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import {
|
||||
readFileSync,
|
||||
statSync,
|
||||
unlinkSync
|
||||
} from 'node:fs';
|
||||
import { variables } from '../variables.js';
|
||||
import { resolve } from '@polymech/commons';
|
||||
|
||||
@ -162,7 +166,7 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
cmd: 'forward_image_to_frontend',
|
||||
base64,
|
||||
mimeType,
|
||||
filename
|
||||
filename: imagePath
|
||||
};
|
||||
|
||||
tauriProcess.stdin?.write(JSON.stringify(imageResponse) + '\n');
|
||||
@ -172,6 +176,40 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
logger.error(`Failed to send image: ${imagePath}`, error.message);
|
||||
}
|
||||
}
|
||||
} else if (message.type === 'delete_request') {
|
||||
logger.info('📨 Received delete request from GUI');
|
||||
const pathToDelete = message.path;
|
||||
if (pathToDelete && isString(pathToDelete)) {
|
||||
try {
|
||||
if (exists(pathToDelete)) {
|
||||
unlinkSync(pathToDelete);
|
||||
logger.info(`✅ File deleted successfully: ${pathToDelete}`);
|
||||
const successResponse = {
|
||||
cmd: 'file_deleted_successfully',
|
||||
path: pathToDelete
|
||||
};
|
||||
tauriProcess.stdin?.write(JSON.stringify(successResponse) + '\n');
|
||||
} else {
|
||||
logger.warn(`⚠️ File not found for deletion: ${pathToDelete}`);
|
||||
const errorResponse = {
|
||||
cmd: 'file_deletion_error',
|
||||
path: pathToDelete,
|
||||
error: 'File not found on server.'
|
||||
};
|
||||
tauriProcess.stdin?.write(JSON.stringify(errorResponse) + '\n');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to delete file: ${pathToDelete}`, error.message);
|
||||
const errorResponse = {
|
||||
cmd: 'file_deletion_error',
|
||||
path: pathToDelete,
|
||||
error: error.message
|
||||
};
|
||||
tauriProcess.stdin?.write(JSON.stringify(errorResponse) + '\n');
|
||||
}
|
||||
} else {
|
||||
logger.error('Invalid delete request from GUI, path is missing.');
|
||||
}
|
||||
} else if (message.type === 'generate_request') {
|
||||
logger.info('📨 Received generation request from GUI');
|
||||
|
||||
@ -179,7 +217,33 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
try {
|
||||
const genPrompt = message.prompt;
|
||||
const genFiles = message.files || [];
|
||||
const genDst = message.dst;
|
||||
|
||||
// --- New logic for destination path ---
|
||||
let dstDir: string;
|
||||
|
||||
if (argv.dst) {
|
||||
const absoluteDst = path.resolve(argv.dst);
|
||||
const dstStat = exists(absoluteDst) ? statSync(absoluteDst) : null;
|
||||
if (dstStat && dstStat.isDirectory()) {
|
||||
dstDir = absoluteDst;
|
||||
} else {
|
||||
dstDir = path.dirname(absoluteDst);
|
||||
}
|
||||
} else if (genFiles.length > 0) {
|
||||
dstDir = path.dirname(genFiles[0]);
|
||||
} else {
|
||||
dstDir = process.cwd(); // fallback to current working dir
|
||||
}
|
||||
|
||||
const baseFileName = genFiles.length > 0
|
||||
? path.basename(genFiles[0], path.extname(genFiles[0]))
|
||||
: 'generated';
|
||||
|
||||
const newFileName = `${baseFileName}_gen_0.png`;
|
||||
|
||||
const finalDstPath = path.resolve(dstDir, newFileName);
|
||||
logger.info(`📝 Determined destination path for generated image: ${finalDstPath}`);
|
||||
// --- End new logic ---
|
||||
|
||||
logger.info(`🎨 Starting image generation: "${genPrompt}"`);
|
||||
|
||||
@ -192,7 +256,7 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
...argv,
|
||||
prompt: genPrompt,
|
||||
include: genFiles,
|
||||
dst: genDst
|
||||
dst: finalDstPath // Use the new path
|
||||
});
|
||||
imageBuffer = await editImage(genPrompt, genFiles, parsedOptions);
|
||||
} else {
|
||||
@ -201,12 +265,15 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
const parsedOptions = ImageOptionsSchema().parse({
|
||||
...argv,
|
||||
prompt: genPrompt,
|
||||
dst: genDst
|
||||
dst: finalDstPath // Use the new path
|
||||
});
|
||||
imageBuffer = await createImage(genPrompt, parsedOptions);
|
||||
}
|
||||
|
||||
if (imageBuffer) {
|
||||
write(finalDstPath, imageBuffer);
|
||||
logger.info(`✅ Image saved to: ${finalDstPath}`);
|
||||
|
||||
// Send the generated image back to the GUI (chat mode)
|
||||
const base64Result = imageBuffer.toString('base64');
|
||||
|
||||
@ -214,11 +281,11 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
cmd: 'forward_image_to_frontend',
|
||||
base64: base64Result,
|
||||
mimeType: 'image/png',
|
||||
filename: path.basename(genDst)
|
||||
filename: finalDstPath
|
||||
};
|
||||
|
||||
tauriProcess.stdin?.write(JSON.stringify(imageResponse) + '\n');
|
||||
logger.info(`✅ Generated image sent to GUI: ${genDst}`);
|
||||
logger.info(`✅ Generated image sent to GUI: ${path.basename(finalDstPath)}`);
|
||||
} else {
|
||||
logger.error('❌ Failed to generate image');
|
||||
|
||||
|
||||
BIN
packages/kbot/tests/assets/DSC05427.JPG
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
packages/kbot/tests/assets/DSC05427_gen_0.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
packages/kbot/tests/assets/MOMO_1704489700094.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
packages/kbot/tests/assets/katfucked.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
packages/kbot/tests/assets/m1.jpg
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
packages/kbot/tests/assets/m1_gen_0.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
packages/kbot/tests/assets/m2.jpg
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
packages/kbot/tests/assets/m2_gen_0.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 45 KiB |