zoom | history

This commit is contained in:
lovebird 2025-09-21 05:10:17 +02:00
parent 75abc7830f
commit 6eda5f2796
12 changed files with 829 additions and 723 deletions

BIN
packages/kbot/cat_gen_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
packages/kbot/cat_gen_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -34,7 +34,8 @@
"mime-types": "^2.1.35",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-terminal": "^1.4.5"
"react-terminal": "^1.4.5",
"react-zoom-pan-pinch": "^3.7.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.13",
@ -2675,6 +2676,20 @@
"ua-parser-js": "^2.0.0"
}
},
"node_modules/react-zoom-pan-pinch": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz",
"integrity": "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==",
"license": "MIT",
"engines": {
"node": ">=8",
"npm": ">=5"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/rollup": {
"version": "4.50.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz",

View File

@ -37,7 +37,8 @@
"mime-types": "^2.1.35",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-terminal": "^1.4.5"
"react-terminal": "^1.4.5",
"react-zoom-pan-pinch": "^3.7.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.13",

View File

@ -45,6 +45,8 @@ function App() {
const [prompts, setPrompts] = useState<PromptTemplate[]>([]);
const [promptHistory, setPromptHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
const [fileHistory, setFileHistory] = useState<string[]>([]);
const [showFileHistory, setShowFileHistory] = useState(false);
const appendStyle = (style: string) => {
@ -97,11 +99,20 @@ function App() {
await generateImage(action.prompt, [targetImage]);
};
const addToHistory = (promptText: string) => {
const addToHistory = async (promptText: string) => {
if (promptText.trim() && !promptHistory.includes(promptText.trim())) {
setPromptHistory(prev => [...prev, promptText.trim()]);
const newHistory = [...promptHistory, promptText.trim()];
setPromptHistory(newHistory);
setHistoryIndex(-1); // Reset to end of history
log.info(`📝 Added to history: "${promptText.substring(0, 50)}..."`);
// Auto-save to store
try {
await saveStore(prompts, newHistory);
log.info('💾 History saved to store');
} catch (error) {
log.error('Failed to save history', { error: (error as Error).message });
}
}
};
@ -257,6 +268,8 @@ function App() {
setGenerationTimeoutId,
setIsGenerating,
prompt,
setCurrentIndex,
setPromptHistory,
});
const addFiles = async (newPaths: string[]) => {
@ -492,7 +505,7 @@ function App() {
if (apiKey) {
// Add to history before generating
addToHistory(prompt);
await addToHistory(prompt);
// Generate image via backend (always chat mode now)
// Only use explicitly selected images. If none are selected, generate from prompt alone.
@ -539,7 +552,7 @@ function App() {
const savePrompts = async (promptsToSave: PromptTemplate[]) => {
try {
await saveStore(promptsToSave);
await saveStore(promptsToSave, promptHistory);
} catch (error) {
log.error('Failed to save prompts', {
error: (error as Error).message

View File

@ -1,232 +1,232 @@
import React from 'react';
import { tauriApi } from '../lib/tauriApi';
import log from '../lib/log';
// Safe JSON stringify to prevent circular reference crashes
function safeStringify(obj: any, maxDepth = 3): string {
const seen = new WeakSet();
function serialize(value: any, depth: number): any {
if (depth > maxDepth) {
return '[Max depth reached]';
}
if (value === null || value === undefined) {
return value;
}
if (typeof value !== 'object') {
return value;
}
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
if (Array.isArray(value)) {
return value.slice(0, 10).map((item, index) => {
if (index >= 10) return '[Truncated]';
return serialize(item, depth + 1);
});
}
const result: any = {};
const keys = Object.keys(value).slice(0, 20); // Limit keys
for (const key of keys) {
try {
result[key] = serialize(value[key], depth + 1);
} catch (e) {
result[key] = '[Serialization Error]';
}
}
if (Object.keys(value).length > 20) {
result['[truncated]'] = `${Object.keys(value).length - 20} more keys...`;
}
return result;
}
try {
return JSON.stringify(serialize(obj, 0), null, 2);
} catch (e) {
return `[Serialization failed: ${e instanceof Error ? e.message : 'Unknown error'}]`;
}
}
interface DebugPanelProps {
debugMessages: any[];
sendIPCMessage: (messageType: string, data: any) => void;
clearDebugMessages: () => void;
ipcInitialized: boolean;
messageToSend: string;
setMessageToSend: (message: string) => void;
sendMessageToImages: () => void;
}
const DebugPanel: React.FC<DebugPanelProps> = ({
debugMessages,
sendIPCMessage,
clearDebugMessages,
ipcInitialized,
messageToSend,
setMessageToSend,
sendMessageToImages,
}) => {
return (
<div className="w-full mt-12">
<div className="glass-card p-6 glass-shimmer shadow-xl">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold accent-text">Debug Panel</h2>
<div className="flex gap-3">
<button
onClick={async () => {
try {
// Get all relevant paths and store info
const dataDir = tauriApi.isTauri()
? await tauriApi.path.appDataDir()
: 'N/A (not in Tauri)';
const storePath = tauriApi.isTauri() && dataDir !== 'N/A (not in Tauri)'
? await tauriApi.path.join(dataDir, '.kbot-gui.json')
: 'N/A';
log.info('System Info & Store Paths', {
platform: navigator.platform,
userAgent: navigator.userAgent,
isTauri: tauriApi.isTauri(),
dataDir,
storePath,
cwd: 'Available in Node.js only',
timestamp: new Date().toISOString(),
windowLocation: window.location.href
});
} catch (error) {
log.error('Failed to get system info', { error: (error as Error).message });
}
}}
className="glass-button text-sm px-4 py-2 rounded-lg"
>
Test Info
</button>
<button
onClick={() => log.error('Test error message')}
className="glass-button text-sm px-4 py-2 rounded-lg border-red-400/50 text-red-600 hover:bg-red-500/20"
>
Test Error
</button>
<button
onClick={() => sendIPCMessage('test-message', { content: 'Hello from GUI', timestamp: Date.now() })}
className="glass-button text-sm px-4 py-2 rounded-lg border-blue-400/50 text-blue-600 hover:bg-blue-500/20"
>
Send IPC
</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"
>
Clear
</button>
</div>
</div>
<div className="glass-card max-h-96 overflow-y-auto">
{debugMessages.length === 0 ? (
<div className="p-4 text-center text-slate-500 dark:text-slate-400">
No debug messages yet.
</div>
) : (
<div className="divide-y divide-slate-200/50 dark:divide-slate-700/50">
{debugMessages.map((msg, index) => (
<div key={index} className={`p-3 hover:bg-slate-50/50 dark:hover:bg-slate-800/50 ${msg.isIPC ? 'border-l-4 border-blue-400 bg-blue-50/30 dark:bg-blue-900/10' : ''}`}>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className={`text-xs px-2 py-1 rounded-full font-semibold uppercase ${msg.isIPC ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 border border-blue-300' : msg.level === 'error' ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' : msg.level === 'warn' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' : msg.level === 'debug' ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'}`}>
{msg.isIPC ? '📨 IPC' : msg.level}
</span>
<span className="text-xs text-slate-500 dark:text-slate-400">{msg.timestamp}</span>
{msg.isIPC && msg.data?.id && (
<span className="text-xs text-blue-500 dark:text-blue-400 bg-blue-100/50 dark:bg-blue-900/20 px-1 rounded font-mono">
{msg.data.id.split('_').pop()}
</span>
)}
</div>
<div className="text-sm text-slate-700 dark:text-slate-300 mb-1">{msg.message}</div>
{msg.data && (
<div className="text-xs text-slate-500 dark:text-slate-400 bg-slate-100/50 dark:bg-slate-800/50 p-2 rounded font-mono max-h-40 overflow-y-auto">
{safeStringify(msg.data)}
</div>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="mt-6 glass-card p-4">
<div className="flex justify-between items-center mb-3">
<h3 className="text-lg font-semibold accent-text">Send Message to images.ts</h3>
<span className={`text-xs text-slate-500 dark:text-slate-400 bg-slate-100/50 dark:bg-slate-800/50 px-2 py-1 rounded ${ipcInitialized ? 'text-green-600' : 'text-red-600'}`}>
{ipcInitialized ? '🟢 Connected' : '🔴 Disconnected'}
</span>
</div>
<div className="flex gap-3">
<textarea
value={messageToSend}
onChange={(e) => setMessageToSend(e.target.value)}
placeholder="Type a message to send to images.ts process..."
className="flex-1 glass-input p-3 rounded-lg min-h-[80px] resize-none text-sm"
rows={3}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
sendMessageToImages();
}
}}
/>
<div className="flex flex-col gap-2">
<button
onClick={sendMessageToImages}
disabled={!messageToSend.trim()}
className="glass-button px-4 py-2 rounded-lg border-blue-400/50 text-blue-600 hover:bg-blue-500/20 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-semibold"
title="Send message (Ctrl+Enter)"
>
📤 Send
</button>
<button
onClick={() => {
setMessageToSend('Hello from GUI!');
}}
className="glass-button px-4 py-2 rounded-lg border-green-400/50 text-green-600 hover:bg-green-500/20 text-xs"
title="Quick test message"
>
Test
</button>
<button
onClick={() => {
const echoMsg = `Echo: ${Date.now()}`;
setMessageToSend(echoMsg);
setTimeout(() => sendMessageToImages(), 100);
}}
className="glass-button px-4 py-2 rounded-lg border-orange-400/50 text-orange-600 hover:bg-orange-500/20 text-xs"
title="Send echo test"
>
Echo
</button>
</div>
</div>
<div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
💡 Press <kbd className="px-1 py-0.5 bg-slate-200 dark:bg-slate-700 rounded text-xs">Ctrl+Enter</kbd> to send quickly
</div>
</div>
</div>
</div>
);
};
export default DebugPanel;
import React from 'react';
import { tauriApi } from '../lib/tauriApi';
import log from '../lib/log';
// Safe JSON stringify to prevent circular reference crashes
function safeStringify(obj: any, maxDepth = 3): string {
const seen = new WeakSet();
function serialize(value: any, depth: number): any {
if (depth > maxDepth) {
return '[Max depth reached]';
}
if (value === null || value === undefined) {
return value;
}
if (typeof value !== 'object') {
return value;
}
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
if (Array.isArray(value)) {
return value.slice(0, 10).map((item, index) => {
if (index >= 10) return '[Truncated]';
return serialize(item, depth + 1);
});
}
const result: any = {};
const keys = Object.keys(value).slice(0, 20); // Limit keys
for (const key of keys) {
try {
result[key] = serialize(value[key], depth + 1);
} catch (e) {
result[key] = '[Serialization Error]';
}
}
if (Object.keys(value).length > 20) {
result['[truncated]'] = `${Object.keys(value).length - 20} more keys...`;
}
return result;
}
try {
return JSON.stringify(serialize(obj, 0), null, 2);
} catch (e) {
return `[Serialization failed: ${e instanceof Error ? e.message : 'Unknown error'}]`;
}
}
interface DebugPanelProps {
debugMessages: any[];
sendIPCMessage: (messageType: string, data: any) => void;
clearDebugMessages: () => void;
ipcInitialized: boolean;
messageToSend: string;
setMessageToSend: (message: string) => void;
sendMessageToImages: () => void;
}
const DebugPanel: React.FC<DebugPanelProps> = ({
debugMessages,
sendIPCMessage,
clearDebugMessages,
ipcInitialized,
messageToSend,
setMessageToSend,
sendMessageToImages,
}) => {
return (
<div className="w-full mt-12">
<div className="glass-card p-6 glass-shimmer shadow-xl">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold accent-text">Debug Panel</h2>
<div className="flex gap-3">
<button
onClick={async () => {
try {
// Get all relevant paths and store info
const dataDir = tauriApi.isTauri()
? await tauriApi.path.appDataDir()
: 'N/A (not in Tauri)';
const storePath = tauriApi.isTauri() && dataDir !== 'N/A (not in Tauri)'
? await tauriApi.path.join(dataDir, '.kbot-gui.json')
: 'N/A';
log.info('System Info & Store Paths', {
platform: navigator.platform,
userAgent: navigator.userAgent,
isTauri: tauriApi.isTauri(),
dataDir,
storePath,
cwd: 'Available in Node.js only',
timestamp: new Date().toISOString(),
windowLocation: window.location.href
});
} catch (error) {
log.error('Failed to get system info', { error: (error as Error).message });
}
}}
className="glass-button text-sm px-4 py-2 rounded-lg"
>
Test Info
</button>
<button
onClick={() => log.error('Test error message')}
className="glass-button text-sm px-4 py-2 rounded-lg border-red-400/50 text-red-600 hover:bg-red-500/20"
>
Test Error
</button>
<button
onClick={() => sendIPCMessage('test-message', { content: 'Hello from GUI', timestamp: Date.now() })}
className="glass-button text-sm px-4 py-2 rounded-lg border-blue-400/50 text-blue-600 hover:bg-blue-500/20"
>
Send IPC
</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"
>
Clear
</button>
</div>
</div>
<div className="glass-card max-h-96 overflow-y-auto">
{debugMessages.length === 0 ? (
<div className="p-4 text-center text-slate-500 dark:text-slate-400">
No debug messages yet.
</div>
) : (
<div className="divide-y divide-slate-200/50 dark:divide-slate-700/50">
{debugMessages.map((msg, index) => (
<div key={index} className={`p-3 hover:bg-slate-50/50 dark:hover:bg-slate-800/50 ${msg.isIPC ? 'border-l-4 border-blue-400 bg-blue-50/30 dark:bg-blue-900/10' : ''}`}>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className={`text-xs px-2 py-1 rounded-full font-semibold uppercase ${msg.isIPC ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 border border-blue-300' : msg.level === 'error' ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' : msg.level === 'warn' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' : msg.level === 'debug' ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'}`}>
{msg.isIPC ? '📨 IPC' : msg.level}
</span>
<span className="text-xs text-slate-500 dark:text-slate-400">{msg.timestamp}</span>
{msg.isIPC && msg.data?.id && (
<span className="text-xs text-blue-500 dark:text-blue-400 bg-blue-100/50 dark:bg-blue-900/20 px-1 rounded font-mono">
{msg.data.id.split('_').pop()}
</span>
)}
</div>
<div className="text-sm text-slate-700 dark:text-slate-300 mb-1">{msg.message}</div>
{msg.data && (
<div className="text-xs text-slate-500 dark:text-slate-400 bg-slate-100/50 dark:bg-slate-800/50 p-2 rounded font-mono max-h-40 overflow-y-auto">
{safeStringify(msg.data)}
</div>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="mt-6 glass-card p-4">
<div className="flex justify-between items-center mb-3">
<h3 className="text-lg font-semibold accent-text">Send Message to images.ts</h3>
<span className={`text-xs text-slate-500 dark:text-slate-400 bg-slate-100/50 dark:bg-slate-800/50 px-2 py-1 rounded ${ipcInitialized ? 'text-green-600' : 'text-red-600'}`}>
{ipcInitialized ? '🟢 Connected' : '🔴 Disconnected'}
</span>
</div>
<div className="flex gap-3">
<textarea
value={messageToSend}
onChange={(e) => setMessageToSend(e.target.value)}
placeholder="Type a message to send to images.ts process..."
className="flex-1 glass-input p-3 rounded-lg min-h-[80px] resize-none text-sm"
rows={3}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
sendMessageToImages();
}
}}
/>
<div className="flex flex-col gap-2">
<button
onClick={sendMessageToImages}
disabled={!messageToSend.trim()}
className="glass-button px-4 py-2 rounded-lg border-blue-400/50 text-blue-600 hover:bg-blue-500/20 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-semibold"
title="Send message (Ctrl+Enter)"
>
📤 Send
</button>
<button
onClick={() => {
setMessageToSend('Hello from GUI!');
}}
className="glass-button px-4 py-2 rounded-lg border-green-400/50 text-green-600 hover:bg-green-500/20 text-xs"
title="Quick test message"
>
Test
</button>
<button
onClick={() => {
const echoMsg = `Echo: ${Date.now()}`;
setMessageToSend(echoMsg);
setTimeout(() => sendMessageToImages(), 100);
}}
className="glass-button px-4 py-2 rounded-lg border-orange-400/50 text-orange-600 hover:bg-orange-500/20 text-xs"
title="Send echo test"
>
Echo
</button>
</div>
</div>
<div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
💡 Press <kbd className="px-1 py-0.5 bg-slate-200 dark:bg-slate-700 rounded text-xs">Ctrl+Enter</kbd> to send quickly
</div>
</div>
</div>
</div>
);
};
export default DebugPanel;

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { ImageFile } from '../types';
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
interface ImageGalleryProps {
images: ImageFile[];
@ -24,6 +25,8 @@ export default function ImageGallery({
}: ImageGalleryProps) {
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxLoaded, setLightboxLoaded] = useState(false);
const [isPanning, setIsPanning] = useState(false);
const panStartRef = useRef<{ x: number; y: number } | null>(null);
// Reset current index if images change, preventing out-of-bounds errors
useEffect(() => {
@ -267,16 +270,56 @@ export default function ImageGallery({
{lightboxOpen && (
<div
className="fixed inset-0 bg-black/95 z-[9999] flex items-center justify-center"
onClick={() => setLightboxOpen(false)}
onMouseDown={(e) => {
panStartRef.current = { x: e.clientX, y: e.clientY };
setIsPanning(false);
}}
onMouseMove={(e) => {
if (panStartRef.current) {
const distance = Math.sqrt(
Math.pow(e.clientX - panStartRef.current.x, 2) +
Math.pow(e.clientY - panStartRef.current.y, 2)
);
if (distance > 5) { // 5px threshold for pan detection
setIsPanning(true);
}
}
}}
onMouseUp={() => {
panStartRef.current = null;
}}
onClick={(e) => {
// Only close if not panning and clicking on background
if (!isPanning && e.target === e.currentTarget) {
setLightboxOpen(false);
}
setIsPanning(false);
}}
>
<div className="relative w-full h-full flex items-center justify-center">
{lightboxLoaded ? (
<img
src={images[safeIndex].src}
alt={images[safeIndex].path}
className="max-w-full max-h-full object-contain"
onClick={(e) => e.stopPropagation()}
/>
<TransformWrapper
initialScale={1}
minScale={0.1}
maxScale={10}
centerOnInit={true}
wheel={{ step: 0.1 }}
doubleClick={{ disabled: false, step: 0.7 }}
pinch={{ step: 5 }}
>
<TransformComponent
wrapperClass="w-full h-full flex items-center justify-center"
contentClass="max-w-full max-h-full"
>
<img
src={images[safeIndex].src}
alt={images[safeIndex].path}
className="max-w-full max-h-full object-contain cursor-grab active:cursor-grabbing"
onClick={(e) => e.stopPropagation()}
draggable={false}
/>
</TransformComponent>
</TransformWrapper>
) : (
<div className="flex items-center justify-center">
<div className="w-12 h-12 border-4 border-white/30 border-t-white rounded-full animate-spin"></div>
@ -333,7 +376,7 @@ export default function ImageGallery({
{/* 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} ESC to close
{images[safeIndex].path.split(/[/\\]/).pop()} {safeIndex + 1} of {images.length} Scroll to zoom Double-click to reset ESC to close
</div>
)}
</div>

View File

@ -1,3 +1,5 @@
export const ENABLE_ZOOM = true;
export enum TauriCommand {
SUBMIT_PROMPT = 'submit_prompt',
LOG_ERROR = 'log_error_to_console',
@ -56,4 +58,5 @@ export const QUICK_ACTIONS = [
prompt: 'professional product photography style, clean background, studio lighting',
icon: '📦'
}
] as const;
] as const;

View File

@ -1,170 +1,180 @@
import { useEffect } from 'react';
import { tauriApi } from '../lib/tauriApi';
import { initializeApp, completeInitialization, InitCallbacks } from '../lib/init';
import log from '../lib/log';
import { TauriEvent } from '../constants';
import { ImageFile } from '../types';
interface TauriListenersProps extends InitCallbacks {
setFiles: React.Dispatch<React.SetStateAction<ImageFile[]>>;
isGenerating: boolean;
generationTimeoutId: NodeJS.Timeout | null;
setGenerationTimeoutId: (id: NodeJS.Timeout | null) => void;
setIsGenerating: (generating: boolean) => void;
prompt: string;
}
export function useTauriListeners({
setPrompt,
setDst,
setApiKey,
setIpcInitialized,
setPrompts,
setFiles,
isGenerating,
generationTimeoutId,
setGenerationTimeoutId,
setIsGenerating,
prompt
}: TauriListenersProps) {
useEffect(() => {
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 () => {
// Initialize APIs first
const { isTauri: isTauriEnv } = await tauriApi.ensureTauriApi();
if (!isTauriEnv) {
log.warn('Tauri APIs not available, running in browser mode.');
return;
}
// Set up event listeners FIRST, before sending any requests
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,
setDst,
setApiKey,
setPrompts,
setIpcInitialized
};
try {
await completeInitialization(data, initCallbacks);
log.info('✅ App initialization completed');
} catch (error) {
log.error('Failed to complete initialization', {
error: (error as Error).message
});
}
}),
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}`;
const isGeneratedImage = isGenerating || document.querySelector('[src^="data:image/svg+xml"]');
if (isGeneratedImage) {
const generatedImageFile: ImageFile = { path: imageData.filename, src, isGenerated: true, selected: true };
setFiles(prev => {
const withoutPlaceholder = prev.filter(file => !file.path.startsWith('generating_') && file.path !== imageData.filename);
// Deselect all other images and select the new generated one
const updatedFiles = withoutPlaceholder.map(file => ({ ...file, selected: false }));
return [...updatedFiles, generatedImageFile];
});
if (generationTimeoutId) {
clearTimeout(generationTimeoutId);
setGenerationTimeoutId(null);
}
setIsGenerating(false);
log.info('✅ Generated image added to files', { filename: imageData.filename, prompt });
} 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];
}
log.warn(`🔄 Image already exists: ${imageData.filename}`);
return prevFiles;
});
}
} else {
log.error('❌ Invalid image data received', {
hasBase64: !!imageData.base64,
hasMimeType: !!imageData.mimeType,
hasFilename: !!imageData.filename,
});
}
}),
tauriApi.listen(TauriEvent.GENERATION_ERROR, (event: any) => {
const errorData = event.payload;
log.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;
log.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;
log.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;
log.error(`Failed to delete file: ${path}`, { error });
})
]);
[unlistenConfig, unlistenImage, unlistenError, unlistenComplete, unlistenDeleted, unlistenDeleteError] = listeners;
// Send config request after listeners are ready
try {
await tauriApi.requestConfigFromImages();
log.info('📤 Config request sent');
} catch (error) {
log.error('Failed to request config', {
error: (error as Error).message
});
}
};
setupListeners();
return () => {
unlistenConfig?.();
unlistenImage?.();
unlistenError?.();
unlistenComplete?.();
unlistenDeleted?.();
unlistenDeleteError?.();
};
}, []); // Empty dependency array to run only once on mount
}
import { useEffect } from 'react';
import { tauriApi } from '../lib/tauriApi';
import { initializeApp, completeInitialization, InitCallbacks } from '../lib/init';
import log from '../lib/log';
import { TauriEvent } from '../constants';
import { ImageFile } from '../types';
interface TauriListenersProps extends InitCallbacks {
setFiles: React.Dispatch<React.SetStateAction<ImageFile[]>>;
isGenerating: boolean;
generationTimeoutId: NodeJS.Timeout | null;
setGenerationTimeoutId: (id: NodeJS.Timeout | null) => void;
setIsGenerating: (generating: boolean) => void;
prompt: string;
setCurrentIndex: (index: number) => void;
setPromptHistory: (history: string[]) => void;
}
export function useTauriListeners({
setPrompt,
setDst,
setApiKey,
setIpcInitialized,
setPrompts,
setFiles,
isGenerating,
generationTimeoutId,
setGenerationTimeoutId,
setIsGenerating,
prompt,
setCurrentIndex,
setPromptHistory
}: TauriListenersProps) {
useEffect(() => {
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 () => {
// Initialize APIs first
const { isTauri: isTauriEnv } = await tauriApi.ensureTauriApi();
if (!isTauriEnv) {
log.warn('Tauri APIs not available, running in browser mode.');
return;
}
// Set up event listeners FIRST, before sending any requests
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,
setDst,
setApiKey,
setPrompts,
setIpcInitialized,
setPromptHistory
};
try {
await completeInitialization(data, initCallbacks);
log.info('✅ App initialization completed');
} catch (error) {
log.error('Failed to complete initialization', {
error: (error as Error).message
});
}
}),
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}`;
const isGeneratedImage = isGenerating || document.querySelector('[src^="data:image/svg+xml"]');
if (isGeneratedImage) {
const generatedImageFile: ImageFile = { path: imageData.filename, src, isGenerated: true, selected: true };
setFiles(prev => {
const withoutPlaceholder = prev.filter(file => !file.path.startsWith('generating_') && file.path !== imageData.filename);
// Deselect all other images and select the new generated one
const updatedFiles = withoutPlaceholder.map(file => ({ ...file, selected: false }));
const newFiles = [...updatedFiles, generatedImageFile];
// Update currentIndex to point to the new generated image
setCurrentIndex(newFiles.length - 1);
return newFiles;
});
if (generationTimeoutId) {
clearTimeout(generationTimeoutId);
setGenerationTimeoutId(null);
}
setIsGenerating(false);
log.info('✅ Generated image added to files', { filename: imageData.filename, prompt });
} 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];
}
log.warn(`🔄 Image already exists: ${imageData.filename}`);
return prevFiles;
});
}
} else {
log.error('❌ Invalid image data received', {
hasBase64: !!imageData.base64,
hasMimeType: !!imageData.mimeType,
hasFilename: !!imageData.filename,
});
}
}),
tauriApi.listen(TauriEvent.GENERATION_ERROR, (event: any) => {
const errorData = event.payload;
log.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;
log.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;
log.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;
log.error(`Failed to delete file: ${path}`, { error });
})
]);
[unlistenConfig, unlistenImage, unlistenError, unlistenComplete, unlistenDeleted, unlistenDeleteError] = listeners;
// Send config request after listeners are ready
try {
await tauriApi.requestConfigFromImages();
log.info('📤 Config request sent');
} catch (error) {
log.error('Failed to request config', {
error: (error as Error).message
});
}
};
setupListeners();
return () => {
unlistenConfig?.();
unlistenImage?.();
unlistenError?.();
unlistenComplete?.();
unlistenDeleted?.();
unlistenDeleteError?.();
};
}, []); // Empty dependency array to run only once on mount
}

View File

@ -1,305 +1,326 @@
import { tauriApi } from './tauriApi';
import { PromptTemplate } from '../types';
import log from './log';
export interface InitConfig {
prompt?: string;
dst?: string;
apiKey?: string;
files?: string[];
}
export interface InitState {
isInitialized: boolean;
isTauriEnv: boolean;
apiInitialized: boolean;
config: InitConfig | null;
prompts: PromptTemplate[];
error: string | null;
}
export interface InitCallbacks {
setPrompt: (prompt: string) => void;
setDst: (dst: string) => void;
setApiKey: (key: string) => void;
setPrompts: (prompts: PromptTemplate[]) => void;
setIpcInitialized: (initialized: boolean) => void;
}
const STORE_FILE_NAME = '.kbot-gui.json';
/**
* Step 1: Initialize Tauri APIs
*/
export async function initAPI(): Promise<{ isTauri: boolean; apiInitialized: boolean }> {
log.info('🚀 Starting API initialization...');
try {
const result = await tauriApi.ensureTauriApi();
log.info('✅ API initialization complete', {
isTauri: result.isTauri,
apiInitialized: result.apiInitialized,
windowTauri: !!(window as any).__TAURI__
});
return result;
} catch (error) {
log.error('❌ API initialization failed', {
error: (error as Error).message
});
throw error;
}
}
/**
* Step 2: Get configuration from images.ts backend
*/
export async function getConfig(): Promise<void> {
log.info('📡 Requesting config from images.ts...');
try {
await tauriApi.requestConfigFromImages();
log.info('📤 Config request sent to images.ts');
} catch (error) {
log.error('❌ Failed to request config', {
error: (error as Error).message
});
throw error;
}
}
/**
* Step 3: Load prompts store after config is received
*/
export async function loadStore(callbacks: Pick<InitCallbacks, 'setPrompts'>): Promise<PromptTemplate[]> {
const { setPrompts } = callbacks;
log.debug('🔄 Loading prompts from store...');
try {
const { isTauri: isTauriEnv } = await tauriApi.ensureTauriApi();
if (!isTauriEnv) {
log.warn('🌐 Not in Tauri environment, skipping store load');
return [];
}
log.info('📂 Attempting to load prompts from store...');
const configDir = await tauriApi.path.appDataDir();
log.debug(`📁 Data directory: ${configDir}`);
const storePath = await tauriApi.path.join(configDir, STORE_FILE_NAME);
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);
log.debug(`📋 Parsed store data:`, data);
if (data.prompts && Array.isArray(data.prompts)) {
setPrompts(data.prompts);
log.info(`✅ Loaded ${data.prompts.length} prompts from store`);
return data.prompts;
} else {
log.warn('⚠️ Store file exists but has no valid prompts array');
return [];
}
} else {
log.info('📭 Store file is empty');
return [];
}
} catch (error) {
const err = error as Error;
log.info(`📂 Prompt store not found, creating initial store...`, {
error: err.message,
storePath: STORE_FILE_NAME
});
// Create initial empty store
try {
const initialPrompts: PromptTemplate[] = [];
const configDir = await tauriApi.path.appDataDir();
const storePath = await tauriApi.path.join(configDir, STORE_FILE_NAME);
log.debug(`📁 Ensuring directory exists: ${configDir}`);
// Tauri should create the APPDATA directory automatically, but let's verify
log.debug(`📁 APPDATA directory should exist: ${configDir}`);
const initialData = JSON.stringify({ prompts: initialPrompts }, null, 2);
log.debug(`💾 Writing initial store data to: ${storePath}`);
await tauriApi.fs.writeTextFile(storePath, initialData);
log.info(`✅ Initial store created successfully at ${storePath}`);
setPrompts(initialPrompts);
return initialPrompts;
} catch (createError) {
const configDir = await tauriApi.path.appDataDir().catch(() => 'unknown');
log.error('Failed to create initial store - directory may not exist', {
error: (createError as Error).message,
errorName: (createError as Error).name,
storePath: STORE_FILE_NAME,
configDir,
note: 'You may need to manually create the APPDATA directory first'
});
return [];
}
}
}
/**
* Process received config data and update state
*/
export function processConfig(
configData: any,
callbacks: Pick<InitCallbacks, 'setPrompt' | 'setDst' | 'setApiKey' | 'setIpcInitialized'>
): InitConfig {
const { setPrompt, setDst, setApiKey, setIpcInitialized } = callbacks;
const config: InitConfig = {};
if (configData.prompt) {
config.prompt = configData.prompt;
setPrompt(configData.prompt);
}
if (configData.dst) {
config.dst = configData.dst;
setDst(configData.dst);
}
if (configData.apiKey) {
config.apiKey = configData.apiKey;
setApiKey(configData.apiKey);
}
if (configData.files) {
config.files = configData.files;
}
setIpcInitialized(true);
// Mark IPC as ready for logging system
log.setIpcReady(true);
log.info('📨 Config processed successfully', {
hasPrompt: !!config.prompt,
hasDst: !!config.dst,
hasApiKey: !!config.apiKey,
fileCount: config.files?.length || 0,
});
return config;
}
/**
* Complete initialization flow
*/
export async function initializeApp(callbacks: InitCallbacks): Promise<InitState> {
// Initialize logging system first
log.initLogging();
const state: InitState = {
isInitialized: false,
isTauriEnv: false,
apiInitialized: false,
config: null,
prompts: [],
error: null
};
try {
log.info('🎯 Starting complete app initialization...');
// Step 1: Initialize APIs
const apiResult = await initAPI();
state.isTauriEnv = apiResult.isTauri;
state.apiInitialized = apiResult.apiInitialized;
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;
}
} catch (error) {
const err = error as Error;
state.error = err.message;
log.error('❌ App initialization failed', {
error: err.message,
step: 'initialization'
});
}
return state;
}
/**
* Complete the initialization after config is received
*/
export async function completeInitialization(
configData: any,
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);
log.info('✅ App initialization completed successfully', {
configKeys: Object.keys(config),
promptCount: prompts.length
});
return { config, prompts };
} catch (error) {
const err = error as Error;
log.error('❌ Failed to complete initialization', {
error: err.message
});
throw error;
}
}
/**
* Save prompts to store
*/
export async function saveStore(prompts: PromptTemplate[]): Promise<void> {
const { isTauri: isTauriEnv } = await tauriApi.ensureTauriApi();
if (!isTauriEnv) {
log.warn('🌐 Not in Tauri, cannot save prompts');
return;
}
try {
log.debug('💾 Starting save prompts process...');
const dataDir = await tauriApi.path.appDataDir();
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 }, null, 2);
log.debug(`💾 Data to save:`, { promptCount: prompts.length, dataLength: dataToSave.length });
await tauriApi.fs.writeTextFile(storePath, dataToSave);
log.info(`✅ Prompts saved to ${storePath}`);
} catch (error) {
log.error('Failed to save prompts', {
error: (error as Error).message,
errorName: (error as Error).name,
errorStack: (error as Error).stack
});
throw error;
}
}
import { tauriApi } from './tauriApi';
import { PromptTemplate } from '../types';
import log from './log';
export interface InitConfig {
prompt?: string;
dst?: string;
apiKey?: string;
files?: string[];
}
export interface InitState {
isInitialized: boolean;
isTauriEnv: boolean;
apiInitialized: boolean;
config: InitConfig | null;
prompts: PromptTemplate[];
error: string | null;
}
export interface InitCallbacks {
setPrompt: (prompt: string) => void;
setDst: (dst: string) => void;
setApiKey: (key: string) => void;
setPrompts: (prompts: PromptTemplate[]) => void;
setIpcInitialized: (initialized: boolean) => void;
setPromptHistory?: (history: string[]) => void;
}
const STORE_FILE_NAME = '.kbot-gui.json';
/**
* Step 1: Initialize Tauri APIs
*/
export async function initAPI(): Promise<{ isTauri: boolean; apiInitialized: boolean }> {
log.info('🚀 Starting API initialization...');
try {
const result = await tauriApi.ensureTauriApi();
log.info('✅ API initialization complete', {
isTauri: result.isTauri,
apiInitialized: result.apiInitialized,
windowTauri: !!(window as any).__TAURI__
});
return result;
} catch (error) {
log.error('❌ API initialization failed', {
error: (error as Error).message
});
throw error;
}
}
/**
* Step 2: Get configuration from images.ts backend
*/
export async function getConfig(): Promise<void> {
log.info('📡 Requesting config from images.ts...');
try {
await tauriApi.requestConfigFromImages();
log.info('📤 Config request sent to images.ts');
} catch (error) {
log.error('❌ Failed to request config', {
error: (error as Error).message
});
throw error;
}
}
/**
* Step 3: Load prompts store after config is received
*/
export async function loadStore(
callbacks: Pick<InitCallbacks, 'setPrompts'>,
setPromptHistory?: (history: string[]) => void,
setFileHistory?: (fileHistory: string[]) => void
): Promise<PromptTemplate[]> {
const { setPrompts } = callbacks;
log.debug('🔄 Loading prompts from store...');
try {
const { isTauri: isTauriEnv } = await tauriApi.ensureTauriApi();
if (!isTauriEnv) {
log.warn('🌐 Not in Tauri environment, skipping store load');
return [];
}
log.info('📂 Attempting to load prompts from store...');
const configDir = await tauriApi.path.appDataDir();
log.debug(`📁 Data directory: ${configDir}`);
const storePath = await tauriApi.path.join(configDir, STORE_FILE_NAME);
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);
log.debug(`📋 Parsed store data:`, data);
if (data.prompts && Array.isArray(data.prompts)) {
setPrompts(data.prompts);
// Load history if available
if (data.history && Array.isArray(data.history) && setPromptHistory) {
setPromptHistory(data.history);
}
// Load file history if available
if (data.fileHistory && Array.isArray(data.fileHistory) && setFileHistory) {
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;
} else {
log.warn('⚠️ Store file exists but has no valid prompts array');
return [];
}
} else {
log.info('📭 Store file is empty');
return [];
}
} catch (error) {
const err = error as Error;
log.info(`📂 Prompt store not found, creating initial store...`, {
error: err.message,
storePath: STORE_FILE_NAME
});
// Create initial empty store
try {
const initialPrompts: PromptTemplate[] = [];
const configDir = await tauriApi.path.appDataDir();
const storePath = await tauriApi.path.join(configDir, STORE_FILE_NAME);
log.debug(`📁 Ensuring directory exists: ${configDir}`);
// Tauri should create the APPDATA directory automatically, but let's verify
log.debug(`📁 APPDATA directory should exist: ${configDir}`);
const initialData = JSON.stringify({ prompts: initialPrompts }, null, 2);
log.debug(`💾 Writing initial store data to: ${storePath}`);
await tauriApi.fs.writeTextFile(storePath, initialData);
log.info(`✅ Initial store created successfully at ${storePath}`);
setPrompts(initialPrompts);
return initialPrompts;
} catch (createError) {
const configDir = await tauriApi.path.appDataDir().catch(() => 'unknown');
log.error('Failed to create initial store - directory may not exist', {
error: (createError as Error).message,
errorName: (createError as Error).name,
storePath: STORE_FILE_NAME,
configDir,
note: 'You may need to manually create the APPDATA directory first'
});
return [];
}
}
}
/**
* Process received config data and update state
*/
export function processConfig(
configData: any,
callbacks: Pick<InitCallbacks, 'setPrompt' | 'setDst' | 'setApiKey' | 'setIpcInitialized'>
): InitConfig {
const { setPrompt, setDst, setApiKey, setIpcInitialized } = callbacks;
const config: InitConfig = {};
if (configData.prompt) {
config.prompt = configData.prompt;
setPrompt(configData.prompt);
}
if (configData.dst) {
config.dst = configData.dst;
setDst(configData.dst);
}
if (configData.apiKey) {
config.apiKey = configData.apiKey;
setApiKey(configData.apiKey);
}
if (configData.files) {
config.files = configData.files;
}
setIpcInitialized(true);
// Mark IPC as ready for logging system
log.setIpcReady(true);
log.info('📨 Config processed successfully', {
hasPrompt: !!config.prompt,
hasDst: !!config.dst,
hasApiKey: !!config.apiKey,
fileCount: config.files?.length || 0,
});
return config;
}
/**
* Complete initialization flow
*/
export async function initializeApp(callbacks: InitCallbacks): Promise<InitState> {
// Initialize logging system first
log.initLogging();
const state: InitState = {
isInitialized: false,
isTauriEnv: false,
apiInitialized: false,
config: null,
prompts: [],
error: null
};
try {
log.info('🎯 Starting complete app initialization...');
// Step 1: Initialize APIs
const apiResult = await initAPI();
state.isTauriEnv = apiResult.isTauri;
state.apiInitialized = apiResult.apiInitialized;
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;
}
} catch (error) {
const err = error as Error;
state.error = err.message;
log.error('❌ App initialization failed', {
error: err.message,
step: 'initialization'
});
}
return state;
}
/**
* Complete the initialization after config is received
*/
export async function completeInitialization(
configData: any,
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
});
return { config, prompts };
} catch (error) {
const err = error as Error;
log.error('❌ Failed to complete initialization', {
error: err.message
});
throw error;
}
}
/**
* Save prompts, history, and file history to store
*/
export async function saveStore(prompts: PromptTemplate[], history?: string[], fileHistory?: string[]): Promise<void> {
const { isTauri: isTauriEnv } = await tauriApi.ensureTauriApi();
if (!isTauriEnv) {
log.warn('🌐 Not in Tauri, cannot save prompts');
return;
}
try {
log.debug('💾 Starting save prompts process...');
const dataDir = await tauriApi.path.appDataDir();
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 });
await tauriApi.fs.writeTextFile(storePath, dataToSave);
log.info(`✅ Prompts saved to ${storePath}`);
} catch (error) {
log.error('Failed to save prompts', {
error: (error as Error).message,
errorName: (error as Error).name,
errorStack: (error as Error).stack
});
throw error;
}
}