diff --git a/packages/kbot/cat.jpg b/packages/kbot/cat.jpg new file mode 100644 index 00000000..afaaf577 Binary files /dev/null and b/packages/kbot/cat.jpg differ diff --git a/packages/kbot/dist-in/commands/images.js b/packages/kbot/dist-in/commands/images.js index 61d18556..20d49359 100644 --- a/packages/kbot/dist-in/commands/images.js +++ b/packages/kbot/dist-in/commands/images.js @@ -2,6 +2,7 @@ import { z } from 'zod'; import * as path from 'node:path'; import { sync as write } from '@polymech/fs/write'; import { sync as exists } from '@polymech/fs/exists'; +import { readFileSync } from 'node:fs'; import { isArray, isString } from '@polymech/core/primitives'; import { OptionsSchema } from '../zod_schema.js'; import { createImage, editImage } from '../lib/images-google.js'; @@ -59,32 +60,99 @@ export const ImageOptionsSchema = () => { }); }; async function launchGuiAndGetPrompt(argv) { + const logger = getLogger(argv); return new Promise((resolve, reject) => { const guiAppPath = getGuiAppPath(); console.log('guiAppPath', guiAppPath); if (!exists(guiAppPath)) { return reject(new Error(`GUI application not found at: ${guiAppPath}. Please build it first by running 'npm run tauri build' in 'gui/tauri-app'.`)); } + // Prepare CLI arguments const args = []; + // Add include files if (argv.include) { const includes = Array.isArray(argv.include) ? argv.include : [argv.include]; - // Resolve all paths to absolute paths before passing them to the GUI const absoluteIncludes = includes.map(p => path.resolve(p)); args.push(...absoluteIncludes); } - // Pass API key as argument (similar to how we pass include files) + // Add API key const config = loadConfig(argv); const apiKey = argv.api_key || config?.google?.key; if (apiKey) { args.push('--api-key', apiKey); } + // Add dst + if (argv.dst) { + args.push('--dst', argv.dst); + } + // Add prompt + if (argv.prompt) { + args.push('--prompt', argv.prompt); + } const tauriProcess = spawn(guiAppPath, args, { stdio: ['pipe', 'pipe', 'pipe'] }); let output = ''; let errorOutput = ''; tauriProcess.stdout.on('data', (data) => { const chunk = data.toString(); - console.log('GUI stdout chunk:', JSON.stringify(chunk)); - output += chunk; + // Check for config requests from the GUI + const lines = chunk.split('\n').filter(line => line.trim()); + for (const line of lines) { + try { + const message = JSON.parse(line); + if (message.type === 'config_request') { + logger.info('📨 Received config request from GUI'); + // Send config data back via Tauri commands + const config = loadConfig(argv); + const apiKey = argv.api_key || config?.google?.key; + const includes = argv.include ? (Array.isArray(argv.include) ? argv.include : [argv.include]) : []; + const absoluteIncludes = includes.map(p => path.resolve(p)); + // Send config via stdin (Tauri will call forward_config_to_frontend) + const configResponse = { + cmd: 'forward_config_to_frontend', + prompt: argv.prompt || null, + dst: argv.dst || null, + apiKey: apiKey || null, + files: absoluteIncludes + }; + tauriProcess.stdin?.write(JSON.stringify(configResponse) + '\n'); + logger.info('📤 Sent config response to GUI'); + // Send image data + for (const imagePath of absoluteIncludes) { + try { + if (exists(imagePath)) { + const imageBuffer = readFileSync(imagePath); + const base64 = imageBuffer.toString('base64'); + const mimeType = path.extname(imagePath).toLowerCase() === '.png' ? 'image/png' : 'image/jpeg'; + const filename = path.basename(imagePath); + // Verify base64 encoding + logger.info(`📸 Image encoding check: ${filename}`, { + bufferSize: imageBuffer.length, + base64Size: base64.length, + base64Sample: base64.substring(0, 50), + isValidBase64: /^[A-Za-z0-9+/]*={0,2}$/.test(base64) + }); + const imageResponse = { + cmd: 'forward_image_to_frontend', + base64, + mimeType, + filename + }; + tauriProcess.stdin?.write(JSON.stringify(imageResponse) + '\n'); + logger.info(`📤 Sent image data: ${filename} (${Math.round(base64.length / 1024)}KB)`); + } + } + catch (error) { + logger.error(`Failed to send image: ${imagePath}`, error.message); + } + } + } + } + catch (e) { + // Not a JSON message, add to regular output + console.log('GUI stdout chunk:', JSON.stringify(line)); + output += line + '\n'; + } + } }); tauriProcess.stderr.on('data', (data) => { const chunk = data.toString(); @@ -185,4 +253,4 @@ export const imageCommand = async (argv) => { logger.error('Failed to parse options or generate image:', error.message, error.issues, error.stack); } }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW1hZ2VzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NvbW1hbmRzL2ltYWdlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sS0FBSyxDQUFDO0FBQ3hCLE9BQU8sS0FBSyxJQUFJLE1BQU0sV0FBVyxDQUFDO0FBQ2xDLE9BQU8sRUFBRSxJQUFJLElBQUksS0FBSyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDbkQsT0FBTyxFQUFFLElBQUksSUFBSSxNQUFNLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUVyRCxPQUFPLEVBQUUsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRTlELE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUNqRCxPQUFPLEVBQUUsV0FBVyxFQUFFLFNBQVMsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ2pFLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDeEMsT0FBTyxFQUFFLE1BQU0sSUFBSSxhQUFhLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDdkQsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUM1QyxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDM0MsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUUxQyxTQUFTLGFBQWE7SUFFbEIsc0VBQXNFO0lBQ3RFLE1BQU0sU0FBUyxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxHQUFHLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUNsRSxvRkFBb0Y7SUFDcEYsTUFBTSxjQUFjLEdBQUcsT0FBTyxDQUFDLFFBQVEsS0FBSyxPQUFP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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW1hZ2VzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NvbW1hbmRzL2ltYWdlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sS0FBSyxDQUFDO0FBQ3hCLE9BQU8sS0FBSyxJQUFJLE1BQU0sV0FBVyxDQUFDO0FBQ2xDLE9BQU8sRUFBRSxJQUFJLElBQUksS0FBSyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDbkQsT0FBTyxFQUFFLElBQUksSUFBSSxNQUFNLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUNyRCxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBRXZDLE9BQU8sRUFBRSxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFFOUQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2pELE9BQU8sRUFBRSxXQUFXLEVBQUUsU0FBUyxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDakUsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUN4QyxPQUFPLEVBQUUsTUFBTSxJQUFJLGFBQWEsRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUN2RCxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDNUMsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUMzQyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sY0FBYyxDQUFDO0FBRzFDLFNBQVMsYUFBYTtJQUVsQixzRUFBc0U7SUFDdEUsTUFBTSxTQUFTLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLEdBQUcsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ2xFLG9GQUFvRjtJQUNw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 \ No newline at end of file diff --git a/packages/kbot/dist-in/lib/ipc.d.ts b/packages/kbot/dist-in/lib/ipc.d.ts new file mode 100644 index 00000000..66efe8df --- /dev/null +++ b/packages/kbot/dist-in/lib/ipc.d.ts @@ -0,0 +1,57 @@ +export interface IPCMessage { + type: 'counter' | 'debug' | 'image' | 'prompt_submit' | 'error' | 'init_data' | 'gui_message'; + data: any; + timestamp?: number; + id?: string; +} +export interface ImagePayload { + base64: string; + mimeType: string; + filename?: string; +} +export interface PromptSubmitPayload { + prompt: string; + files: string[]; + dst: string; +} +export interface CounterPayload { + count: number; + message?: string; +} +export interface DebugPayload { + level: 'info' | 'warn' | 'error' | 'debug'; + message: string; + data?: any; +} +export interface InitDataPayload { + prompt?: string; + dst?: string; + apiKey?: string; + files?: string[]; +} +export interface GuiMessagePayload { + message: string; + timestamp: number; + source: string; +} +export declare class IPCClient { + private guiAppPath; + private process; + private messageHandlers; + private counter; + private isReady; + constructor(guiAppPath: string); + launch(args?: string[]): Promise; + private handleMessage; + onMessage(type: string, handler: (message: IPCMessage) => void): void; + sendMessage(message: IPCMessage): void; + sendDebugMessage(level: DebugPayload['level'], message: string, data?: any): void; + sendCounterMessage(count?: number, message?: string): void; + sendImageMessage(base64: string, mimeType: string, filename?: string): void; + sendInitData(prompt?: string, dst?: string, apiKey?: string, files?: string[]): void; + sendIPCViaTauri(messageType: string, data: any): Promise; + waitForPromptSubmit(): Promise; + close(): void; +} +export declare function getGuiAppPath(): string; +export declare function createIPCClient(): IPCClient; diff --git a/packages/kbot/dist-in/lib/ipc.js b/packages/kbot/dist-in/lib/ipc.js new file mode 100644 index 00000000..a4c1cf63 --- /dev/null +++ b/packages/kbot/dist-in/lib/ipc.js @@ -0,0 +1,238 @@ +import { spawn } from 'node:child_process'; +import * as path from 'node:path'; +import { sync as exists } from '@polymech/fs/exists'; +export class IPCClient { + guiAppPath; + process = null; + messageHandlers = new Map(); + counter = 0; + isReady = false; + constructor(guiAppPath) { + this.guiAppPath = guiAppPath; + } + async launch(args = []) { + return new Promise((resolve, reject) => { + if (!exists(this.guiAppPath)) { + return reject(new Error(`GUI application not found at: ${this.guiAppPath}`)); + } + this.process = spawn(this.guiAppPath, args, { + stdio: ['pipe', 'pipe', 'pipe'] + }); + let output = ''; + let errorOutput = ''; + this.process.stdout?.on('data', (data) => { + const chunk = data.toString(); + // Try to parse each line as a potential IPC message first + const lines = chunk.split('\n').filter(line => line.trim()); + let hasIPCMessage = false; + for (const line of lines) { + try { + const parsed = JSON.parse(line); + // Check if it's a structured IPC message + if (parsed.type && parsed.data !== undefined) { + this.handleMessage(parsed); + hasIPCMessage = true; + } + // Check if it's a raw GUI message (from console.log in browser mode) + else if (parsed.message && parsed.source === 'gui') { + const ipcMessage = { + type: 'gui_message', + data: parsed, + timestamp: parsed.timestamp || Date.now(), + id: `gui_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + }; + this.handleMessage(ipcMessage); + hasIPCMessage = true; + } + } + catch (e) { + // Not a JSON message, continue + } + } + // Only log non-IPC stdout (to avoid binary data spam) + if (!hasIPCMessage && chunk.trim() && !chunk.includes('"base64"')) { + console.log('[IPC] GUI stdout:', chunk); + } + // Also check for GUI messages in stdout + if (!hasIPCMessage && chunk.trim()) { + const lines = chunk.split('\n').filter(line => line.trim()); + for (const line of lines) { + try { + const possibleMessage = JSON.parse(line); + if (possibleMessage.type === 'gui_message') { + this.handleMessage(possibleMessage); + } + } + catch (e) { + // Not a JSON message, ignore + } + } + } + output += chunk; + }); + this.process.stderr?.on('data', (data) => { + const chunk = data.toString(); + console.log('[IPC] GUI stderr:', chunk); + errorOutput += chunk; + }); + this.process.on('close', (code) => { + console.log('[IPC] GUI process closed with code:', code); + if (code === 0) { + const trimmedOutput = output.trim(); + resolve(); + } + else { + reject(new Error(`Tauri app exited with code ${code}. stderr: ${errorOutput}`)); + } + }); + this.process.on('error', (err) => { + reject(err); + }); + // Give the process a moment to start + setTimeout(() => resolve(), 1000); + }); + } + handleMessage(message) { + // Create a safe version for logging (without binary data) + const safeMessage = { ...message }; + if (safeMessage.type === 'image' && safeMessage.data && typeof safeMessage.data === 'object' && 'base64' in safeMessage.data) { + safeMessage.data = { + ...safeMessage.data, + base64: `[BASE64 DATA - ${safeMessage.data.base64.length} chars]` + }; + } + console.log('[IPC] Received message:', safeMessage); + const handler = this.messageHandlers.get(message.type); + if (handler) { + handler(message); + } + else { + console.log('[IPC] No handler for message type:', message.type); + } + } + onMessage(type, handler) { + this.messageHandlers.set(type, handler); + } + sendMessage(message) { + if (!this.process || !this.process.stdin) { + console.error('[IPC] Cannot send message: process not available'); + return; + } + const messageWithMeta = { + ...message, + timestamp: Date.now(), + id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + }; + const jsonMessage = JSON.stringify(messageWithMeta) + '\n'; + // Create a safe version for logging (without binary data) + const safeMessage = { ...messageWithMeta }; + if (safeMessage.type === 'image' && safeMessage.data && typeof safeMessage.data === 'object' && 'base64' in safeMessage.data) { + safeMessage.data = { + ...safeMessage.data, + base64: `[BASE64 DATA - ${safeMessage.data.base64.length} chars]` + }; + } + console.log('[IPC] Sending message:', JSON.stringify(safeMessage)); + this.process.stdin.write(jsonMessage); + } + sendDebugMessage(level, message, data) { + this.sendMessage({ + type: 'debug', + data: { level, message, data } + }); + } + sendCounterMessage(count, message) { + if (count === undefined) { + this.counter++; + count = this.counter; + } + this.sendMessage({ + type: 'counter', + data: { count, message } + }); + } + sendImageMessage(base64, mimeType, filename) { + this.sendMessage({ + type: 'image', + data: { base64, mimeType, filename } + }); + } + sendInitData(prompt, dst, apiKey, files) { + this.sendMessage({ + type: 'init_data', + data: { prompt, dst, apiKey, files } + }); + } + // Send IPC message via Tauri command (when GUI is ready) + async sendIPCViaTauri(messageType, data) { + if (!this.process) { + console.error('[IPC] Cannot send via Tauri: process not available'); + return; + } + // Send a special command to tell the GUI to forward this as an event + const command = { + type: 'tauri_command', + command: 'forward_ipc_message', + args: { messageType, data } + }; + const jsonMessage = JSON.stringify(command) + '\n'; + console.log('[IPC] Sending Tauri command:', JSON.stringify({ ...command, args: { messageType, data: messageType === 'image' ? '[IMAGE DATA]' : data } })); + this.process.stdin?.write(jsonMessage); + } + async waitForPromptSubmit() { + return new Promise((resolve) => { + this.onMessage('prompt_submit', (message) => { + resolve(message.data); + }); + // Also handle the legacy format for backwards compatibility + this.process?.on('close', (code) => { + if (code === 0) { + // Try to parse the final output as legacy format + // This will be handled by the existing logic in images.ts + resolve(null); + } + }); + }); + } + close() { + if (this.process) { + this.process.kill(); + this.process = null; + } + } +} +export function getGuiAppPath() { + // Get the directory of this script file, then navigate to the GUI app + const scriptDir = path.dirname(new URL(import.meta.url).pathname); + // On Windows, URL.pathname can have an extra leading slash, so we need to handle it + const cleanScriptDir = process.platform === 'win32' && scriptDir.startsWith('/') + ? scriptDir.substring(1) + : scriptDir; + const packageRoot = path.resolve(cleanScriptDir, '..', '..'); + // Determine platform-specific subdirectory and executable name + let platformDir; + let executableName; + switch (process.platform) { + case 'win32': + platformDir = 'win-64'; + executableName = 'tauri-app.exe'; + break; + case 'darwin': + platformDir = 'osx-64'; + executableName = 'tauri-app'; + break; + case 'linux': + platformDir = 'linux-64'; + executableName = 'tauri-app'; + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + return path.join(packageRoot, 'dist', platformDir, executableName); +} +// Utility function to create and configure an IPC client +export function createIPCClient() { + const guiAppPath = getGuiAppPath(); + return new IPCClient(guiAppPath); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaXBjLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2xpYi9pcGMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLEtBQUssRUFBZ0IsTUFBTSxvQkFBb0IsQ0FBQztBQUN6RCxPQUFPLEtBQUssSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUNsQyxPQUFPLEVBQUUsSUFBSSxJQUFJLE1BQU0sRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBNkNyRCxNQUFNLE9BQU8sU0FBUztJQU1FO0lBTFosT0FBTyxHQUF3QixJQUFJLENBQUM7SUFDcEMsZUFBZSxHQUErQyxJQUFJLEdBQUcsRUFBRSxDQUFDO0lBQ3hFLE9BQU8sR0FBRyxDQUFDLENBQUM7SUFDWixPQUFPLEdBQUcsS0FBSyxDQUFDO0lBRXhCLFlBQW9CLFVBQWtCO1FBQWxCLGVBQVUsR0FBVixVQUFVLENBQVE7SUFBRyxDQUFDO0lBRTFDLEtBQUssQ0FBQyxNQUFNLENBQUMsT0FBaUIsRUFBRTtRQUM1QixPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxFQUFFO1lBQ25DLElBQUksQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUM7Z0JBQzNCLE9BQU8sTUFBTSxDQUFDLElBQUksS0FBSyxDQUFDLGlDQUFpQyxJQUFJLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQyxDQUFDO1lBQ2pGLENBQUM7WUFFRCxJQUFJLENBQUMsT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLElBQUksRUFBRTtnQkFDeEMsS0FBSyxFQUFFLENBQUMsTUFBTSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUM7YUFDbEMsQ0FBQyxDQUFDO1lBRUgsSUFBSSxNQUFNLEdBQUcsRUFBRSxDQUFDO1lBQ2hCLElBQUksV0FBVyxHQUFHLEVBQUUsQ0FBQztZQUVyQixJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxF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 \ No newline at end of file diff --git a/packages/kbot/dist/win-64/tauri-app.exe b/packages/kbot/dist/win-64/tauri-app.exe index 72ab6cc2..e3d20340 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/docs/ipc.md b/packages/kbot/docs/ipc.md new file mode 100644 index 00000000..bb693df3 --- /dev/null +++ b/packages/kbot/docs/ipc.md @@ -0,0 +1,235 @@ +# IPC Communication Documentation + +## Overview + +This document describes the Inter-Process Communication (IPC) system between the `images.ts` command and the Tauri GUI application. + +## Current Architecture + +### Components + +1. **images.ts** - Node.js CLI command process +2. **tauri-app.exe** - Tauri desktop application (Rust + Web frontend) +3. **IPC Client** - Node.js library for managing communication +4. **Tauri Commands** - Rust functions exposed to frontend +5. **React Frontend** - TypeScript/React UI + +## Communication Flows + +### 1. Initial Configuration Passing + +```mermaid +sequenceDiagram + participant CLI as images.ts CLI + participant IPC as IPC Client + participant Tauri as tauri-app.exe + participant Frontend as React Frontend + participant Rust as Tauri Rust Backend + + CLI->>IPC: createIPCClient() + CLI->>IPC: launch([]) + IPC->>Tauri: spawn tauri-app.exe + + Note over CLI,Rust: Initial data sending + CLI->>IPC: sendInitData(prompt, dst, apiKey, files) + IPC->>Tauri: stdout: {"type":"init_data","data":{...}} + Tauri->>Frontend: IPC message handling + Frontend->>Frontend: setPrompt(), setDst(), setApiKey() + + Note over CLI,Rust: Image data sending + CLI->>IPC: sendImageMessage(base64, mimeType, filename) + IPC->>Tauri: stdout: {"type":"image","data":{...}} + Tauri->>Frontend: IPC message handling + Frontend->>Frontend: addFiles([{path, src}]) +``` + +### 2. GUI to CLI Messaging (Current Implementation) + +```mermaid +sequenceDiagram + participant Frontend as React Frontend + participant Rust as Tauri Rust Backend + participant Tauri as tauri-app.exe + participant IPC as IPC Client + participant CLI as images.ts CLI + + Note over Frontend,CLI: User sends message from GUI + Frontend->>Frontend: sendMessageToImages() + Frontend->>Rust: safeInvoke('send_message_to_stdout', message) + Rust->>Rust: send_message_to_stdout command + Rust->>Tauri: println!(message) to stdout + Tauri->>IPC: stdout data received + IPC->>IPC: parse JSON from stdout + IPC->>CLI: handleMessage() callback + CLI->>CLI: gui_message handler + + Note over Frontend,CLI: Echo response + CLI->>IPC: sendDebugMessage('Echo: ...') + IPC->>Tauri: stdout: {"type":"debug","data":{...}} + Tauri->>Frontend: IPC message handling + Frontend->>Frontend: addDebugMessage() +``` + +### 3. Console Message Forwarding + +```mermaid +sequenceDiagram + participant Frontend as React Frontend + participant Console as Console Hijack + participant Rust as Tauri Rust Backend + participant CLI as images.ts CLI + + Note over Frontend,CLI: Console messages forwarding + Frontend->>Console: console.log/error/warn() + Console->>Console: hijacked in main.tsx + Console->>Rust: safeInvoke('log_error_to_console') + Rust->>Rust: log_error_to_console command + Rust->>CLI: eprintln! to stderr + CLI->>CLI: stderr logging +``` + +## Current Issues & Complexity + +### Problem 1: Multiple Communication Channels + +We have **3 different communication paths**: + +1. **IPC Messages** (structured): `{"type": "init_data", "data": {...}}` +2. **Raw GUI Messages** (via Tauri command): `{"message": "hello", "source": "gui"}` +3. **Console Forwarding** (via hijacking): All console.* calls + +### Problem 2: Inconsistent Message Formats + +- **From CLI to GUI**: Structured IPC messages +- **From GUI to CLI**: Raw JSON via stdout +- **Console logs**: String messages via stderr + +### Problem 3: Complex Parsing Logic + +The IPC client has to handle multiple message formats: + +```typescript +// Structured IPC message +if (parsed.type && parsed.data !== undefined) { + this.handleMessage(parsed as IPCMessage); +} +// Raw GUI message +else if (parsed.message && parsed.source === 'gui') { + const ipcMessage: IPCMessage = { + type: 'gui_message', + data: parsed, + // ... + }; + this.handleMessage(ipcMessage); +} +``` + +## Recommended Simplification + +### Option 1: Unified IPC Messages + +**All communication should use the same format:** + +```typescript +interface IPCMessage { + type: 'init_data' | 'gui_message' | 'debug' | 'image' | 'prompt_submit'; + data: any; + timestamp: number; + id: string; +} +``` + +**Sequence:** +```mermaid +sequenceDiagram + participant Frontend as React Frontend + participant Rust as Tauri Rust Backend + participant CLI as images.ts CLI + + Note over Frontend,CLI: Unified messaging + Frontend->>Rust: safeInvoke('send_ipc_message', {type, data}) + Rust->>CLI: stdout: {"type":"gui_message","data":{...},"timestamp":...} + CLI->>Rust: stdout: {"type":"debug","data":{...},"timestamp":...} + Rust->>Frontend: handleMessage(message) +``` + +### Option 2: Direct Tauri IPC (Recommended) + +**Use Tauri's built-in event system:** + +```mermaid +sequenceDiagram + participant Frontend as React Frontend + participant Rust as Tauri Rust Backend + participant CLI as images.ts CLI + + Note over Frontend,CLI: Tauri events + Frontend->>Rust: emit('gui-message', data) + Rust->>CLI: HTTP/WebSocket/Named Pipe + CLI->>Rust: HTTP/WebSocket/Named Pipe response + Rust->>Frontend: emit('cli-response', data) +``` + +## Current File Structure + +``` +src/ +├── lib/ipc.ts # IPC Client (Node.js side) +├── commands/images.ts # CLI command with IPC integration +gui/tauri-app/ +├── src/App.tsx # React frontend with IPC handling +├── src/main.tsx # Console hijacking setup +└── src-tauri/src/lib.rs # Tauri commands and state management +``` + +## Configuration Passing Methods + +### Method 1: CLI Arguments (Original) +```bash +tauri-app.exe --api-key "key" --dst "output.png" --prompt "text" file1.png file2.png +``` + +### Method 2: IPC Messages (Current) +```typescript +ipcClient.sendInitData(prompt, dst, apiKey, files); +``` + +### Method 3: Environment Variables +```bash +export API_KEY="key" +export DST="output.png" +tauri-app.exe +``` + +### Method 4: Temporary Config File +```typescript +// Write config.json +fs.writeFileSync('/tmp/config.json', JSON.stringify({prompt, dst, apiKey})); +// Launch app +spawn('tauri-app.exe', ['--config', '/tmp/config.json']); +``` + +## Recommendations + +1. **Simplify to single communication method** - Either all CLI args OR all IPC messages +2. **Remove console hijacking** - Use proper logging/debug channels +3. **Use consistent message format** - Same structure for all message types +4. **Consider Tauri's built-in IPC** - Events, commands, or invoke system +5. **Separate concerns** - Config passing vs. runtime messaging + +## Questions for Review + +1. Do we need bidirectional messaging during runtime, or just initial config passing? +2. Should console messages be forwarded, or use proper debug channels? +3. Is the complexity worth it, or should we use simpler CLI args + file output? +4. Could we use Tauri's built-in event system instead of stdout parsing? + +## Current Status + +- ✅ Config passing works (init_data messages) +- ✅ Image passing works (base64 via IPC) +- ✅ GUI → CLI messaging works (via Tauri command) +- ✅ CLI → GUI messaging works (debug messages) +- ❌ System is overly complex with multiple communication paths +- ❌ Inconsistent message formats +- ❌ Console hijacking adds unnecessary complexity diff --git a/packages/kbot/gui/tauri-app/ref/ProfileCard.tsx b/packages/kbot/gui/tauri-app/ref/ProfileCard.tsx new file mode 100644 index 00000000..a1b0b686 --- /dev/null +++ b/packages/kbot/gui/tauri-app/ref/ProfileCard.tsx @@ -0,0 +1,379 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Profile, PlotStatus, TemperatureProfileCommand } from '@/types'; +import BezierEditor from '@/components/profiles/bezier/BezierEditor'; +import { Edit, Trash2, Play, Pause, StopCircle, Copy, CopyPlus } from 'lucide-react'; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { T, translate } from '../../i18n'; +import { useModbus } from '../../contexts/ModbusContext'; +import { getSlaveIdFromGroup, findCoilForProfile } from '../../lib/controllerUtils'; +import { PV_REGISTER_NAME_SUFFIX, PROFILE_REGISTER_NAMES } from '../../constants'; +import { useNavigate } from 'react-router-dom'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +interface ProfileCardProps { + profile: Profile; + onDelete: (id: string) => void; + onCommand: (profileSlot: number, command: TemperatureProfileCommand) => void; + zones?: { id: string, name: string }[]; + onApplyToZone?: (profileId: string, zoneId: string) => void; + onDuplicate: (profileToDuplicate: Profile) => void; + onCopyTo: (profileToCopy: Profile) => void; + canDuplicate?: boolean; +} + +const formatDuration = (ms: number): string => { + if (isNaN(ms) || ms < 0) return '00h 00min 00s'; + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + // For durations less than 1 hour, show minutes and seconds + if (hours === 0) { + const paddedMinutes = minutes.toString().padStart(2, '0'); + const paddedSeconds = seconds.toString().padStart(2, '0'); + return `${paddedMinutes}min ${paddedSeconds}s`; + } + + // For durations 1+ hours, show hours and minutes + const paddedHours = hours.toString().padStart(2, '0'); + const paddedMinutes = minutes.toString().padStart(2, '0'); + return `${paddedHours}h ${paddedMinutes}min`; +}; + +// Helper function to get status text +const getStatusText = (status: PlotStatus): string => { + switch (status) { + case PlotStatus.IDLE: + return 'Idle'; + case PlotStatus.RUNNING: + return 'Running'; + case PlotStatus.INITIALIZING: + return 'Warmup'; + case PlotStatus.PAUSED: + return 'Paused'; + case PlotStatus.FINISHED: + return 'Finished'; + case PlotStatus.STOPPED: + return 'Stopped'; + default: + return 'Unknown'; + } +}; + +const ProfileCard: React.FC = ({ + profile, + onDelete, + onCommand, + zones, + onApplyToZone, + onDuplicate, + onCopyTo, + canDuplicate +}) => { + const navigate = useNavigate(); + const profileId = String(profile.slot); + const [plainTextDescription, setPlainTextDescription] = useState(''); + const { registers, settings, coils, updateCoil } = useModbus(); + const [isToggling, setIsToggling] = useState(false); + + const enableCoil = useMemo(() => { + if (!coils || !profile.name) return null; + return findCoilForProfile( + coils, + profile.name, + profile.slot, + PROFILE_REGISTER_NAMES.ENABLED + ); + }, [coils, profile.name, profile.slot]); + + const isEnabled = enableCoil ? enableCoil.value : false; + // console.log("isEnabled",profile.enabled,enableCoil); + + const handleToggle = async (newState: boolean) => { + if (!enableCoil) return; + setIsToggling(true); + try { + await updateCoil(enableCoil.address, newState); + } catch (error) { + console.error(`Failed to toggle profile ${profile.slot}`, error); + } finally { + setIsToggling(false); + } + }; + + // Create a name-to-slaveid map from the partition config + const controllerNameToSlaveIdMap = React.useMemo(() => { + const map = new Map(); + if (!settings) return map; + settings.partitions.forEach(partition => { + partition.controllers?.forEach(controller => { + if (controller.name) { + map.set(controller.name, controller.slaveid); + } + }); + }); + return map; + }, [settings]); + + const getControllerPv = (controllerName: string): number | string => { + const slaveid = controllerNameToSlaveIdMap.get(controllerName); + + if (slaveid === undefined) { + // Fallback or error for controllers not in the static config + // This might happen if profiles are associated with controllers not in PARTITION_CONFIG + const fallbackSlaveId = getSlaveIdFromGroup(controllerName); + if(fallbackSlaveId) { + // You can decide if you want to support this fallback. + // For now, let's just log a warning and return N/A if not in the map. + } + console.warn(`Could not determine slaveid for controller from settings: ${controllerName}`,controllerNameToSlaveIdMap); + return 'N/A'; + } + + const pvRegister = registers.find( + reg => getSlaveIdFromGroup(reg.group) === slaveid && reg.name.endsWith(PV_REGISTER_NAME_SUFFIX) + ); + + if (pvRegister && typeof pvRegister.value === 'number') { + return pvRegister.value.toFixed(1); + } else { + // It's possible for the register to exist but the value not be a number yet. + // Or for the register not to be found immediately. + console.error(`PV register not found for controller ${controllerName} (slaveid: ${slaveid})`); + } + + return 'N/A'; + }; + + useEffect(() => { + const getPlainTextFromMarkdown = async (markdown: string = '') => { + return markdown; + }; + + if (profile.description) { + getPlainTextFromMarkdown(profile.description); + } else { + setPlainTextDescription(''); + } + }, [profile.description]); + + return ( + + +
+ + {profile.name} + +
+
+ + +
+
+
+
+
+ {formatDuration(profile.duration)} Total +
+
+ {getStatusText(profile.status)} +
+
+ {profile.max}°C Max +
+ {(profile.status === PlotStatus.RUNNING || profile.status === PlotStatus.PAUSED || profile.status === PlotStatus.INITIALIZING) && ( + <> + {profile.currentTemp !== undefined && ( +
+ {profile.currentTemp}°C Now +
+ )} + {profile.elapsed !== undefined && ( +
+ {formatDuration(profile.elapsed)} Elapsed +
+ )} + {profile.remaining !== undefined && ( +
+ {formatDuration(profile.remaining)} Remaining +
+ )} + + + )} +
+
+
+ + {plainTextDescription && ( +

+ {plainTextDescription} +

+ )} + + {profile.associatedControllerNames && profile.associatedControllerNames.length > 0 && ( +
+

{translate("Associated Controllers:")}

+
+ {profile.associatedControllerNames.map((name, index) => { + const slaveid = controllerNameToSlaveIdMap.get(name); + return ( +
+ {name}{slaveid !== undefined ? ` (${slaveid})` : ''}: + {getControllerPv(name)}°C +
+ ); + })} +
+
+ )} + +
+ {}} + max={profile.max} + duration={profile.duration} + readonly + showGridLabels={false} + className="h-40 w-full" + elapsedTime={profile.elapsed} + isRunning={profile.status === PlotStatus.RUNNING || profile.status === PlotStatus.PAUSED || profile.status === PlotStatus.INITIALIZING} + currentTemp={profile.currentTemp} + /> +
+ + {zones && zones.length > 0 && onApplyToZone && ( +
+

Apply to zone:

+ +
+ )} +
+ + + + {(profile.status === PlotStatus.IDLE || profile.status === PlotStatus.FINISHED || profile.status === PlotStatus.STOPPED) && ( + + )} + {(profile.status === PlotStatus.RUNNING || profile.status === PlotStatus.INITIALIZING) && ( + + )} + {profile.status === PlotStatus.PAUSED && ( + + )} + + {(profile.status === PlotStatus.RUNNING || profile.status === PlotStatus.PAUSED || profile.status === PlotStatus.INITIALIZING) && ( + + )} + + + + + + + +
+ ); +}; + +export default ProfileCard; diff --git a/packages/kbot/gui/tauri-app/src-tauri/Cargo.lock b/packages/kbot/gui/tauri-app/src-tauri/Cargo.lock index ab406ae9..ad02bcba 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/Cargo.lock +++ b/packages/kbot/gui/tauri-app/src-tauri/Cargo.lock @@ -4046,6 +4046,7 @@ version = "0.1.0" dependencies = [ "dirs 5.0.1", "pathdiff", + "rand 0.8.5", "serde", "serde_json", "tauri", diff --git a/packages/kbot/gui/tauri-app/src-tauri/Cargo.toml b/packages/kbot/gui/tauri-app/src-tauri/Cargo.toml index 190ebb72..74ff5e8f 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/Cargo.toml +++ b/packages/kbot/gui/tauri-app/src-tauri/Cargo.toml @@ -27,4 +27,5 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" pathdiff = "0.2.3" dirs = "5.0.1" +rand = "0.8" diff --git a/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs b/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs index fffd0375..f08a5aad 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs +++ b/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs @@ -1,9 +1,8 @@ -use tauri::Manager; +use tauri::{Manager, Emitter}; use serde::{Serialize, Deserialize}; -use dirs; -struct CliArgs(std::sync::Mutex>); -struct ApiKey(std::sync::Mutex>); +struct Counter(std::sync::Mutex); +struct DebugMessages(std::sync::Mutex>); #[derive(Serialize, Deserialize)] struct Payload { @@ -12,6 +11,36 @@ struct Payload { dst: String, } +#[derive(Serialize, Deserialize)] +struct IPCMessage { + #[serde(rename = "type")] + message_type: String, + data: serde_json::Value, + timestamp: Option, + id: Option, +} + +#[derive(Serialize, Deserialize)] +struct CounterPayload { + count: u32, + message: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +struct DebugPayload { + level: String, + message: String, + data: Option, +} + +#[derive(Serialize, Deserialize)] +struct ImagePayload { + base64: String, + #[serde(rename = "mimeType")] + mime_type: String, + filename: Option, +} + // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] fn submit_prompt(prompt: &str, files: Vec, dst: &str, window: tauri::Window) { @@ -33,79 +62,321 @@ fn submit_prompt(prompt: &str, files: Vec, dst: &str, window: tauri::Win let _ = window.app_handle().exit(0); } -#[tauri::command] -fn get_cli_args(state: tauri::State<'_, CliArgs>) -> Result, String> { - eprintln!("[RUST LOG]: get_cli_args command called."); - let args = state.0.lock().unwrap().clone(); - eprintln!("[RUST LOG]: - Returning args: {:?}", args); - Ok(args) -} - -#[tauri::command] -fn resolve_path_relative_to_home(absolute_path: String) -> Result { - eprintln!("[RUST LOG]: resolve_path_relative_to_home command called."); - eprintln!("[RUST LOG]: - Received absolute path: {}", absolute_path); - - let home_dir = dirs::home_dir().ok_or_else(|| "Could not find home directory".to_string())?; - - let path_to_resolve = std::path::Path::new(&absolute_path); - - let relative_path = pathdiff::diff_paths(path_to_resolve, home_dir) - .ok_or_else(|| "Failed to calculate relative path from home directory".to_string())?; - - let result = relative_path.to_string_lossy().to_string(); - eprintln!("[RUST LOG]: - Resolved to path relative to home: {}", result); - Ok(result) -} - #[tauri::command] fn log_error_to_console(error: &str) { eprintln!("[WebView ERROR forwarded from JS]: {}", error); } #[tauri::command] -fn get_api_key(state: tauri::State<'_, ApiKey>) -> Result, String> { - eprintln!("[RUST LOG]: get_api_key command called."); - let api_key = state.0.lock().unwrap().clone(); - eprintln!("[RUST LOG]: - Returning API key: {:?}", api_key.is_some()); - Ok(api_key) +fn increment_counter(state: tauri::State<'_, Counter>) -> Result { + eprintln!("[RUST LOG]: increment_counter command called."); + let mut counter = state.0.lock().unwrap(); + *counter += 1; + let current_value = *counter; + eprintln!("[RUST LOG]: - Counter incremented to: {}", current_value); + Ok(current_value) +} + +#[tauri::command] +fn get_counter(state: tauri::State<'_, Counter>) -> Result { + eprintln!("[RUST LOG]: get_counter command called."); + let counter = state.0.lock().unwrap(); + let current_value = *counter; + eprintln!("[RUST LOG]: - Current counter value: {}", current_value); + Ok(current_value) +} + +#[tauri::command] +fn reset_counter(state: tauri::State<'_, Counter>) -> Result { + eprintln!("[RUST LOG]: reset_counter command called."); + let mut counter = state.0.lock().unwrap(); + *counter = 0; + eprintln!("[RUST LOG]: - Counter reset to: 0"); + Ok(0) +} + +#[tauri::command] +fn add_debug_message(message: String, level: String, data: Option, state: tauri::State<'_, DebugMessages>) -> Result<(), String> { + eprintln!("[RUST LOG]: add_debug_message command called."); + eprintln!("[RUST LOG]: - Level: {}", level); + eprintln!("[RUST LOG]: - Message: {}", message); + + let debug_payload = DebugPayload { + level, + message, + data, + }; + + let mut messages = state.0.lock().unwrap(); + messages.push(debug_payload); + + // Keep only the last 100 messages to prevent memory issues + if messages.len() > 100 { + let len = messages.len(); + messages.drain(0..len - 100); + } + + eprintln!("[RUST LOG]: - Debug message added. Total messages: {}", messages.len()); + Ok(()) +} + +#[tauri::command] +fn get_debug_messages(state: tauri::State<'_, DebugMessages>) -> Result, String> { + eprintln!("[RUST LOG]: get_debug_messages command called."); + let messages = state.0.lock().unwrap(); + let result = messages.clone(); + eprintln!("[RUST LOG]: - Returning {} debug messages", result.len()); + Ok(result) +} + +#[tauri::command] +fn clear_debug_messages(state: tauri::State<'_, DebugMessages>) -> Result<(), String> { + eprintln!("[RUST LOG]: clear_debug_messages command called."); + let mut messages = state.0.lock().unwrap(); + messages.clear(); + eprintln!("[RUST LOG]: - Debug messages cleared"); + Ok(()) +} + +#[tauri::command] +fn send_ipc_message(message_type: String, data: serde_json::Value, _window: tauri::Window) -> Result<(), String> { + eprintln!("[RUST LOG]: send_ipc_message command called."); + eprintln!("[RUST LOG]: - Type: {}", message_type); + eprintln!("[RUST LOG]: - Data: {}", data); + + let ipc_message = IPCMessage { + message_type, + data, + timestamp: Some(std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64), + id: Some(format!("msg_{}_{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(), + rand::random::())), + }; + + let json_message = serde_json::to_string(&ipc_message).unwrap(); + eprintln!("[RUST LOG]: - Sending IPC message to stdout: {}", json_message); + println!("{}", json_message); + + Ok(()) +} + +#[tauri::command] +fn send_message_to_stdout(message: String) -> Result<(), String> { + eprintln!("[RUST LOG]: send_message_to_stdout command called."); + eprintln!("[RUST LOG]: - Message: {}", message); + + // Send directly to stdout (this will be captured by images.ts) + println!("{}", message); + + Ok(()) +} + +#[tauri::command] +fn generate_image_via_backend(prompt: String, files: Vec, dst: String) -> Result<(), String> { + eprintln!("[RUST LOG]: generate_image_via_backend called"); + eprintln!("[RUST LOG]: - Prompt: {}", prompt); + eprintln!("[RUST LOG]: - Files: {:?}", files); + eprintln!("[RUST LOG]: - Dst: {}", dst); + + // Send generation request to images.ts via stdout + let request = serde_json::json!({ + "type": "generate_request", + "prompt": prompt, + "files": files, + "dst": dst, + "timestamp": std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() + }); + + println!("{}", serde_json::to_string(&request).unwrap()); + eprintln!("[RUST LOG]: Generation request sent to images.ts"); + + Ok(()) +} + +#[tauri::command] +fn request_config_from_images(_app: tauri::AppHandle) -> Result<(), String> { + eprintln!("[RUST LOG]: request_config_from_images called"); + + // Send request to images.ts via stdout + let request = serde_json::json!({ + "type": "config_request", + "timestamp": std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() + }); + + println!("{}", serde_json::to_string(&request).unwrap()); + eprintln!("[RUST LOG]: Config request sent to images.ts"); + + Ok(()) +} + +#[tauri::command] +fn forward_config_to_frontend(prompt: Option, dst: Option, api_key: Option, files: Vec, app: tauri::AppHandle) -> Result<(), String> { + eprintln!("[RUST LOG]: forward_config_to_frontend called"); + + let config_data = serde_json::json!({ + "prompt": prompt, + "dst": dst, + "apiKey": api_key, + "files": files + }); + + if let Err(e) = app.emit("config-received", &config_data) { + eprintln!("[RUST LOG]: Failed to emit config-received: {}", e); + return Err(format!("Failed to emit config: {}", e)); + } + + eprintln!("[RUST LOG]: Config forwarded to frontend successfully"); + Ok(()) +} + +#[tauri::command] +fn forward_image_to_frontend(base64: String, mime_type: String, filename: String, app: tauri::AppHandle) -> Result<(), String> { + eprintln!("[RUST LOG]: forward_image_to_frontend called for {}", filename); + + let image_data = serde_json::json!({ + "base64": base64, + "mimeType": mime_type, + "filename": filename + }); + + if let Err(e) = app.emit("image-received", &image_data) { + eprintln!("[RUST LOG]: Failed to emit image-received: {}", e); + return Err(format!("Failed to emit image: {}", e)); + } + + eprintln!("[RUST LOG]: Image forwarded to frontend successfully"); + Ok(()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let cli_args: Vec = std::env::args().skip(1).collect(); - - // Parse API key from CLI args (--api-key value) - let mut api_key = None; - let mut file_args = Vec::new(); - - let mut i = 0; - while i < cli_args.len() { - if cli_args[i] == "--api-key" && i + 1 < cli_args.len() { - api_key = Some(cli_args[i + 1].clone()); - i += 2; // Skip both --api-key and its value - } else { - file_args.push(cli_args[i].clone()); - i += 1; - } - } - - // Fallback to environment variables if not provided via CLI - if api_key.is_none() { - api_key = std::env::var("GOOGLE_API_KEY") - .or_else(|_| std::env::var("GEMINI_API_KEY")) - .or_else(|_| std::env::var("API_KEY")) - .ok(); - } - - tauri::Builder::default() - .manage(CliArgs(std::sync::Mutex::new(file_args))) - .manage(ApiKey(std::sync::Mutex::new(api_key))) + let app = tauri::Builder::default() + .manage(Counter(std::sync::Mutex::new(0))) + .manage(DebugMessages(std::sync::Mutex::new(Vec::new()))) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_http::init()) - .invoke_handler(tauri::generate_handler![submit_prompt, log_error_to_console, get_cli_args, resolve_path_relative_to_home, get_api_key]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .invoke_handler(tauri::generate_handler![ + submit_prompt, + log_error_to_console, + increment_counter, + get_counter, + reset_counter, + add_debug_message, + get_debug_messages, + clear_debug_messages, + send_message_to_stdout, + send_ipc_message, + request_config_from_images, + forward_config_to_frontend, + forward_image_to_frontend + ]) + .setup(|app| { + let app_handle = app.handle().clone(); + + // Listen for stdin commands from images.ts + std::thread::spawn(move || { + use std::io::{self, BufRead, BufReader}; + let stdin = io::stdin(); + let reader = BufReader::new(stdin); + + eprintln!("[RUST LOG]: Stdin listener thread started"); + + for line in reader.lines() { + if let Ok(line_content) = line { + if line_content.trim().is_empty() { + continue; + } + + // Log stdin command but hide binary data + let log_content = if line_content.contains("\"base64\"") { + format!("[COMMAND WITH BASE64 DATA - {} chars]", line_content.len()) + } else { + line_content.clone() + }; + eprintln!("[RUST LOG]: Received stdin command: {}", log_content); + + // Parse command from images.ts + if let Ok(command) = serde_json::from_str::(&line_content) { + if let Some(cmd) = command.get("cmd").and_then(|v| v.as_str()) { + eprintln!("[RUST LOG]: Processing command: {}", cmd); + + match cmd { + "forward_config_to_frontend" => { + eprintln!("[RUST LOG]: Forwarding config to frontend"); + eprintln!("[RUST LOG]: - prompt: {:?}", command.get("prompt")); + eprintln!("[RUST LOG]: - dst: {:?}", command.get("dst")); + eprintln!("[RUST LOG]: - apiKey: {:?}", command.get("apiKey").map(|_| "[REDACTED]")); + eprintln!("[RUST LOG]: - files: {:?}", command.get("files")); + + let config_data = serde_json::json!({ + "prompt": command.get("prompt"), + "dst": command.get("dst"), + "apiKey": command.get("apiKey"), + "files": command.get("files") + }); + + if let Err(e) = app_handle.emit("config-received", &config_data) { + eprintln!("[RUST LOG]: Failed to emit config-received: {}", e); + } else { + eprintln!("[RUST LOG]: Config emitted successfully to frontend"); + } + } + "forward_image_to_frontend" => { + if let (Some(filename), Some(base64), Some(mime_type)) = ( + command.get("filename").and_then(|v| v.as_str()), + command.get("base64").and_then(|v| v.as_str()), + command.get("mimeType").and_then(|v| v.as_str()) + ) { + eprintln!("[RUST LOG]: Forwarding image to frontend: {}", filename); + let image_data = serde_json::json!({ + "base64": base64, + "mimeType": mime_type, + "filename": filename + }); + + if let Err(e) = app_handle.emit("image-received", &image_data) { + eprintln!("[RUST LOG]: Failed to emit image-received: {}", e); + } else { + eprintln!("[RUST LOG]: Image emitted successfully: {}", filename); + } + } + } + _ => { + eprintln!("[RUST LOG]: Unknown command: {}", cmd); + } + } + } + } else { + eprintln!("[RUST LOG]: Failed to parse command as JSON"); + } + } + } + eprintln!("[RUST LOG]: Stdin listener thread ended"); + }); + + Ok(()) + }) + .build(tauri::generate_context!()) + .expect("error while building tauri application"); + + app.run(|_app_handle, event| match event { + tauri::RunEvent::ExitRequested { api, .. } => { + api.prevent_exit(); + } + _ => {} + }); } diff --git a/packages/kbot/gui/tauri-app/src/App.tsx b/packages/kbot/gui/tauri-app/src/App.tsx index 7709b510..16506625 100644 --- a/packages/kbot/gui/tauri-app/src/App.tsx +++ b/packages/kbot/gui/tauri-app/src/App.tsx @@ -1,8 +1,72 @@ import { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; -import { open, save } from '@tauri-apps/plugin-dialog'; -import { readFile, writeFile, BaseDirectory } from '@tauri-apps/plugin-fs'; -import { fetch } from '@tauri-apps/plugin-http'; + +let invoke: any, open: any, save: any, readFile: any, writeFile: any, BaseDirectory: any, fetch: any, listen: any, getCurrentWindow: any; +let isTauri = false; + +// Detect Tauri environment by trying to import getCurrentWindow +import("@tauri-apps/api/window").then(module => { + getCurrentWindow = module.getCurrentWindow; + isTauri = true; + console.log('✅ Tauri window API loaded - running in Tauri environment'); +}).catch(() => { + console.log('🌐 Tauri window API not available - running in browser mode'); + isTauri = false; +}); + +// Load other Tauri APIs +import("@tauri-apps/api/core").then(module => { + invoke = module.invoke; + console.log('✅ Tauri core API loaded'); +}).catch(() => { + console.log('❌ Tauri core API not available'); +}); + +import("@tauri-apps/api/event").then(module => { + listen = module.listen; + console.log('✅ Tauri event API loaded'); +}).catch(() => { + console.log('❌ Tauri event API not available'); +}); + +import("@tauri-apps/plugin-dialog").then(module => { + open = module.open; + save = module.save; + console.log('✅ Tauri dialog plugin loaded'); +}).catch(() => { + console.log('❌ Tauri dialog plugin not available'); +}); + +import("@tauri-apps/plugin-fs").then(module => { + readFile = module.readFile; + writeFile = module.writeFile; + BaseDirectory = module.BaseDirectory; + console.log('✅ Tauri fs plugin loaded'); +}).catch(() => { + console.log('❌ Tauri fs plugin not available'); +}); + +import("@tauri-apps/plugin-http").then(module => { + fetch = module.fetch; + console.log('✅ Tauri http plugin loaded'); +}).catch(() => { + console.log('❌ Tauri http plugin not available'); + fetch = window.fetch; // Browser fallback +}); + +// Browser fallback +if (typeof window !== 'undefined' && !fetch) { + fetch = window.fetch; +} + +// Safe invoke function that works in both Tauri and browser environments +const safeInvoke = (command: string, args?: any) => { + if (invoke) { + return invoke(command, args); + } else { + console.log(`[Browser Mode] Would invoke: ${command}`, args); + return Promise.resolve(null); + } +}; // Path imports commented out since they're not currently used // import { // homeDir, audioDir, cacheDir, configDir, dataDir, localDataDir, desktopDir, @@ -43,6 +107,12 @@ function App() { const [generatedImages, setGeneratedImages] = useState([]); const [isGenerating, setIsGenerating] = useState(false); const [apiKey, setApiKey] = useState(""); + const [isDarkMode, setIsDarkMode] = useState(false); + const [counter, setCounter] = useState(0); + const [debugMessages, setDebugMessages] = useState([]); + const [showDebugPanel, setShowDebugPanel] = useState(true); // Default open for debugging + const [ipcInitialized, setIpcInitialized] = useState(false); + const [messageToSend, setMessageToSend] = useState(""); const generateDefaultDst = (fileCount: number, firstFilePath?: string) => { if (fileCount === 1 && firstFilePath) { @@ -58,27 +128,60 @@ function App() { }; const addFiles = async (newPaths: string[]) => { + if (!isTauri) { + console.log('[Browser Mode] File operations not available'); + return; + } + + // Wait for Tauri APIs to be loaded + let attempts = 0; + while ((!readFile || !BaseDirectory) && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 100)); + attempts++; + } + + if (!readFile || !BaseDirectory) { + console.error('Tauri APIs not loaded after 5 seconds, cannot process files'); + return; + } + const uniqueNewPaths = newPaths.filter(newPath => !files.some(f => f.path === newPath)); const newImageFiles: ImageFile[] = []; - console.log('uniqueNewPaths', uniqueNewPaths); + console.log('Processing uniqueNewPaths:', uniqueNewPaths); + for (const path of uniqueNewPaths) { try { + console.log('Processing path:', path); + // Ask Rust to make the path relative to the HOME directory - console.log('path', path); - const relativePath = await invoke('resolve_path_relative_to_home', { absolutePath: path }); + const relativePath = await safeInvoke('resolve_path_relative_to_home', { absolutePath: path }) as string; + console.log('Relative path resolved:', relativePath); + + if (!relativePath) { + console.warn(`Could not resolve relative path for: ${path}`); + continue; + } + const buffer = await readFile(relativePath, { baseDir: BaseDirectory.Home }); + console.log('File read successfully, buffer size:', buffer.length); + const base64 = arrayBufferToBase64(Array.from(buffer)); - //const mimeType = lookup(path) || 'image/png'; - const mimeType = 'image/png'; + const mimeType = path.toLowerCase().endsWith('.png') ? 'image/png' : + path.toLowerCase().endsWith('.jpg') || path.toLowerCase().endsWith('.jpeg') ? 'image/jpeg' : + 'image/png'; // default const src = `data:${mimeType};base64,${base64}`; + newImageFiles.push({ path, src }); + console.log('Successfully processed file:', path); } catch (e) { const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); console.error(`Failed to read file: ${path}`, e); - invoke('log_error_to_console', { error: `[Frontend Error] Failed to read file ${path}: ${errorMessage}` }); + safeInvoke('log_error_to_console', { error: `[Frontend Error] Failed to read file ${path}: ${errorMessage}` }); } } + + console.log('Adding files to state:', newImageFiles.length); setFiles(prevFiles => [...prevFiles, ...newImageFiles]); }; @@ -183,35 +286,208 @@ function App() { toString: error?.toString() || 'No string representation' }; console.error('Error details:', errorDetails); - invoke('log_error_to_console', { error: `[Frontend Error] Image generation failed: ${JSON.stringify(errorDetails)}` }); + safeInvoke('log_error_to_console', { error: `[Frontend Error] Image generation failed: ${JSON.stringify(errorDetails)}` }); } finally { setIsGenerating(false); } }; + // Theme management useEffect(() => { - const fetchCliArgs = async () => { - try { - const cliFiles = await invoke('get_cli_args'); - if (cliFiles && cliFiles.length > 0) { - addFiles(cliFiles); - } - - // Try to get API key from CLI environment or args - try { - const key = await invoke('get_api_key'); - if (key) { - setApiKey(key); - setChatMode(true); // Enable chat mode if API key is available - } - } catch (e) { - console.log("No API key provided, using simple mode"); - } - } catch (e) { - console.error("Failed to get CLI arguments:", e); + // Load theme from localStorage + const savedTheme = localStorage.getItem('theme'); + if (savedTheme === 'dark') { + setIsDarkMode(true); + document.documentElement.classList.add('dark'); + } else if (savedTheme === 'light') { + setIsDarkMode(false); + document.documentElement.classList.remove('dark'); + } else { + // Default to system preference + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + setIsDarkMode(prefersDark); + if (prefersDark) { + document.documentElement.classList.add('dark'); } + } + }, []); + + const toggleTheme = () => { + const newDarkMode = !isDarkMode; + setIsDarkMode(newDarkMode); + + if (newDarkMode) { + document.documentElement.classList.add('dark'); + localStorage.setItem('theme', 'dark'); + } else { + document.documentElement.classList.remove('dark'); + localStorage.setItem('theme', 'light'); + } + }; + + useEffect(() => { + const initializeApp = async () => { + console.log('Initializing app...'); + console.log('isTauri:', isTauri); + + // Wait for Tauri APIs to be loaded first (check if any loaded) + let attempts = 0; + while (!invoke && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 100)); + attempts++; + } + + // Update isTauri based on whether APIs actually loaded + const actuallyInTauri = !!invoke; + console.log('Environment detection result:', { + initialDetection: isTauri, + actuallyInTauri, + invokeLoaded: !!invoke, + listenLoaded: !!listen + }); + + if (actuallyInTauri) { + + // Initialize counter from backend + try { + await getCounter(); + addDebugMessage('info', 'IPC system initialized successfully'); + } catch (e) { + console.log("Failed to initialize counter:", e); + addDebugMessage('warn', 'Counter initialization failed - running in browser mode'); + } + + } + + // Set up Tauri event listeners for config and images + const setupTauriEventListeners = async () => { + if (actuallyInTauri && listen) { + try { + console.log('Setting up Tauri event listeners'); + + // Listen for config data + await listen('config-received', (event: any) => { + console.log('Received config event:', event); + const data = event.payload; + + console.log('Config data details:', { + prompt: data.prompt, + dst: data.dst, + apiKey: data.apiKey ? '[REDACTED]' : null, + files: data.files + }); + + if (data.prompt) { + console.log('Setting prompt from config:', data.prompt); + setPrompt(data.prompt); + } else { + console.log('No prompt in config data'); + } + + if (data.dst) { + console.log('Setting dst from config:', data.dst); + setDst(data.dst); + } else { + console.log('No dst in config data'); + } + + if (data.apiKey) { + console.log('Setting API key from config'); + setApiKey(data.apiKey); + setChatMode(true); + } else { + console.log('No API key in config data'); + } + + setIpcInitialized(true); + addDebugMessage('info', '📨 Config received from images.ts', { + hasPrompt: !!data.prompt, + hasDst: !!data.dst, + hasApiKey: !!data.apiKey, + fileCount: data.files?.length || 0 + }); + }); + + // Listen for image data + await listen('image-received', (event: any) => { + const imageData = event.payload; + + addDebugMessage('debug', '🖼️ Processing image data', { + filename: imageData.filename, + mimeType: imageData.mimeType, + base64Length: imageData.base64?.length, + base64Sample: imageData.base64?.substring(0, 50) + '...', + hasValidData: !!(imageData.base64 && imageData.mimeType && imageData.filename) + }); + + if (imageData.base64 && imageData.mimeType && imageData.filename) { + const src = `data:${imageData.mimeType};base64,${imageData.base64}`; + + // Test if the base64 is valid by trying to create an image + const testImg = new Image(); + testImg.onload = () => { + addDebugMessage('info', `✅ Image loaded successfully: ${imageData.filename}`); + }; + testImg.onerror = () => { + addDebugMessage('error', `❌ Failed to load image: ${imageData.filename}`, { + mimeType: imageData.mimeType, + base64Start: imageData.base64.substring(0, 100) + }); + }; + testImg.src = src; + + const newImageFile = { path: imageData.filename, src }; + setFiles(prevFiles => { + const exists = prevFiles.some(f => f.path === imageData.filename); + if (!exists) { + addDebugMessage('info', `📁 Adding image to files: ${imageData.filename}`); + return [...prevFiles, newImageFile]; + } + addDebugMessage('warn', `🔄 Image already exists: ${imageData.filename}`); + return prevFiles; + }); + + addDebugMessage('info', '📨 Image received from images.ts', { + filename: imageData.filename, + mimeType: imageData.mimeType, + size: `${Math.round(imageData.base64.length/1024)}KB` + }); + } else { + addDebugMessage('error', '❌ Invalid image data received', { + hasBase64: !!imageData.base64, + hasMimeType: !!imageData.mimeType, + hasFilename: !!imageData.filename + }); + } + }); + + addDebugMessage('info', 'Tauri event listeners set up'); + + // NOW request config from images.ts (after listeners are ready) + try { + console.log('Requesting config from images.ts...'); + await safeInvoke('request_config_from_images'); + addDebugMessage('info', 'Config request sent to images.ts'); + } catch (e) { + console.error('Failed to request config:', e); + addDebugMessage('error', `Failed to request config: ${e}`); + } + + } catch (error) { + console.error('Failed to set up Tauri event listeners:', error); + addDebugMessage('error', `Failed to set up event listeners: ${error}`); + } + } else { + addDebugMessage('warn', 'Tauri event listeners not available - running in browser mode'); + } + }; + + // Set up event listeners with delay to ensure listen function is loaded + setTimeout(setupTauriEventListeners, 500); }; - fetchCliArgs(); + + // Delay the initialization slightly to ensure everything is loaded + setTimeout(initializeApp, 200); }, []); // Auto-generate default destination file when files change @@ -271,17 +547,160 @@ function App() { } else { // Simple mode: send to CLI try { - const result = await invoke("submit_prompt", { prompt, files: files.map(f => f.path), dst }); + const result = await safeInvoke("submit_prompt", { prompt, files: files.map(f => f.path), dst }); console.log('Submit result:', result); } catch (error) { console.error('Submit error:', error); - invoke('log_error_to_console', { error: `[Frontend Error] Submit failed: ${JSON.stringify(error)}` }); + safeInvoke('log_error_to_console', { error: `[Frontend Error] Submit failed: ${JSON.stringify(error)}` }); } } } - const saveGeneratedImage = async (generatedImage: GeneratedImage) => { + // Counter functions + const incrementCounter = async () => { + if (!isTauri) { + setCounter(prev => prev + 1); + return; + } + try { + const newCount = await safeInvoke('increment_counter') as number; + setCounter(newCount); + addDebugMessage('info', `Counter incremented to ${newCount}`); + } catch (error) { + console.error('Failed to increment counter:', error); + addDebugMessage('error', `Failed to increment counter: ${error}`); + } + }; + + const resetCounter = async () => { + if (!isTauri) { + setCounter(0); + return; + } + + try { + await safeInvoke('reset_counter'); + setCounter(0); + addDebugMessage('info', 'Counter reset to 0'); + } catch (error) { + console.error('Failed to reset counter:', error); + addDebugMessage('error', `Failed to reset counter: ${error}`); + } + }; + + const getCounter = async () => { + if (!isTauri) return; + + try { + const currentCount = await safeInvoke('get_counter') as number; + setCounter(currentCount); + addDebugMessage('info', `Current counter value: ${currentCount}`); + } catch (error) { + console.error('Failed to get counter:', error); + addDebugMessage('error', `Failed to get counter: ${error}`); + } + }; + + // Debug message functions + const addDebugMessage = async (level: 'info' | 'warn' | 'error' | 'debug', message: string, data?: any) => { + const timestamp = new Date().toLocaleTimeString(); + const debugMsg = { level, message, data, timestamp }; + + // Add to local state + setDebugMessages(prev => [...prev.slice(-99), debugMsg]); // Keep last 100 messages + + if (!isTauri) return; + + try { + await safeInvoke('add_debug_message', { message, level, data }); + } catch (error) { + console.error('Failed to add debug message:', error); + } + }; + + const clearDebugMessages = async () => { + setDebugMessages([]); + + if (!isTauri) return; + + try { + await safeInvoke('clear_debug_messages'); + addDebugMessage('info', 'Debug messages cleared'); + } catch (error) { + console.error('Failed to clear debug messages:', error); + } + }; + + const sendIPCMessage = async (messageType: string, data: any) => { + if (!isTauri) { + console.log(`[Browser Mode] Would send IPC message: ${messageType}`, data); + return; + } + + try { + await safeInvoke('send_ipc_message', { messageType, data }); + addDebugMessage('info', `IPC message sent: ${messageType}`, data); + } catch (error) { + console.error('Failed to send IPC message:', error); + addDebugMessage('error', `Failed to send IPC message: ${error}`); + } + }; + + const sendMessageToImages = async () => { + if (!messageToSend.trim()) return; + + const message = { + message: messageToSend, + timestamp: Date.now(), + source: 'gui' + }; + + try { + // Send via Tauri command to stdout (bypasses console hijacking) + await safeInvoke('send_message_to_stdout', { message: JSON.stringify(message) }); + + // Also add to our debug panel + addDebugMessage('info', `📤 Sent to images.ts: ${messageToSend}`, message); + } catch (error) { + console.error('Failed to send message to images.ts:', error); + addDebugMessage('error', `Failed to send message: ${error}`); + } + + // Clear the input + setMessageToSend(''); + }; + + + const saveGeneratedImage = async (generatedImage: GeneratedImage) => { + if (!isTauri) { + console.log('[Browser Mode] File save not available - downloading instead'); + // Browser fallback: trigger download + const base64Match = generatedImage.src.match(/^data:([^;]+);base64,(.+)$/); + if (base64Match) { + const link = document.createElement('a'); + link.download = `generated_${generatedImage.id}.png`; + link.href = generatedImage.src; + link.click(); + + // Mark as saved + setGeneratedImages(prev => + prev.map(img => + img.id === generatedImage.id + ? { ...img, saved: true } + : img + ) + ); + } + return; + } + + try { + if (!save || !writeFile) { + console.error('Save functions not available'); + return; + } + const defaultFilename = `generated_${generatedImage.id}.png`; const filePath = await save({ defaultPath: defaultFilename, @@ -314,12 +733,53 @@ function App() { } } catch (error) { console.error('Failed to save image:', error); - invoke('log_error_to_console', { error: `[Frontend Error] Failed to save image: ${JSON.stringify(error)}` }); + safeInvoke('log_error_to_console', { error: `[Frontend Error] Failed to save image: ${JSON.stringify(error)}` }); } }; async function openFilePicker() { + if (!isTauri) { + console.log('[Browser Mode] File picker not available'); + // Browser fallback: create file input + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.accept = 'image/*'; + input.onchange = (e) => { + const target = e.target as HTMLInputElement; + if (target.files) { + const fileArray = Array.from(target.files); + const newImageFiles: ImageFile[] = []; + let loadedCount = 0; + + fileArray.forEach(file => { + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target?.result) { + newImageFiles.push({ + path: file.name, + src: e.target.result as string + }); + loadedCount++; + if (loadedCount === fileArray.length) { + setFiles(prevFiles => [...prevFiles, ...newImageFiles]); + } + } + }; + reader.readAsDataURL(file); + }); + } + }; + input.click(); + return; + } + try { + if (!open) { + console.error('Open function not available'); + return; + } + const selected = await open({ multiple: true, filters: [{ @@ -332,7 +792,7 @@ function App() { } } catch (e) { console.error('File picker error:', e); - invoke('log_error_to_console', { error: `[Frontend Error] File picker error: ${JSON.stringify(e)}` }); + safeInvoke('log_error_to_console', { error: `[Frontend Error] File picker error: ${JSON.stringify(e)}` }); } } @@ -354,104 +814,182 @@ function App() { } } catch (e) { console.error('Save dialog error:', e); - invoke('log_error_to_console', { error: `[Frontend Error] Save dialog error: ${JSON.stringify(e)}` }); + safeInvoke('log_error_to_console', { error: `[Frontend Error] Save dialog error: ${JSON.stringify(e)}` }); } } return ( -
-
-
-

Image Prompt

-
- Simple +
+ {/* Background decoration */} +
+
+
+
+ +
+
+

Image Prompt

+
+ {/* Debug Panel Toggle */} - Chat - {!apiKey && (API key required)} + + {/* Counter Display */} +
+ Counter: {counter} +
+ + +
+
+ + {/* Theme Toggle */} + + + {/* Chat Mode Toggle */} +
+ Simple + + Chat + {!apiKey && (API key required)} +
{ e.preventDefault(); submit(); }} > -