tauri - lightbox prompt | file history | store | icons
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
BIN
packages/kbot/dist/win-64/tauri-app.exe
vendored
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1006 KiB After Width: | Height: | Size: 1024 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.3 MiB |
BIN
packages/kbot/generated_gen_11.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
packages/kbot/generated_gen_12.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
packages/kbot/generated_gen_13.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
packages/kbot/generated_gen_14.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1021 KiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 972 KiB After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 2.3 MiB |
10
packages/kbot/gui/tauri-app/package-lock.json
generated
@ -31,6 +31,7 @@
|
||||
"@tauri-apps/plugin-store": "^2.4.0",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@tauri-apps/plugin-upload": "^2.3.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
@ -2473,6 +2474,15 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.544.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz",
|
||||
"integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.19",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
"@tauri-apps/plugin-store": "^2.4.0",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@tauri-apps/plugin-upload": "^2.3.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
|
||||
@ -18,9 +18,6 @@ pub fn start_stdin_listener(app_handle: tauri::AppHandle) {
|
||||
// Parse command from images.ts
|
||||
if let Ok(command) = serde_json::from_str::<serde_json::Value>(&line_content) {
|
||||
if let Some(cmd) = command.get("cmd").and_then(|v| v.as_str()) {
|
||||
log_json("info", "Processing command", Some(serde_json::json!({
|
||||
"command": cmd
|
||||
})));
|
||||
|
||||
match cmd {
|
||||
"forward_config_to_frontend" => {
|
||||
@ -132,12 +129,6 @@ fn handle_image_forward(command: &serde_json::Value, app_handle: &tauri::AppHand
|
||||
command.get("base64").and_then(|v| v.as_str()),
|
||||
command.get("mimeType").and_then(|v| v.as_str())
|
||||
) {
|
||||
log_json("info", "Forwarding image to frontend", Some(serde_json::json!({
|
||||
"filename": filename,
|
||||
"mime_type": mime_type,
|
||||
"base64_size": base64.len()
|
||||
})));
|
||||
|
||||
let image_data = serde_json::json!({
|
||||
"base64": base64,
|
||||
"mimeType": mime_type,
|
||||
@ -150,9 +141,7 @@ fn handle_image_forward(command: &serde_json::Value, app_handle: &tauri::AppHand
|
||||
"filename": filename
|
||||
})));
|
||||
} else {
|
||||
log_json("info", "Image emitted successfully", Some(serde_json::json!({
|
||||
"filename": filename
|
||||
})));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import { ImageFile, PromptTemplate } from "./types";
|
||||
import { useTauriListeners } from "./hooks/useTauriListeners";
|
||||
import { tauriApi } from "./lib/tauriApi";
|
||||
import { saveStore } from "./lib/init";
|
||||
import { saveToStore } from "./lib/init";
|
||||
import { QUICK_STYLES, QUICK_ACTIONS } from "./constants";
|
||||
import log from "./lib/log";
|
||||
import Header from "./components/Header";
|
||||
@ -46,6 +46,14 @@ function App() {
|
||||
const [promptHistory, setPromptHistory] = useState<string[]>([]);
|
||||
const [historyIndex, setHistoryIndex] = useState(-1);
|
||||
const [fileHistory, setFileHistory] = useState<string[]>([]);
|
||||
|
||||
// Debug wrapper for setFileHistory
|
||||
const setFileHistoryWithLogging = (history: string[]) => {
|
||||
log.debug(`🔧 setFileHistory called with ${history.length} files`, {
|
||||
files: history.map(f => f.split(/[/\\]/).pop())
|
||||
});
|
||||
setFileHistory(history);
|
||||
};
|
||||
const [showFileHistory, setShowFileHistory] = useState(false);
|
||||
|
||||
|
||||
@ -80,7 +88,7 @@ function App() {
|
||||
log.info(`🎨 Style replaced with: ${style}`);
|
||||
};
|
||||
|
||||
const executeQuickAction = async (action: { name: string; prompt: string; icon: string }) => {
|
||||
const executeQuickAction = async (action: { name: string; prompt: string; iconName: string }) => {
|
||||
const selectedImages = getSelectedImages();
|
||||
|
||||
if (selectedImages.length === 0) {
|
||||
@ -108,7 +116,7 @@ function App() {
|
||||
|
||||
// Auto-save to store
|
||||
try {
|
||||
await saveStore(prompts, newHistory);
|
||||
await saveToStore({ history: newHistory });
|
||||
log.info('💾 History saved to store');
|
||||
} catch (error) {
|
||||
log.error('Failed to save history', { error: (error as Error).message });
|
||||
@ -138,6 +146,74 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const addToFileHistory = async (filePath: string) => {
|
||||
if (!filePath) return;
|
||||
|
||||
// Use functional update to get current state
|
||||
setFileHistory(currentHistory => {
|
||||
// Remove if already exists, then add to front, then limit to 8
|
||||
const filteredHistory = currentHistory.filter(path => path !== filePath);
|
||||
const newFileHistory = [filePath, ...filteredHistory].slice(0, 8);
|
||||
|
||||
log.info(`📁 Adding to file history: ${filePath.split(/[/\\]/).pop()}`, {
|
||||
currentHistoryLength: currentHistory.length,
|
||||
newHistoryLength: newFileHistory.length,
|
||||
fullPath: filePath,
|
||||
maxEntries: 8
|
||||
});
|
||||
|
||||
// Auto-save to store
|
||||
saveToStore({ fileHistory: newFileHistory }).then(() => {
|
||||
log.info('💾 File history saved to store', {
|
||||
savedCount: newFileHistory.length,
|
||||
savedFiles: newFileHistory.map(f => f.split(/[/\\]/).pop())
|
||||
});
|
||||
}).catch(error => {
|
||||
log.error('Failed to save file history', { error: error.message });
|
||||
});
|
||||
|
||||
return newFileHistory;
|
||||
});
|
||||
};
|
||||
|
||||
const openFileFromHistory = async (filePath: string) => {
|
||||
try {
|
||||
if (await tauriApi.fs.readFile(filePath)) {
|
||||
await addFiles([filePath]);
|
||||
setShowFileHistory(false);
|
||||
log.info(`📂 Reopened from history: ${filePath.split(/[/\\]/).pop()}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`File no longer exists: ${filePath.split(/[/\\]/).pop()}`);
|
||||
// Remove from history if file doesn't exist
|
||||
const updatedHistory = fileHistory.filter(f => f !== filePath);
|
||||
setFileHistoryWithLogging(updatedHistory);
|
||||
await saveToStore({ fileHistory: updatedHistory });
|
||||
}
|
||||
};
|
||||
|
||||
const onFileHistoryCleanup = async (validFiles: string[]) => {
|
||||
setFileHistoryWithLogging(validFiles);
|
||||
await saveToStore({ fileHistory: validFiles });
|
||||
};
|
||||
|
||||
const handleLightboxPromptSubmit = async (promptText: string, imagePath: string) => {
|
||||
// Set the prompt and select the image for editing
|
||||
setPrompt(promptText);
|
||||
|
||||
// Find and select the image
|
||||
setFiles(prev => prev.map(file => ({
|
||||
...file,
|
||||
selected: file.path === imagePath
|
||||
})));
|
||||
|
||||
// Add to history and generate
|
||||
await addToHistory(promptText);
|
||||
await generateImage(promptText, [{ path: imagePath, src: '', isGenerated: false }]);
|
||||
|
||||
log.info(`🎨 Lightbox edit: "${promptText}" on ${imagePath.split(/[/\\]/).pop()}`);
|
||||
};
|
||||
|
||||
const importPrompts = async () => {
|
||||
try {
|
||||
const selected = await tauriApi.dialog.open({
|
||||
@ -270,6 +346,8 @@ function App() {
|
||||
prompt,
|
||||
setCurrentIndex,
|
||||
setPromptHistory,
|
||||
setFileHistory: setFileHistoryWithLogging,
|
||||
addToFileHistory,
|
||||
});
|
||||
|
||||
const addFiles = async (newPaths: string[]) => {
|
||||
@ -552,7 +630,7 @@ function App() {
|
||||
|
||||
const savePrompts = async (promptsToSave: PromptTemplate[]) => {
|
||||
try {
|
||||
await saveStore(promptsToSave, promptHistory);
|
||||
await saveToStore({ prompts: promptsToSave });
|
||||
} catch (error) {
|
||||
log.error('Failed to save prompts', {
|
||||
error: (error as Error).message
|
||||
@ -690,6 +768,12 @@ function App() {
|
||||
promptHistory={promptHistory}
|
||||
historyIndex={historyIndex}
|
||||
navigateHistory={navigateHistory}
|
||||
fileHistory={fileHistory}
|
||||
showFileHistory={showFileHistory}
|
||||
setShowFileHistory={setShowFileHistory}
|
||||
openFileFromHistory={openFileFromHistory}
|
||||
onFileHistoryCleanup={onFileHistoryCleanup}
|
||||
onLightboxPromptSubmit={handleLightboxPromptSubmit}
|
||||
/>
|
||||
|
||||
{/* Debug Panel */}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { ImageFile } from '../types';
|
||||
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
|
||||
import { ArrowUp } from 'lucide-react';
|
||||
|
||||
interface ImageGalleryProps {
|
||||
images: ImageFile[];
|
||||
@ -11,6 +12,12 @@ interface ImageGalleryProps {
|
||||
showSelection?: boolean;
|
||||
currentIndex: number;
|
||||
setCurrentIndex: (index: number) => void;
|
||||
onDoubleClick?: (imagePath: string) => void;
|
||||
onLightboxPromptSubmit?: (prompt: string, imagePath: string) => void;
|
||||
promptHistory?: string[];
|
||||
historyIndex?: number;
|
||||
navigateHistory?: (direction: 'up' | 'down') => void;
|
||||
isGenerating?: boolean;
|
||||
}
|
||||
|
||||
export default function ImageGallery({
|
||||
@ -21,13 +28,27 @@ export default function ImageGallery({
|
||||
onImageSaveAs,
|
||||
showSelection = false,
|
||||
currentIndex,
|
||||
setCurrentIndex
|
||||
setCurrentIndex,
|
||||
onDoubleClick,
|
||||
onLightboxPromptSubmit,
|
||||
promptHistory = [],
|
||||
historyIndex = -1,
|
||||
navigateHistory,
|
||||
isGenerating = false
|
||||
}: ImageGalleryProps) {
|
||||
const [lightboxOpen, setLightboxOpen] = useState(false);
|
||||
const [lightboxLoaded, setLightboxLoaded] = useState(false);
|
||||
const [isPanning, setIsPanning] = useState(false);
|
||||
const [lightboxPrompt, setLightboxPrompt] = useState('');
|
||||
const panStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||
|
||||
// Sync lightbox prompt with history navigation
|
||||
useEffect(() => {
|
||||
if (lightboxOpen && historyIndex >= 0 && historyIndex < promptHistory.length) {
|
||||
setLightboxPrompt(promptHistory[historyIndex]);
|
||||
}
|
||||
}, [historyIndex, promptHistory, lightboxOpen]);
|
||||
|
||||
// Reset current index if images change, preventing out-of-bounds errors
|
||||
useEffect(() => {
|
||||
if (images.length > 0 && currentIndex >= images.length) {
|
||||
@ -188,7 +209,13 @@ export default function ImageGallery({
|
||||
type="button"
|
||||
key={image.path}
|
||||
onClick={(e) => handleThumbnailClick(e, image.path, index)}
|
||||
onDoubleClick={() => openLightbox(index)}
|
||||
onDoubleClick={() => {
|
||||
if (onDoubleClick) {
|
||||
onDoubleClick(image.path);
|
||||
} else {
|
||||
openLightbox(index);
|
||||
}
|
||||
}}
|
||||
className={`group relative aspect-square rounded-lg overflow-hidden transition-all duration-300 border-2 ${
|
||||
currentIndex === index
|
||||
? 'ring-2 ring-orange-500 border-orange-500'
|
||||
@ -373,10 +400,105 @@ export default function ImageGallery({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Lightbox Prompt Field */}
|
||||
{lightboxLoaded && onLightboxPromptSubmit && (
|
||||
<div className="absolute bottom-16 left-1/2 transform -translate-x-1/2 w-[80vw] max-w-4xl">
|
||||
<div className="bg-black/80 backdrop-blur-sm rounded-xl p-4 shadow-2xl border border-white/20">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
value={lightboxPrompt}
|
||||
onChange={(e) => setLightboxPrompt(e.target.value)}
|
||||
placeholder="Quick edit prompt..."
|
||||
disabled={isGenerating}
|
||||
rows={2}
|
||||
className="w-full bg-white/10 border border-white/30 rounded-lg px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent backdrop-blur-sm disabled:opacity-50 resize-none"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && lightboxPrompt.trim() && !isGenerating) {
|
||||
onLightboxPromptSubmit(lightboxPrompt, images[safeIndex].path);
|
||||
setLightboxPrompt('');
|
||||
// Keep lightbox open to show generation progress
|
||||
} else if (e.key === 'Escape') {
|
||||
setLightboxPrompt('');
|
||||
} else if (e.ctrlKey && e.key === 'ArrowUp' && navigateHistory) {
|
||||
e.preventDefault();
|
||||
navigateHistory('up');
|
||||
} else if (e.ctrlKey && e.key === 'ArrowDown' && navigateHistory) {
|
||||
e.preventDefault();
|
||||
navigateHistory('down');
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{/* History navigation buttons */}
|
||||
{promptHistory.length > 0 && (
|
||||
<div className="absolute right-2 top-2 flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateHistory?.('up');
|
||||
}}
|
||||
disabled={isGenerating || historyIndex <= 0}
|
||||
className="p-1 text-white/60 hover:text-white disabled:opacity-30 transition-colors"
|
||||
title={`Previous prompt (${historyIndex + 1}/${promptHistory.length}) - Ctrl+↑`}
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateHistory?.('down');
|
||||
}}
|
||||
disabled={isGenerating}
|
||||
className="p-1 text-white/60 hover:text-white disabled:opacity-30 transition-colors"
|
||||
title={`Next prompt - Ctrl+↓`}
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (lightboxPrompt.trim() && !isGenerating) {
|
||||
onLightboxPromptSubmit(lightboxPrompt, images[safeIndex].path);
|
||||
setLightboxPrompt('');
|
||||
// Keep lightbox open to show generation progress
|
||||
}
|
||||
}}
|
||||
disabled={!lightboxPrompt.trim() || isGenerating}
|
||||
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white p-2 rounded-lg transition-colors duration-200 disabled:opacity-50 flex items-center justify-center group relative self-start"
|
||||
title="Generate (Enter)"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
|
||||
) : (
|
||||
<ArrowUp size={16} />
|
||||
)}
|
||||
{/* Hover tooltip */}
|
||||
<div className="absolute bottom-full mb-2 left-1/2 transform -translate-x-1/2 bg-black text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap">
|
||||
Generate
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info */}
|
||||
{lightboxLoaded && (
|
||||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black/75 text-white px-4 py-2 rounded-lg text-sm">
|
||||
{images[safeIndex].path.split(/[/\\]/).pop()} • {safeIndex + 1} of {images.length} • Scroll to zoom • Double-click to reset • ESC to close
|
||||
{isGenerating ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-3 w-3 border-2 border-white border-t-transparent"></div>
|
||||
Generating edit... • ESC to close
|
||||
</div>
|
||||
) : (
|
||||
`${images[safeIndex].path.split(/[/\\]/).pop()} • ${safeIndex + 1} of ${images.length} • Scroll to zoom • Double-click to reset • ESC to close`
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,35 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ImageFile, PromptTemplate } from '../types';
|
||||
import { QUICK_ACTIONS } from '../constants';
|
||||
import ImageGallery from './ImageGallery';
|
||||
import { useDropZone } from '../hooks/useDropZone';
|
||||
import PromptManager from './PromptManager';
|
||||
import TemplateManager from './TemplateManager';
|
||||
import { tauriApi } from '../lib/tauriApi';
|
||||
import { saveToStore } from '../lib/init';
|
||||
import log from '../lib/log';
|
||||
import { Eraser, Sparkles, Crop, Palette, Package, FolderOpen, Plus, History, ChevronUp, ChevronDown } from 'lucide-react';
|
||||
|
||||
function arrayBufferToBase64(buffer: number[]) {
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
function getActionIcon(iconName: string) {
|
||||
const iconMap = {
|
||||
'Eraser': Eraser,
|
||||
'Sparkles': Sparkles,
|
||||
'Crop': Crop,
|
||||
'Palette': Palette,
|
||||
'Package': Package
|
||||
};
|
||||
const IconComponent = iconMap[iconName as keyof typeof iconMap];
|
||||
return IconComponent ? <IconComponent size={16} /> : <span>{iconName}</span>;
|
||||
}
|
||||
|
||||
interface PromptFormProps {
|
||||
prompt: string;
|
||||
@ -34,10 +60,16 @@ interface PromptFormProps {
|
||||
quickStyles: readonly string[];
|
||||
appendStyle: (style: string) => void;
|
||||
quickActions: typeof QUICK_ACTIONS;
|
||||
executeQuickAction: (action: { name: string; prompt: string; icon: string }) => Promise<void>;
|
||||
executeQuickAction: (action: { name: string; prompt: string; iconName: string }) => Promise<void>;
|
||||
promptHistory: string[];
|
||||
historyIndex: number;
|
||||
navigateHistory: (direction: 'up' | 'down') => void;
|
||||
fileHistory: string[];
|
||||
showFileHistory: boolean;
|
||||
setShowFileHistory: (show: boolean) => void;
|
||||
openFileFromHistory: (filePath: string) => Promise<void>;
|
||||
onFileHistoryCleanup: (validFiles: string[]) => Promise<void>;
|
||||
onLightboxPromptSubmit: (prompt: string, imagePath: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const PromptForm: React.FC<PromptFormProps> = ({
|
||||
@ -73,9 +105,72 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
promptHistory,
|
||||
historyIndex,
|
||||
navigateHistory,
|
||||
fileHistory,
|
||||
showFileHistory,
|
||||
setShowFileHistory,
|
||||
openFileFromHistory,
|
||||
onFileHistoryCleanup,
|
||||
onLightboxPromptSubmit,
|
||||
}) => {
|
||||
const selectedCount = getSelectedImages().length;
|
||||
const { ref: dropZoneRef, dragIn } = useDropZone({ onDrop: addFiles });
|
||||
const [historyImages, setHistoryImages] = useState<ImageFile[]>([]);
|
||||
const [historyCurrentIndex, setHistoryCurrentIndex] = useState(0);
|
||||
|
||||
// Load images for file history when modal opens
|
||||
useEffect(() => {
|
||||
if (showFileHistory && fileHistory.length > 0) {
|
||||
const loadHistoryImages = async () => {
|
||||
const imageFiles: ImageFile[] = [];
|
||||
const validFiles: string[] = [];
|
||||
|
||||
for (const filePath of fileHistory) {
|
||||
try {
|
||||
// First check if file exists, then read it
|
||||
log.debug(`Checking file: ${filePath}`);
|
||||
const buffer = await tauriApi.fs.readFile(filePath);
|
||||
// Use the same conversion method as main app
|
||||
const base64 = arrayBufferToBase64(Array.from(buffer));
|
||||
const ext = filePath.toLowerCase().split('.').pop();
|
||||
const mimeType = ext === 'png' ? 'image/png' : 'image/jpeg';
|
||||
const src = `data:${mimeType};base64,${base64}`;
|
||||
imageFiles.push({
|
||||
path: filePath,
|
||||
src,
|
||||
isGenerated: true,
|
||||
selected: false
|
||||
});
|
||||
validFiles.push(filePath);
|
||||
} catch (error) {
|
||||
// File doesn't exist, skip it and log with full path
|
||||
log.warn(`File not found in history: ${filePath}`, {
|
||||
filename: filePath.split(/[/\\]/).pop(),
|
||||
fullPath: filePath,
|
||||
error: (error as Error).message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If some files were invalid, update the fileHistory and store
|
||||
if (validFiles.length !== fileHistory.length) {
|
||||
log.info(`Cleaning up file history: ${fileHistory.length - validFiles.length} invalid files removed`);
|
||||
try {
|
||||
await onFileHistoryCleanup(validFiles);
|
||||
log.info('💾 File history cleaned and saved');
|
||||
} catch (cleanupError) {
|
||||
log.error('Failed to cleanup file history', { error: (cleanupError as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
setHistoryImages(imageFiles);
|
||||
setHistoryCurrentIndex(0);
|
||||
};
|
||||
loadHistoryImages();
|
||||
} else if (showFileHistory) {
|
||||
// No file history, show empty state immediately
|
||||
setHistoryImages([]);
|
||||
}
|
||||
}, [showFileHistory, fileHistory]);
|
||||
|
||||
return (
|
||||
<form
|
||||
@ -147,7 +242,7 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
className="glass-button px-3 py-3 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-700 disabled:opacity-50"
|
||||
title={`Previous prompt (${historyIndex + 1}/${promptHistory.length}) - Ctrl+↑`}
|
||||
>
|
||||
↑
|
||||
<ChevronUp size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -156,18 +251,19 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
className="glass-button px-3 py-3 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-700 disabled:opacity-50"
|
||||
title={`Next prompt - Ctrl+↓`}
|
||||
>
|
||||
↓
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right column: Templates and Actions (1/3 width) */}
|
||||
<div className="space-y-4">
|
||||
{/* Quick Style Picker */}
|
||||
|
||||
{/* Right column: Quick Tools (1/3 width) */}
|
||||
<div className="space-y-3">
|
||||
{/* Quick Styles - Compact */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Quick Styles</h4>
|
||||
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Styles</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{quickStyles.map((style) => (
|
||||
<button
|
||||
@ -182,9 +278,9 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
{/* Quick Actions - Icons Only */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Quick Actions</h4>
|
||||
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Actions</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{quickActions.map((action) => {
|
||||
const hasSelectedImages = getSelectedImages().length > 0;
|
||||
@ -195,16 +291,16 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
type="button"
|
||||
onClick={() => executeQuickAction(action)}
|
||||
disabled={isGenerating || !hasSelectedImages}
|
||||
className={`text-xs px-2 py-1 rounded transition-colors duration-200 ${
|
||||
className={`text-lg px-2 py-2 rounded transition-colors duration-200 ${
|
||||
!hasAnyImages
|
||||
? 'bg-slate-50 text-slate-400 cursor-not-allowed'
|
||||
: !hasSelectedImages
|
||||
? 'bg-orange-50 text-orange-600 cursor-not-allowed'
|
||||
: 'bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-300'
|
||||
} ${isGenerating ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
title={!hasAnyImages ? 'Add an image first' : !hasSelectedImages ? 'Select an image first' : `Apply ${action.name}`}
|
||||
title={!hasAnyImages ? 'Add an image first' : !hasSelectedImages ? 'Select an image first' : action.name}
|
||||
>
|
||||
{action.icon} {action.name}
|
||||
{getActionIcon(action.iconName)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@ -213,20 +309,18 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-slate-200/50 dark:border-slate-700/50 rounded-xl p-4 bg-slate-50/30 dark:bg-slate-800/30">
|
||||
<PromptManager
|
||||
prompts={prompts}
|
||||
onSelectPrompt={setPrompt}
|
||||
currentPrompt={prompt}
|
||||
onSavePrompt={(name, text) => {
|
||||
const newPrompts = [...prompts, { name, text }];
|
||||
setPrompts(newPrompts);
|
||||
savePrompts(newPrompts);
|
||||
}}
|
||||
onImportPrompts={importPrompts}
|
||||
onExportPrompts={exportPrompts}
|
||||
/>
|
||||
</div>
|
||||
<TemplateManager
|
||||
prompts={prompts}
|
||||
currentPrompt={prompt}
|
||||
onSelectPrompt={setPrompt}
|
||||
onSavePrompt={(name, text) => {
|
||||
const newPrompts = [...prompts, { name, text }];
|
||||
setPrompts(newPrompts);
|
||||
savePrompts(newPrompts);
|
||||
}}
|
||||
onImportPrompts={importPrompts}
|
||||
onExportPrompts={exportPrompts}
|
||||
/>
|
||||
|
||||
{/* Two-column layout: Destination + Source */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@ -247,10 +341,11 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={openSaveDialog}
|
||||
className="glass-button font-semibold py-4 px-5 rounded-xl whitespace-nowrap"
|
||||
className="glass-button font-semibold py-4 px-4 rounded-xl whitespace-nowrap flex items-center gap-2"
|
||||
title="Browse for save location"
|
||||
>
|
||||
📁 Browse
|
||||
<FolderOpen size={16} />
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -263,24 +358,34 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2 text-center">
|
||||
Source Images
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openFilePicker}
|
||||
className="w-full glass-button font-semibold py-4 px-6 rounded-xl hover:border-slate-400/60 dark:hover-border-slate-500/60"
|
||||
className="w-full glass-button font-semibold py-4 px-6 rounded-xl hover:border-slate-400/60 dark:hover-border-slate-500/60 flex items-center justify-center gap-2"
|
||||
>
|
||||
📸 Select Images to Edit (or Drop Here)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addImageFromUrl('https://picsum.photos/640/640')}
|
||||
className="glass-button font-semibold py-4 px-5 rounded-xl whitespace-nowrap"
|
||||
title="Add random image from URL"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<FolderOpen size={16} />
|
||||
Select Images to Edit (or Drop Here)
|
||||
</button>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addImageFromUrl('https://picsum.photos/640/640')}
|
||||
className="glass-button py-2 px-3 rounded-lg"
|
||||
title="Add random image from URL"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFileHistory(true)}
|
||||
disabled={fileHistory.length === 0}
|
||||
className="glass-button py-2 px-3 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title={fileHistory.length > 0 ? `Open from history (${fileHistory.length} files)` : 'No file history yet'}
|
||||
>
|
||||
<History size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -316,6 +421,11 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
onImageDelete={onImageDelete}
|
||||
currentIndex={currentIndex}
|
||||
setCurrentIndex={setCurrentIndex}
|
||||
onLightboxPromptSubmit={onLightboxPromptSubmit}
|
||||
promptHistory={promptHistory}
|
||||
historyIndex={historyIndex}
|
||||
navigateHistory={navigateHistory}
|
||||
isGenerating={isGenerating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -336,6 +446,56 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File History Modal */}
|
||||
{showFileHistory && (
|
||||
<div className="fixed inset-0 bg-black/50 z-[9999] flex items-center justify-center p-4">
|
||||
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
|
||||
<div className="flex justify-between items-center p-6 border-b border-slate-200 dark:border-slate-700">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">File History</h3>
|
||||
<button
|
||||
onClick={() => setShowFileHistory(false)}
|
||||
className="text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 text-2xl"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 overflow-y-auto max-h-[60vh]">
|
||||
{historyImages.length === 0 ? (
|
||||
<div className="text-center text-slate-500 dark:text-slate-400 py-8">
|
||||
{fileHistory.length === 0
|
||||
? "No file history yet. Generated images will appear here."
|
||||
: "Loading images from history..."
|
||||
}
|
||||
</div>
|
||||
) : (
|
||||
<ImageGallery
|
||||
images={historyImages}
|
||||
onImageSelection={(imagePath: string) => {
|
||||
// Single click selects for preview
|
||||
setHistoryImages(prev =>
|
||||
prev.map(img => ({
|
||||
...img,
|
||||
selected: img.path === imagePath
|
||||
}))
|
||||
);
|
||||
}}
|
||||
onImageRemove={undefined}
|
||||
onImageDelete={undefined}
|
||||
onImageSaveAs={undefined}
|
||||
showSelection={false}
|
||||
currentIndex={historyCurrentIndex}
|
||||
setCurrentIndex={setHistoryCurrentIndex}
|
||||
onDoubleClick={(imagePath: string) => {
|
||||
// Double click picks the image and closes modal
|
||||
openFileFromHistory(imagePath);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { PromptTemplate } from '../types';
|
||||
import { Save, Download, Upload } from 'lucide-react';
|
||||
|
||||
interface TemplateManagerProps {
|
||||
prompts: PromptTemplate[];
|
||||
currentPrompt: string;
|
||||
onSelectPrompt: (prompt: string) => void;
|
||||
onSavePrompt: (name: string, text: string) => void;
|
||||
onImportPrompts: () => void;
|
||||
onExportPrompts: () => void;
|
||||
}
|
||||
|
||||
const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
prompts,
|
||||
currentPrompt,
|
||||
onSelectPrompt,
|
||||
onSavePrompt,
|
||||
onImportPrompts,
|
||||
onExportPrompts,
|
||||
}) => {
|
||||
const handleSaveTemplate = () => {
|
||||
if (!currentPrompt.trim()) return;
|
||||
|
||||
const name = prompt('Enter template name:');
|
||||
if (name && name.trim()) {
|
||||
onSavePrompt(name.trim(), currentPrompt);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-slate-200/50 dark:border-slate-700/50 rounded-xl p-4 bg-slate-50/30 dark:bg-slate-800/30">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Left: Template Picker */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Templates</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{prompts.length === 0 ? (
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">No templates saved yet</span>
|
||||
) : (
|
||||
prompts.map((template) => (
|
||||
<button
|
||||
key={template.name}
|
||||
type="button"
|
||||
onClick={() => onSelectPrompt(template.text)}
|
||||
className="text-xs px-2 py-1 rounded bg-purple-100 hover:bg-purple-200 dark:bg-purple-900 dark:hover:bg-purple-800 text-purple-700 dark:text-purple-300 transition-colors duration-200"
|
||||
title={`Load template: ${template.text.substring(0, 50)}...`}
|
||||
>
|
||||
{template.name}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Template Management Icons */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Manage</h4>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveTemplate}
|
||||
disabled={!currentPrompt.trim()}
|
||||
className="p-2 rounded bg-green-100 hover:bg-green-200 dark:bg-green-900 dark:hover:bg-green-800 text-green-700 dark:text-green-300 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Save current prompt as template"
|
||||
>
|
||||
<Save size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onImportPrompts}
|
||||
className="p-2 rounded bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-300 transition-colors duration-200"
|
||||
title="Import templates from file"
|
||||
>
|
||||
<Upload size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onExportPrompts}
|
||||
disabled={prompts.length === 0}
|
||||
className="p-2 rounded bg-orange-100 hover:bg-orange-200 dark:bg-orange-900 dark:hover:bg-orange-800 text-orange-700 dark:text-orange-300 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Export templates to file"
|
||||
>
|
||||
<Download size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateManager;
|
||||
@ -36,27 +36,27 @@ export const QUICK_ACTIONS = [
|
||||
{
|
||||
name: 'Remove Background',
|
||||
prompt: 'remove the background, make it transparent',
|
||||
icon: '🔲'
|
||||
iconName: 'Eraser'
|
||||
},
|
||||
{
|
||||
name: 'Cleanup',
|
||||
prompt: 'clean up and enhance the image, remove noise and artifacts',
|
||||
icon: '✨'
|
||||
iconName: 'Sparkles'
|
||||
},
|
||||
{
|
||||
name: 'Crop Foreground',
|
||||
prompt: 'crop to focus on the main subject, remove unnecessary background',
|
||||
icon: '✂️'
|
||||
iconName: 'Crop'
|
||||
},
|
||||
{
|
||||
name: 'Improve Colors',
|
||||
prompt: 'enhance colors, improve saturation and contrast',
|
||||
icon: '🎨'
|
||||
iconName: 'Palette'
|
||||
},
|
||||
{
|
||||
name: 'Product Presentation',
|
||||
prompt: 'professional product photography style, clean background, studio lighting',
|
||||
icon: '📦'
|
||||
iconName: 'Package'
|
||||
}
|
||||
] as const;
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@ interface TauriListenersProps extends InitCallbacks {
|
||||
prompt: string;
|
||||
setCurrentIndex: (index: number) => void;
|
||||
setPromptHistory: (history: string[]) => void;
|
||||
setFileHistory: (fileHistory: string[]) => void;
|
||||
addToFileHistory: (filePath: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useTauriListeners({
|
||||
@ -29,7 +31,9 @@ export function useTauriListeners({
|
||||
setIsGenerating,
|
||||
prompt,
|
||||
setCurrentIndex,
|
||||
setPromptHistory
|
||||
setPromptHistory,
|
||||
setFileHistory,
|
||||
addToFileHistory
|
||||
}: TauriListenersProps) {
|
||||
useEffect(() => {
|
||||
let unlistenConfig: (() => void) | undefined;
|
||||
@ -52,9 +56,6 @@ export function useTauriListeners({
|
||||
const listeners = await Promise.all([
|
||||
tauriApi.listen(TauriEvent.CONFIG_RECEIVED, async (event: any) => {
|
||||
const data = event.payload;
|
||||
|
||||
log.info('📨 Config received from backend');
|
||||
|
||||
// Process config and update state
|
||||
const initCallbacks: InitCallbacks = {
|
||||
setPrompt,
|
||||
@ -62,7 +63,8 @@ export function useTauriListeners({
|
||||
setApiKey,
|
||||
setPrompts,
|
||||
setIpcInitialized,
|
||||
setPromptHistory
|
||||
setPromptHistory,
|
||||
setFileHistory
|
||||
};
|
||||
|
||||
try {
|
||||
@ -76,12 +78,7 @@ export function useTauriListeners({
|
||||
}),
|
||||
tauriApi.listen(TauriEvent.IMAGE_RECEIVED, (event: any) => {
|
||||
const imageData = event.payload;
|
||||
log.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}`;
|
||||
@ -106,14 +103,16 @@ export function useTauriListeners({
|
||||
setGenerationTimeoutId(null);
|
||||
}
|
||||
setIsGenerating(false);
|
||||
log.info('✅ Generated image added to files', { filename: imageData.filename, prompt });
|
||||
// Add to file history for easy reopening
|
||||
addToFileHistory(imageData.filename).catch(err =>
|
||||
log.error('Failed to add to file history', { error: err.message })
|
||||
);
|
||||
} else {
|
||||
const newImageFile: ImageFile = { path: imageData.filename, src, isGenerated: false };
|
||||
setFiles(prevFiles => {
|
||||
const exists = prevFiles.some(f => f.path === imageData.filename);
|
||||
if (!exists) {
|
||||
log.info(`📁 Adding input image: ${imageData.filename}`);
|
||||
return [...prevFiles, newImageFile];
|
||||
return [...prevFiles, newImageFile];
|
||||
}
|
||||
log.warn(`🔄 Image already exists: ${imageData.filename}`);
|
||||
return prevFiles;
|
||||
|
||||
@ -25,6 +25,7 @@ export interface InitCallbacks {
|
||||
setPrompts: (prompts: PromptTemplate[]) => void;
|
||||
setIpcInitialized: (initialized: boolean) => void;
|
||||
setPromptHistory?: (history: string[]) => void;
|
||||
setFileHistory?: (fileHistory: string[]) => void;
|
||||
}
|
||||
|
||||
const STORE_FILE_NAME = '.kbot-gui.json';
|
||||
@ -97,7 +98,6 @@ export async function loadStore(
|
||||
log.debug(`📄 Store path resolved to: ${storePath}`);
|
||||
|
||||
const content = await tauriApi.fs.readTextFile(storePath);
|
||||
log.debug(`📖 File content length: ${content?.length || 0}`);
|
||||
|
||||
if (content) {
|
||||
const data = JSON.parse(content);
|
||||
@ -113,9 +113,11 @@ export async function loadStore(
|
||||
|
||||
// Load file history if available
|
||||
if (data.fileHistory && Array.isArray(data.fileHistory) && setFileHistory) {
|
||||
log.debug(`Setting file history with ${data.fileHistory.length} files`, {
|
||||
files: data.fileHistory.map((f: string) => f.split(/[/\\]/).pop())
|
||||
});
|
||||
setFileHistory(data.fileHistory);
|
||||
}
|
||||
|
||||
}
|
||||
log.info(`✅ Loaded ${data.prompts.length} prompts, ${data.history?.length || 0} history entries, ${data.fileHistory?.length || 0} file history entries from store`);
|
||||
|
||||
return data.prompts;
|
||||
@ -236,10 +238,6 @@ export async function initializeApp(callbacks: InitCallbacks): Promise<InitState
|
||||
if (state.isTauriEnv) {
|
||||
// Step 2: Request config (this will trigger CONFIG_RECEIVED event)
|
||||
await getConfig();
|
||||
|
||||
log.info('⏳ Waiting for config to be received via event...');
|
||||
// Note: The actual config processing and store loading will happen
|
||||
// in the CONFIG_RECEIVED event handler
|
||||
} else {
|
||||
log.warn('🌐 Running in browser mode, skipping config request');
|
||||
state.isInitialized = true;
|
||||
@ -265,18 +263,12 @@ export async function completeInitialization(
|
||||
callbacks: InitCallbacks
|
||||
): Promise<{ config: InitConfig; prompts: PromptTemplate[] }> {
|
||||
try {
|
||||
log.info('🎯 Completing initialization after config received...');
|
||||
|
||||
// Process the received config
|
||||
const config = processConfig(configData, callbacks);
|
||||
|
||||
// Load the prompts store
|
||||
const prompts = await loadStore(callbacks, callbacks.setPromptHistory);
|
||||
|
||||
log.info('✅ App initialization completed successfully', {
|
||||
configKeys: Object.keys(config),
|
||||
promptCount: prompts.length
|
||||
});
|
||||
// Load the prompts store
|
||||
const prompts = await loadStore(callbacks, callbacks.setPromptHistory, callbacks.setFileHistory);
|
||||
|
||||
return { config, prompts };
|
||||
|
||||
@ -290,9 +282,13 @@ export async function completeInitialization(
|
||||
}
|
||||
|
||||
/**
|
||||
* Save prompts, history, and file history to store
|
||||
* Save specific data to store (merges with existing)
|
||||
*/
|
||||
export async function saveStore(prompts: PromptTemplate[], history?: string[], fileHistory?: string[]): Promise<void> {
|
||||
export async function saveToStore(updates: {
|
||||
prompts?: PromptTemplate[],
|
||||
history?: string[],
|
||||
fileHistory?: string[]
|
||||
}): Promise<void> {
|
||||
const { isTauri: isTauriEnv } = await tauriApi.ensureTauriApi();
|
||||
|
||||
if (!isTauriEnv) {
|
||||
@ -306,15 +302,35 @@ export async function saveStore(prompts: PromptTemplate[], history?: string[], f
|
||||
log.debug(`📁 Got data dir: ${dataDir}`);
|
||||
const storePath = await tauriApi.path.join(dataDir, STORE_FILE_NAME);
|
||||
log.debug(`📄 Store path: ${storePath}`);
|
||||
const dataToSave = JSON.stringify({
|
||||
prompts,
|
||||
history: history || [],
|
||||
fileHistory: fileHistory || []
|
||||
}, null, 2);
|
||||
log.debug(`💾 Data to save:`, { promptCount: prompts.length, dataLength: dataToSave.length });
|
||||
|
||||
// Load existing data first to merge
|
||||
let existingData = { prompts: [], history: [], fileHistory: [] };
|
||||
try {
|
||||
const existingContent = await tauriApi.fs.readTextFile(storePath);
|
||||
if (existingContent) {
|
||||
existingData = JSON.parse(existingContent);
|
||||
}
|
||||
} catch (loadError) {
|
||||
log.debug('No existing store to merge with');
|
||||
}
|
||||
|
||||
// Merge with existing data - only update provided fields
|
||||
const mergedData = {
|
||||
prompts: updates.prompts !== undefined ? updates.prompts : existingData.prompts || [],
|
||||
history: updates.history !== undefined ? updates.history : existingData.history || [],
|
||||
fileHistory: updates.fileHistory !== undefined ? updates.fileHistory : existingData.fileHistory || []
|
||||
};
|
||||
|
||||
const dataToSave = JSON.stringify(mergedData, null, 2);
|
||||
log.debug(`💾 Data to save:`, {
|
||||
promptCount: mergedData.prompts.length,
|
||||
historyCount: mergedData.history.length,
|
||||
fileHistoryCount: mergedData.fileHistory.length,
|
||||
dataLength: dataToSave.length
|
||||
});
|
||||
|
||||
await tauriApi.fs.writeTextFile(storePath, dataToSave);
|
||||
log.info(`✅ Prompts saved to ${storePath}`);
|
||||
log.info(`✅ Store saved with ${mergedData.prompts.length} prompts, ${mergedData.history.length} history, ${mergedData.fileHistory.length} files`);
|
||||
} catch (error) {
|
||||
log.error('Failed to save prompts', {
|
||||
error: (error as Error).message,
|
||||
|
||||