zoom | history
This commit is contained in:
parent
75abc7830f
commit
6eda5f2796
BIN
packages/kbot/cat_gen_1.png
Normal file
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
BIN
packages/kbot/cat_gen_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
BIN
packages/kbot/dist/win-64/tauri-app.exe
vendored
BIN
packages/kbot/dist/win-64/tauri-app.exe
vendored
Binary file not shown.
BIN
packages/kbot/generated_gen_10.png
Normal file
BIN
packages/kbot/generated_gen_10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
17
packages/kbot/gui/tauri-app/package-lock.json
generated
17
packages/kbot/gui/tauri-app/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user