diff --git a/packages/kbot/cat_gen_0.png b/packages/kbot/cat_gen_0.png new file mode 100644 index 00000000..8370f589 Binary files /dev/null and b/packages/kbot/cat_gen_0.png differ diff --git a/packages/kbot/dist/win-64/tauri-app.exe b/packages/kbot/dist/win-64/tauri-app.exe index 882e382a..e4984d04 100644 Binary files a/packages/kbot/dist/win-64/tauri-app.exe and b/packages/kbot/dist/win-64/tauri-app.exe differ diff --git a/packages/kbot/generated_gen_8.png b/packages/kbot/generated_gen_8.png new file mode 100644 index 00000000..3806f2e5 Binary files /dev/null and b/packages/kbot/generated_gen_8.png differ diff --git a/packages/kbot/generated_gen_9.png b/packages/kbot/generated_gen_9.png new file mode 100644 index 00000000..c61e4b8c Binary files /dev/null and b/packages/kbot/generated_gen_9.png differ diff --git a/packages/kbot/gui/tauri-app/package-lock.json b/packages/kbot/gui/tauri-app/package-lock.json index 33ce1c33..cc7143a3 100644 --- a/packages/kbot/gui/tauri-app/package-lock.json +++ b/packages/kbot/gui/tauri-app/package-lock.json @@ -33,7 +33,8 @@ "@tauri-apps/plugin-upload": "^2.3.0", "mime-types": "^2.1.35", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-terminal": "^1.4.5" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.13", @@ -2010,6 +2011,26 @@ } } }, + "node_modules/detect-europe-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", + "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -2151,6 +2172,26 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/is-standalone-pwa": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", + "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, "node_modules/jiti": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", @@ -2623,6 +2664,17 @@ "node": ">=0.10.0" } }, + "node_modules/react-terminal": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/react-terminal/-/react-terminal-1.4.5.tgz", + "integrity": "sha512-uv50LYNb4G/ACr8ghPUuZkypY0UxWm+YBK3M0MLaBG6kALeS9FxNCBhmQw/Uu9ckkE2k5E3kZPUVbR5D6uGOlA==", + "license": "MIT", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "ua-parser-js": "^2.0.0" + } + }, "node_modules/rollup": { "version": "4.50.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", @@ -2763,6 +2815,67 @@ "node": ">=14.17" } }, + "node_modules/ua-is-frozen": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", + "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/ua-parser-js": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.5.tgz", + "integrity": "sha512-sZErtx3rhpvZQanWW5umau4o/snfoLqRcQwQIZ54377WtRzIecnIKvjpkd5JwPcSUMglGnbIgcsQBGAbdi3S9Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "AGPL-3.0-or-later", + "dependencies": { + "detect-europe-js": "^0.1.2", + "is-standalone-pwa": "^0.1.1", + "ua-is-frozen": "^0.1.2", + "undici": "^7.12.0" + }, + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", diff --git a/packages/kbot/gui/tauri-app/package.json b/packages/kbot/gui/tauri-app/package.json index 582f842b..85e11905 100644 --- a/packages/kbot/gui/tauri-app/package.json +++ b/packages/kbot/gui/tauri-app/package.json @@ -36,7 +36,8 @@ "@tauri-apps/plugin-upload": "^2.3.0", "mime-types": "^2.1.35", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-terminal": "^1.4.5" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.13", diff --git a/packages/kbot/gui/tauri-app/src/App.tsx b/packages/kbot/gui/tauri-app/src/App.tsx index 5005ddb9..1bccd262 100644 --- a/packages/kbot/gui/tauri-app/src/App.tsx +++ b/packages/kbot/gui/tauri-app/src/App.tsx @@ -43,39 +43,88 @@ function App() { const [generationTimeoutId, setGenerationTimeoutId] = useState(null); const [currentIndex, setCurrentIndex] = useState(0); const [prompts, setPrompts] = useState([]); + const [promptHistory, setPromptHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); const appendStyle = (style: string) => { setPrompt(prev => { const trimmed = prev.trim(); - if (trimmed) { - return `${trimmed}, ${style}`; + + // Remove any existing styles from QUICK_STYLES + let cleanPrompt = trimmed; + QUICK_STYLES.forEach(existingStyle => { + // Remove style if it exists (with or without comma) + const patterns = [ + new RegExp(`,\\s*${existingStyle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*`, 'gi'), + new RegExp(`^${existingStyle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*,?\\s*`, 'gi'), + new RegExp(`\\s*,\\s*${existingStyle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'gi') + ]; + patterns.forEach(pattern => { + cleanPrompt = cleanPrompt.replace(pattern, ''); + }); + }); + + // Clean up any double commas or trailing/leading commas + cleanPrompt = cleanPrompt.replace(/,\s*,/g, ',').replace(/^,\s*|,\s*$/g, '').trim(); + + // Add the new style + if (cleanPrompt) { + return `${cleanPrompt}, ${style}`; } else { return style; } }); - log.info(`🎨 Style applied: ${style}`); + log.info(`🎨 Style replaced with: ${style}`); }; const executeQuickAction = async (action: { name: string; prompt: string; icon: string }) => { - // Find the last non-generated image to use as input - const inputImages = files.filter(f => !f.isGenerated); - if (inputImages.length === 0) { - log.warn('No input images available for quick action'); + const selectedImages = getSelectedImages(); + + if (selectedImages.length === 0) { + log.warn('Please select an image first to use quick actions'); return; } - const lastImage = inputImages[inputImages.length - 1]; - - // Select the last image - handleImageSelection(lastImage.path, false); + // Use the first selected image + const targetImage = selectedImages[0]; // Set the action prompt setPrompt(action.prompt); // Generate with the selected image - log.info(`🚀 Executing quick action: ${action.name}`); - await generateImage(action.prompt, [lastImage]); + log.info(`🚀 Executing quick action: ${action.name} on selected image ${targetImage.path}`); + await generateImage(action.prompt, [targetImage]); + }; + + const addToHistory = (promptText: string) => { + if (promptText.trim() && !promptHistory.includes(promptText.trim())) { + setPromptHistory(prev => [...prev, promptText.trim()]); + setHistoryIndex(-1); // Reset to end of history + log.info(`📝 Added to history: "${promptText.substring(0, 50)}..."`); + } + }; + + const navigateHistory = (direction: 'up' | 'down') => { + if (promptHistory.length === 0) return; + + let newIndex = historyIndex; + + if (direction === 'up') { + newIndex = historyIndex === -1 ? promptHistory.length - 1 : Math.max(0, historyIndex - 1); + } else { + newIndex = historyIndex === -1 ? -1 : historyIndex + 1; + if (newIndex >= promptHistory.length) newIndex = -1; + } + + setHistoryIndex(newIndex); + + if (newIndex === -1) { + setPrompt(''); + } else { + setPrompt(promptHistory[newIndex]); + log.info(`📜 History: ${newIndex + 1}/${promptHistory.length}`); + } }; const importPrompts = async () => { @@ -436,8 +485,15 @@ function App() { }, []); async function submit() { + if (!prompt.trim()) { + log.warn('Please enter a prompt first'); + return; + } if (apiKey) { + // Add to history before generating + addToHistory(prompt); + // Generate image via backend (always chat mode now) // Only use explicitly selected images. If none are selected, generate from prompt alone. const imagesToUse = getSelectedImages(); @@ -618,6 +674,9 @@ function App() { appendStyle={appendStyle} quickActions={QUICK_ACTIONS} executeQuickAction={executeQuickAction} + promptHistory={promptHistory} + historyIndex={historyIndex} + navigateHistory={navigateHistory} /> {/* Debug Panel */} diff --git a/packages/kbot/gui/tauri-app/src/components/ImageGallery.tsx b/packages/kbot/gui/tauri-app/src/components/ImageGallery.tsx index 5bf1e287..a8332902 100644 --- a/packages/kbot/gui/tauri-app/src/components/ImageGallery.tsx +++ b/packages/kbot/gui/tauri-app/src/components/ImageGallery.tsx @@ -124,52 +124,57 @@ export default function ImageGallery({ const isSelected = currentImage.selected || false; return ( -
- {/* Main Image Display - Compact */} -
-
- {currentImage.path} openLightbox(safeIndex)} - title="Double-click for fullscreen" - /> -
- {/* Compact overlays */} - {isGenerated && isSelected && ( -
- - - - ✓ +
+
+ {/* Left column: Main Image Display */} +
+
+
+ {currentImage.path} openLightbox(safeIndex)} + title="Double-click for fullscreen" + /> +
+ {/* Compact overlays */} + {isGenerated && isSelected && ( +
+ + + + ✓ +
+ )} + + {isGenerated && !isSelected && ( +
+ ✨ +
+ )}
- )} - - {isGenerated && !isSelected && ( -
- ✨ -
- )}
+ + {/* Image Info */} +
+

+ {currentImage.path.split(/[/\\]/).pop()} • {safeIndex + 1}/{images.length} +

+
- {/* Compact Image Info */} -
-

- {currentImage.path.split(/[/\\]/).pop()} • {safeIndex + 1}/{images.length} -

-
- - {/* Thumbnails Grid - Compact */} -
+ {/* Right column: Thumbnails */} +
+

Images ({images.length})

+
{images.map((image, index) => { const thumbIsGenerating = image.path.startsWith('generating_'); const thumbIsGenerated = !!image.isGenerated; @@ -255,6 +260,8 @@ export default function ImageGallery({ ); })}
+
+
{/* Lightbox Modal */} {lightboxOpen && ( diff --git a/packages/kbot/gui/tauri-app/src/components/PromptForm.tsx b/packages/kbot/gui/tauri-app/src/components/PromptForm.tsx index d347592f..c2603b6b 100644 --- a/packages/kbot/gui/tauri-app/src/components/PromptForm.tsx +++ b/packages/kbot/gui/tauri-app/src/components/PromptForm.tsx @@ -35,6 +35,9 @@ interface PromptFormProps { appendStyle: (style: string) => void; quickActions: typeof QUICK_ACTIONS; executeQuickAction: (action: { name: string; prompt: string; icon: string }) => Promise; + promptHistory: string[]; + historyIndex: number; + navigateHistory: (direction: 'up' | 'down') => void; } const PromptForm: React.FC = ({ @@ -67,6 +70,9 @@ const PromptForm: React.FC = ({ appendStyle, quickActions, executeQuickAction, + promptHistory, + historyIndex, + navigateHistory, }) => { const selectedCount = getSelectedImages().length; const { ref: dropZoneRef, dragIn } = useDropZone({ onDrop: addFiles }); @@ -80,132 +86,180 @@ const PromptForm: React.FC = ({ }} >
-
- -