images interface

This commit is contained in:
babayaga 2025-09-23 20:32:47 +02:00
parent 649c91ec35
commit 9284894589
16 changed files with 1884 additions and 113 deletions

View File

@ -0,0 +1,135 @@
# Image Generation Architecture — Platform v5
This document captures the shape of the refreshed multiplatform image generation plan that we will break down into actionable tasks next. It keeps the current CLI + desktop flow, layers in mobile (Android/iOS) expectations, and sketches a browser/web-app path with configurable endpoints.
## 1. CLI Desktop (Current Flow)
- **Ownership**: `src/commands/images.ts` remains the orchestration point; it spawns the packaged Tauri desktop binary and handles filesystem writes.
- **IPC Contract**: JSON payloads over `stdin`/`stdout` between the CLI and Tauri. The CLI continues to push resolved prompts, destination paths, API key, and included files.
- **Image Ops**: Google Generative AI integration stays in Node-land (`createImage`, `editImage`) with `@polymech/fs` helpers for persistence.
```ts
// CLI-side launch (simplified excerpt)
const tauriProcess = spawn(getGuiAppPath(), args, { stdio: ['pipe', 'pipe', 'pipe'] });
tauriProcess.stdin?.write(JSON.stringify({
cmd: 'forward_config_to_frontend',
prompt: argv.prompt,
dst: argv.dst,
apiKey: apiKey,
files: absoluteIncludes,
}) + '\n');
```
**Libraries**: existing stack (`@polymech` packages, `tslog`, Node core modules). No new work required beyond polish/bugfix.
## 2. Android / iOS — Standalone Tauri
Desktop spawning is not available on mobile; the GUI ships as the full application. We lean on the TypeScript layer plus Tauris HTTP plugin to hit Googles endpoints without wiring Rust-side HTTP clients.
### Requirements
- Bundle `@tauri-apps/plugin-http`, `@tauri-apps/plugin-os`, `@tauri-apps/plugin-fs`.
- Rely on the existing `tauriApi.fetch` abstraction so we do not unwrap the plugin everywhere.
- Persist lightweight state (prompt history, cached API key) in app data dir just like desktop.
### Example TypeScript Mobile Client
```ts
// gui/tauri-app/src/lib/mobileClient.ts
import { tauriApi } from './tauriApi';
const GOOGLE_BASE = 'https://generativelanguage.googleapis.com/v1beta';
export async function mobileCreateImage(prompt: string, apiKey: string, model = 'gemini-2.5-flash-image-preview') {
const response = await tauriApi.fetch(`${GOOGLE_BASE}/models/${model}:generateContent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }),
});
const data = await response.json();
const inline = data.candidates?.[0]?.content?.parts?.find((part: any) => part.inlineData)?.inlineData;
if (!inline?.data) throw new Error('No image data in Gemini response');
return Buffer.from(inline.data, 'base64');
}
```
### Configuration Notes
- `tauri.conf.json` must whitelist `https://generativelanguage.googleapis.com/**` inside the HTTP plugin scope and CSP `connect-src`.
- Add platform detection inside the React/Svelte front-end to toggle mobile-first UX and storage paths.
**Libraries**: `@tauri-apps/plugin-http`, `@tauri-apps/api`, `@google/generative-ai` (optional; the REST fetch example above avoids it if desired), existing UI stack.
## 3. Web App — Browser, Configurable Endpoints
Constraints (CORS, secret handling) require a server-side companion and a client that can be pointed at custom endpoints per user/tenant. The browser front-end holds no secrets; all API keys live server-side.
### Backend Sketch (Hono)
```ts
// web/api/imageServer.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { GoogleGenerativeAI } from '@google/generative-ai';
const app = new Hono();
app.use('/*', cors({
origin: ['http://localhost:3000', 'https://your-frontend.example'],
allowHeaders: ['Content-Type', 'Authorization'],
allowMethods: ['POST', 'OPTIONS'],
}));
app.post('/api/images/create', async (c) => {
const { prompt, apiKey, model = 'gemini-2.5-flash-image-preview' } = await c.req.json();
const genAI = new GoogleGenerativeAI(apiKey);
const modelClient = genAI.getGenerativeModel({ model });
const result = await modelClient.generateContent(prompt);
const inline = result.response.candidates?.[0]?.content?.parts?.find((part) => 'inlineData' in part)?.inlineData;
if (!inline?.data) return c.json({ success: false, error: 'No image data' }, 500);
return c.json({ success: true, image: inline });
});
export default app;
```
### Browser Client Stub
```ts
// web/client/webImageClient.ts
export class WebImageClient {
constructor(private endpoint: string) {}
async createImage(prompt: string, apiKeyAlias: string) {
const res = await fetch(`${this.endpoint}/api/images/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, apiKey: apiKeyAlias }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!data.success) throw new Error(data.error || 'Unknown backend error');
return data.image; // caller decides how to render Blob/Base64
}
}
```
### Configuration Extension
- Expand shared config schema with a `web.apiEndpoint` block and optional per-user overrides.
- Allow `cli` users to pass `--web-endpoint` for headless flows that still want the backend.
- Document environment variable support (`REACT_APP_API_ENDPOINT`, `VITE_IMAGE_API_URL`, etc.).
**Libraries**: `hono`, `hono/cors`, `@google/generative-ai`, hosting runtime (`bun`, `node`, or serverless). Front-end remains React/Vite/SvelteKit as today.
## Cross-Platform Checklist (Preview)
- Align TypeScript interfaces (`UnifiedImageGenerator`) so desktop/mobile/web can plug into the same UI surface.
- Ensure persistent storage format (`.kbot-gui.json`) works across platforms—consider namespacing mobile vs desktop history entries.
- Plan rate limiting and API key management per platform (mobile secure storage, web backend vault).
- Identify testing layers (unit mocks for fetch, integration harness for Tauri mobile, e2e web flows).
This structure will be decomposed into a detailed TODO roadmap in the following slice.

View File

@ -0,0 +1,603 @@
# Image Generation Architecture — Multi-Platform Strategy
This document outlines the architectural approach for supporting image generation across CLI (desktop), mobile (Android/iOS), and web platforms while maintaining code reuse and consistent user experience.
## Current State Analysis
The existing CLI flow works well for desktop scenarios:
- `src/commands/images.ts` orchestrates the process
- Spawns Tauri desktop binary via `spawn()`
- Handles image operations through Google Generative AI
- Uses filesystem operations via `@polymech/fs`
- IPC communication over stdin/stdout with JSON payloads
## 1. CLI Desktop (Current Flow - Maintained)
**Architecture**: CLI spawns Tauri GUI, handles all image operations in Node.js
```ts
// src/commands/images.ts (existing pattern)
const tauriProcess = spawn(getGuiAppPath(), args, { stdio: ['pipe', 'pipe', 'pipe'] });
// Send config to GUI
tauriProcess.stdin?.write(JSON.stringify({
cmd: 'forward_config_to_frontend',
prompt: argv.prompt,
dst: argv.dst,
apiKey: apiKey,
files: absoluteIncludes,
}) + '\n');
// Handle generation requests from GUI
if (message.type === 'generate_request') {
const imageBuffer = genFiles.length > 0
? await editImage(genPrompt, genFiles, parsedOptions)
: await createImage(genPrompt, parsedOptions);
write(finalDstPath, imageBuffer);
}
```
**Libraries**:
- Existing stack: `@polymech/fs`, `tslog`, Node core modules
- Google Generative AI integration
- Tauri for GUI spawning
**No changes required** - this flow remains optimal for desktop CLI usage.
## 2. Android/iOS - Standalone Tauri with TypeScript HTTP Client
**Architecture**: Tauri app runs standalone, TypeScript handles HTTP calls directly
Since mobile platforms cannot spawn processes, the Tauri app becomes the primary application. We leverage Tauri's HTTP plugin to make API calls from the TypeScript frontend.
### Configuration Updates
```json
// gui/tauri-app/src-tauri/tauri.conf.json
{
"plugins": {
"http": {
"scope": [
"https://generativelanguage.googleapis.com/**"
]
}
},
"security": {
"csp": "connect-src 'self' https://generativelanguage.googleapis.com"
}
}
```
### Mobile Image Client
```ts
// gui/tauri-app/src/lib/mobileImageClient.ts
import { tauriApi } from './tauriApi';
const GOOGLE_GENERATIVE_AI_BASE = 'https://generativelanguage.googleapis.com/v1beta';
export interface MobileImageOptions {
model?: string;
apiKey: string;
}
export class MobileImageClient {
constructor(private options: MobileImageOptions) {}
async createImage(prompt: string): Promise<Buffer> {
const { model = 'gemini-2.5-flash-image-preview', apiKey } = this.options;
const response = await tauriApi.fetch(`${GOOGLE_GENERATIVE_AI_BASE}/models/${model}:generateContent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
contents: [{
parts: [{ text: prompt }]
}]
}),
});
if (!response.ok) {
throw new Error(`Google API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const inline = data.candidates?.[0]?.content?.parts?.find(
(part: any) => part.inlineData
)?.inlineData;
if (!inline?.data) {
throw new Error('No image data in Gemini response');
}
return Buffer.from(inline.data, 'base64');
}
async editImage(prompt: string, imageFiles: string[]): Promise<Buffer> {
const { model = 'gemini-2.5-flash-image-preview', apiKey } = this.options;
// Read image files using Tauri FS
const imageParts = await Promise.all(
imageFiles.map(async (filePath) => {
const imageData = await tauriApi.fs.readFile(filePath);
const base64 = btoa(String.fromCharCode(...imageData));
const mimeType = filePath.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';
return {
inlineData: {
mimeType,
data: base64
}
};
})
);
const response = await tauriApi.fetch(`${GOOGLE_GENERATIVE_AI_BASE}/models/${model}:generateContent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
contents: [{
parts: [
{ text: prompt },
...imageParts
]
}]
}),
});
if (!response.ok) {
throw new Error(`Google API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const inline = data.candidates?.[0]?.content?.parts?.find(
(part: any) => part.inlineData
)?.inlineData;
if (!inline?.data) {
throw new Error('No image data in Gemini response');
}
return Buffer.from(inline.data, 'base64');
}
}
```
### Mobile Integration
```ts
// gui/tauri-app/src/components/MobileImageWizard.tsx
import { MobileImageClient } from '../lib/mobileImageClient';
export function MobileImageWizard() {
const [apiKey, setApiKey] = useState('');
const [prompt, setPrompt] = useState('');
const handleGenerate = async () => {
const client = new MobileImageClient({ apiKey });
try {
const imageBuffer = await client.createImage(prompt);
// Save to mobile app data directory
const appDataDir = await tauriApi.path.appDataDir();
const imagePath = await tauriApi.path.join(appDataDir, `generated_${Date.now()}.png`);
await tauriApi.fs.writeFile(imagePath, imageBuffer);
// Update UI with generated image
setGeneratedImage(imagePath);
} catch (error) {
console.error('Generation failed:', error);
}
};
return (
<div className="mobile-image-wizard">
{/* Mobile-optimized UI */}
</div>
);
}
```
**Libraries**:
- `@tauri-apps/plugin-http` - HTTP requests
- `@tauri-apps/plugin-fs` - File system operations
- `@tauri-apps/plugin-os` - Platform detection
- Existing React/TypeScript stack
## 3. Web App - Browser with Backend API
**Architecture**: Browser frontend + backend API server, configurable endpoints
Web browsers have CORS restrictions and cannot store API keys securely. We need a backend service to handle API calls and a configurable frontend.
### Backend API Server (Hono)
```ts
// web/api/imageServer.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { z } from 'zod';
const app = new Hono();
// CORS configuration
app.use('/*', cors({
origin: [
'http://localhost:3000',
'http://localhost:5173', // Vite dev
process.env.FRONTEND_URL || 'https://your-app.example.com'
],
allowHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
allowMethods: ['POST', 'GET', 'OPTIONS'],
}));
// Request schemas
const CreateImageSchema = z.object({
prompt: z.string().min(1),
model: z.string().default('gemini-2.5-flash-image-preview'),
userApiKey: z.string().optional(), // User-provided API key
});
const EditImageSchema = z.object({
prompt: z.string().min(1),
images: z.array(z.object({
data: z.string(), // base64
mimeType: z.string(),
})),
model: z.string().default('gemini-2.5-flash-image-preview'),
userApiKey: z.string().optional(),
});
// Middleware for API key resolution
const resolveApiKey = async (c: any, userApiKey?: string) => {
// Priority: user-provided > environment > tenant-specific
return userApiKey ||
process.env.GOOGLE_GENERATIVE_AI_KEY ||
await getTenantApiKey(c.req.header('X-Tenant-ID'));
};
app.post('/api/images/create', async (c) => {
try {
const body = await c.req.json();
const { prompt, model, userApiKey } = CreateImageSchema.parse(body);
const apiKey = await resolveApiKey(c, userApiKey);
if (!apiKey) {
return c.json({ success: false, error: 'No API key available' }, 401);
}
const genAI = new GoogleGenerativeAI(apiKey);
const modelClient = genAI.getGenerativeModel({ model });
const result = await modelClient.generateContent(prompt);
const response = await result.response;
const inline = response.candidates?.[0]?.content?.parts?.find(
(part) => 'inlineData' in part
)?.inlineData;
if (!inline?.data) {
return c.json({ success: false, error: 'No image data in response' }, 500);
}
return c.json({
success: true,
image: {
data: inline.data,
mimeType: inline.mimeType || 'image/png'
}
});
} catch (error) {
console.error('Create image error:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
app.post('/api/images/edit', async (c) => {
try {
const body = await c.req.json();
const { prompt, images, model, userApiKey } = EditImageSchema.parse(body);
const apiKey = await resolveApiKey(c, userApiKey);
if (!apiKey) {
return c.json({ success: false, error: 'No API key available' }, 401);
}
const genAI = new GoogleGenerativeAI(apiKey);
const modelClient = genAI.getGenerativeModel({ model });
const parts = [
{ text: prompt },
...images.map(img => ({
inlineData: {
mimeType: img.mimeType,
data: img.data
}
}))
];
const result = await modelClient.generateContent({ contents: [{ parts }] });
const response = await result.response;
const inline = response.candidates?.[0]?.content?.parts?.find(
(part) => 'inlineData' in part
)?.inlineData;
if (!inline?.data) {
return c.json({ success: false, error: 'No image data in response' }, 500);
}
return c.json({
success: true,
image: {
data: inline.data,
mimeType: inline.mimeType || 'image/png'
}
});
} catch (error) {
console.error('Edit image error:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Health check
app.get('/api/health', (c) => {
return c.json({ status: 'ok', timestamp: new Date().toISOString() });
});
export default app;
```
### Web Client
```ts
// web/client/webImageClient.ts
export interface WebImageClientConfig {
endpoint: string;
apiKey?: string; // Optional user API key
tenantId?: string;
}
export interface ImageResult {
data: string; // base64
mimeType: string;
}
export class WebImageClient {
constructor(private config: WebImageClientConfig) {}
async createImage(prompt: string, model?: string): Promise<ImageResult> {
const response = await fetch(`${this.config.endpoint}/api/images/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.config.tenantId && { 'X-Tenant-ID': this.config.tenantId }),
},
body: JSON.stringify({
prompt,
model,
userApiKey: this.config.apiKey,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Network error' }));
throw new Error(error.error || `HTTP ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Unknown server error');
}
return data.image;
}
async editImage(prompt: string, imageFiles: File[], model?: string): Promise<ImageResult> {
// Convert files to base64
const images = await Promise.all(
imageFiles.map(async (file) => ({
data: await fileToBase64(file),
mimeType: file.type,
}))
);
const response = await fetch(`${this.config.endpoint}/api/images/edit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.config.tenantId && { 'X-Tenant-ID': this.config.tenantId }),
},
body: JSON.stringify({
prompt,
images,
model,
userApiKey: this.config.apiKey,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Network error' }));
throw new Error(error.error || `HTTP ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Unknown server error');
}
return data.image;
}
}
// Utility function
async function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
resolve(result.split(',')[1]); // Remove data:image/...;base64, prefix
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
```
### Web Frontend Integration
```tsx
// web/components/WebImageWizard.tsx
import { WebImageClient } from '../client/webImageClient';
export function WebImageWizard() {
const [client, setClient] = useState<WebImageClient | null>(null);
const [endpoint, setEndpoint] = useState(process.env.REACT_APP_API_ENDPOINT || '');
const [apiKey, setApiKey] = useState('');
useEffect(() => {
if (endpoint) {
setClient(new WebImageClient({ endpoint, apiKey }));
}
}, [endpoint, apiKey]);
const handleGenerate = async (prompt: string) => {
if (!client) return;
try {
const result = await client.createImage(prompt);
// Create blob URL for display
const blob = new Blob([
Uint8Array.from(atob(result.data), c => c.charCodeAt(0))
], { type: result.mimeType });
const imageUrl = URL.createObjectURL(blob);
setGeneratedImage(imageUrl);
// Optionally trigger download
const link = document.createElement('a');
link.href = imageUrl;
link.download = `generated_${Date.now()}.png`;
link.click();
} catch (error) {
console.error('Generation failed:', error);
}
};
return (
<div className="web-image-wizard">
<div className="config-section">
<input
type="url"
placeholder="API Endpoint"
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
/>
<input
type="password"
placeholder="API Key (optional)"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</div>
{/* Rest of UI */}
</div>
);
}
```
**Libraries**:
- **Backend**: `hono`, `hono/cors`, `@google/generative-ai`, `zod`
- **Frontend**: React/Vue/Svelte, standard web APIs
- **Deployment**: Bun, Node.js, or serverless (Vercel, Netlify Functions)
## Configuration Schema Extension
```ts
// shared/config/imageConfig.ts
export interface ImageConfig {
// Existing CLI config
cli?: {
model?: string;
logLevel?: number;
};
// Mobile-specific config
mobile?: {
model?: string;
cacheDir?: string;
maxImageSize?: number;
};
// Web-specific config
web?: {
apiEndpoint: string;
tenantId?: string;
allowUserApiKeys?: boolean;
maxFileSize?: number;
};
// Shared Google AI config
google?: {
key?: string; // For CLI and mobile
defaultModel?: string;
};
}
```
## Platform Detection and Unified Interface
```ts
// shared/lib/unifiedImageClient.ts
export interface UnifiedImageGenerator {
createImage(prompt: string, options?: any): Promise<Buffer | ImageResult>;
editImage(prompt: string, images: string[] | File[], options?: any): Promise<Buffer | ImageResult>;
}
export async function createImageClient(config: ImageConfig): Promise<UnifiedImageGenerator> {
// Detect platform
if (typeof window === 'undefined') {
// Node.js CLI environment
const { CLIImageClient } = await import('./cliImageClient');
return new CLIImageClient(config.cli, config.google);
} else if ((window as any).__TAURI__) {
// Tauri mobile/desktop environment
const { MobileImageClient } = await import('./mobileImageClient');
return new MobileImageClient({ apiKey: config.google?.key || '' });
} else {
// Web browser environment
const { WebImageClient } = await import('./webImageClient');
return new WebImageClient({
endpoint: config.web?.apiEndpoint || '',
tenantId: config.web?.tenantId,
});
}
}
```
## Summary
This architecture provides:
1. **CLI Desktop**: Maintains current efficient Node.js-based approach
2. **Mobile**: Leverages Tauri HTTP plugin for direct API calls from TypeScript
3. **Web**: Secure backend API with configurable endpoints and tenant support
Each platform optimizes for its constraints while sharing common TypeScript interfaces and configuration schemas. The next step is to break this down into actionable implementation tasks.

View File

@ -0,0 +1,871 @@
# Multi-Platform Image Generation Architecture
## Overview
This document outlines the architecture for supporting image generation across multiple platforms:
1. **CLI Desktop** (current implementation) - Node.js CLI spawning Tauri GUI
2. **Mobile** (Android/iOS) - Standalone Tauri app with HTTP API calls
3. **Web App** - Browser-based application with configurable endpoints
## Current Architecture (CLI Desktop)
### Flow
```
CLI (images.ts) → Spawn Tauri Process → IPC Communication → Google AI API → Image Generation
```
### Key Components
- **CLI Entry**: `src/commands/images.ts` - Main command handler
- **Image Generation**: `src/lib/images-google.ts` - Google Generative AI integration
- **Tauri GUI**: `gui/tauri-app/` - Desktop GUI application
- **IPC Bridge**: Stdin/stdout communication between CLI and Tauri
### Current Implementation Details
```typescript
// CLI spawns Tauri process
const tauriProcess = spawn(guiAppPath, args, { stdio: ['pipe', 'pipe', 'pipe'] });
// Communication via JSON messages
const configResponse = {
cmd: 'forward_config_to_frontend',
prompt: argv.prompt || null,
dst: argv.dst || null,
apiKey: apiKey || null,
files: absoluteIncludes
};
```
## Platform-Specific Architectures
### 1. CLI Desktop (Current - Keep As-Is)
**Pros**:
- Direct file system access
- Native performance
- Existing implementation works well
**Architecture**:
```
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ CLI App │───▶│ Tauri GUI │───▶│ Google AI API │
│ (images.ts) │ │ (Rust) │ │ (Direct) │
└─────────────┘ └──────────────┘ └─────────────────┘
```
### 2. Mobile (Android/iOS) - Standalone Tauri
**Challenge**: No CLI spawning capability on mobile
**Solution**: Standalone Tauri app with HTTP client for API calls
**Architecture**:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Tauri App │───▶│ HTTP Client │───▶│ Google AI API │
│ (Standalone) │ │ (tauri-plugin- │ │ (via HTTP) │
│ │ │ http) │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
**Implementation Strategy**:
#### Option A: TypeScript Frontend HTTP (Recommended)
```typescript
// src/lib/images-mobile.ts
import { tauriApi } from '../gui/tauri-app/src/lib/tauriApi';
export class MobileImageGenerator {
private apiKey: string;
private baseUrl = 'https://generativelanguage.googleapis.com/v1beta';
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async createImage(prompt: string): Promise<Buffer> {
const response = await tauriApi.fetch(`${this.baseUrl}/models/gemini-2.5-flash-image-preview:generateContent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
contents: [{
parts: [{ text: prompt }]
}]
})
});
const data = await response.json();
const imageData = data.candidates[0].content.parts[0].inlineData.data;
return Buffer.from(imageData, 'base64');
}
async editImage(prompt: string, imageFiles: File[]): Promise<Buffer> {
const parts = [];
// Add image parts
for (const file of imageFiles) {
const arrayBuffer = await file.arrayBuffer();
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
parts.push({
inlineData: {
mimeType: file.type,
data: base64
}
});
}
// Add text prompt
parts.push({ text: prompt });
const response = await tauriApi.fetch(`${this.baseUrl}/models/gemini-2.5-flash-image-preview:generateContent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
contents: [{ parts }]
})
});
const data = await response.json();
const imageData = data.candidates[0].content.parts[0].inlineData.data;
return Buffer.from(imageData, 'base64');
}
}
```
#### Mobile-Specific Tauri Configuration
```json
// gui/tauri-app/src-tauri/tauri.conf.json (mobile additions)
{
"plugins": {
"http": {
"all": true,
"request": true,
"scope": [
"https://generativelanguage.googleapis.com/**"
]
}
},
"security": {
"csp": {
"default-src": "'self'",
"connect-src": "'self' https://generativelanguage.googleapis.com"
}
}
}
```
### 3. Web App - Browser-Based with Configurable Endpoints
**Challenge**: CORS restrictions, no direct Google AI API access
**Solution**: Backend API server (Hono) + configurable endpoints
**Architecture**:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Web App │───▶│ Backend API │───▶│ Google AI API │
│ (React/TS) │ │ (Hono.js) │ │ (Server) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
#### Backend API Server (Hono.js)
```typescript
// src/web/image-api-server.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { GoogleGenerativeAI } from '@google/generative-ai';
const app = new Hono();
app.use('/*', cors({
origin: ['http://localhost:3000', 'https://your-domain.com'],
allowHeaders: ['Content-Type', 'Authorization'],
allowMethods: ['POST', 'GET', 'OPTIONS'],
}));
interface ImageRequest {
prompt: string;
images?: Array<{
data: string; // base64
mimeType: string;
}>;
apiKey: string;
model?: string;
}
app.post('/api/images/create', async (c) => {
try {
const { prompt, apiKey, model = 'gemini-2.5-flash-image-preview' }: ImageRequest = await c.req.json();
const genAI = new GoogleGenerativeAI(apiKey);
const genModel = genAI.getGenerativeModel({ model });
const result = await genModel.generateContent(prompt);
const response = result.response;
if (!response.candidates?.[0]?.content?.parts) {
throw new Error('No image generated');
}
const imageData = response.candidates[0].content.parts.find(part =>
'inlineData' in part
)?.inlineData;
if (!imageData) {
throw new Error('No image data in response');
}
return c.json({
success: true,
image: {
data: imageData.data,
mimeType: imageData.mimeType
}
});
} catch (error) {
return c.json({
success: false,
error: error.message
}, 500);
}
});
app.post('/api/images/edit', async (c) => {
try {
const { prompt, images, apiKey, model = 'gemini-2.5-flash-image-preview' }: ImageRequest = await c.req.json();
const genAI = new GoogleGenerativeAI(apiKey);
const genModel = genAI.getGenerativeModel({ model });
const parts = [];
// Add image parts
if (images) {
for (const img of images) {
parts.push({
inlineData: {
mimeType: img.mimeType,
data: img.data
}
});
}
}
// Add text prompt
parts.push({ text: prompt });
const result = await genModel.generateContent(parts);
const response = result.response;
if (!response.candidates?.[0]?.content?.parts) {
throw new Error('No image generated');
}
const imageData = response.candidates[0].content.parts.find(part =>
'inlineData' in part
)?.inlineData;
if (!imageData) {
throw new Error('No image data in response');
}
return c.json({
success: true,
image: {
data: imageData.data,
mimeType: imageData.mimeType
}
});
} catch (error) {
return c.json({
success: false,
error: error.message
}, 500);
}
});
export default app;
// Server startup
if (import.meta.main) {
const port = parseInt(process.env.PORT || '3001');
console.log(`🚀 Image API server starting on port ${port}`);
Bun.serve({
fetch: app.fetch,
port,
});
}
```
#### Web Frontend Client
```typescript
// src/web/image-client.ts
export interface WebImageConfig {
apiEndpoint: string; // e.g., 'http://localhost:3001' or 'https://api.yourservice.com'
apiKey: string;
}
export class WebImageGenerator {
private config: WebImageConfig;
constructor(config: WebImageConfig) {
this.config = config;
}
async createImage(prompt: string): Promise<Blob> {
const response = await fetch(`${this.config.apiEndpoint}/api/images/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt,
apiKey: this.config.apiKey
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Unknown error');
}
// Convert base64 to blob
const binaryString = atob(data.image.data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new Blob([bytes], { type: data.image.mimeType });
}
async editImage(prompt: string, imageFiles: File[]): Promise<Blob> {
const images = [];
for (const file of imageFiles) {
const arrayBuffer = await file.arrayBuffer();
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
images.push({
data: base64,
mimeType: file.type
});
}
const response = await fetch(`${this.config.apiEndpoint}/api/images/edit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt,
images,
apiKey: this.config.apiKey
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Unknown error');
}
// Convert base64 to blob
const binaryString = atob(data.image.data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new Blob([bytes], { type: data.image.mimeType });
}
}
```
#### Web App Configuration
```typescript
// src/web/config.ts
export interface PlatformConfig {
platform: 'cli' | 'mobile' | 'web';
// Web-specific config
web?: {
apiEndpoint: string;
corsEnabled: boolean;
allowedOrigins: string[];
};
// Mobile-specific config
mobile?: {
directApiAccess: boolean;
cacheImages: boolean;
maxImageSize: number;
};
// CLI-specific config (existing)
cli?: {
guiEnabled: boolean;
tempDir: string;
};
}
export const getDefaultConfig = (): PlatformConfig => {
// Detect platform
const isTauri = !!(window as any).__TAURI__;
const isMobile = isTauri && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const isWeb = !isTauri;
if (isMobile) {
return {
platform: 'mobile',
mobile: {
directApiAccess: true,
cacheImages: true,
maxImageSize: 5 * 1024 * 1024 // 5MB
}
};
} else if (isWeb) {
return {
platform: 'web',
web: {
apiEndpoint: process.env.REACT_APP_API_ENDPOINT || 'http://localhost:3001',
corsEnabled: true,
allowedOrigins: ['http://localhost:3000']
}
};
} else {
return {
platform: 'cli',
cli: {
guiEnabled: true,
tempDir: process.env.TEMP || '/tmp'
}
};
}
};
```
## Platform Detection & Unified Interface
```typescript
// src/lib/image-generator-factory.ts
import { WebImageGenerator } from '../web/image-client';
import { MobileImageGenerator } from './images-mobile';
import { createImage, editImage } from './images-google'; // CLI version
import { getDefaultConfig, PlatformConfig } from '../web/config';
export interface UnifiedImageGenerator {
createImage(prompt: string): Promise<Buffer | Blob>;
editImage(prompt: string, images: File[] | string[]): Promise<Buffer | Blob>;
}
export class ImageGeneratorFactory {
static create(config?: PlatformConfig): UnifiedImageGenerator {
const platformConfig = config || getDefaultConfig();
switch (platformConfig.platform) {
case 'web':
return new WebImageGeneratorAdapter(
new WebImageGenerator({
apiEndpoint: platformConfig.web!.apiEndpoint,
apiKey: '' // Will be set later
})
);
case 'mobile':
return new MobileImageGeneratorAdapter(
new MobileImageGenerator('') // API key set later
);
case 'cli':
default:
return new CLIImageGeneratorAdapter();
}
}
}
// Adapters to normalize the interface
class WebImageGeneratorAdapter implements UnifiedImageGenerator {
constructor(private generator: WebImageGenerator) {}
async createImage(prompt: string): Promise<Blob> {
return this.generator.createImage(prompt);
}
async editImage(prompt: string, images: File[]): Promise<Blob> {
return this.generator.editImage(prompt, images);
}
}
class MobileImageGeneratorAdapter implements UnifiedImageGenerator {
constructor(private generator: MobileImageGenerator) {}
async createImage(prompt: string): Promise<Buffer> {
return this.generator.createImage(prompt);
}
async editImage(prompt: string, images: File[]): Promise<Buffer> {
return this.generator.editImage(prompt, images);
}
}
class CLIImageGeneratorAdapter implements UnifiedImageGenerator {
async createImage(prompt: string): Promise<Buffer> {
// Use existing CLI implementation
return createImage(prompt, {} as any) as Promise<Buffer>;
}
async editImage(prompt: string, images: string[]): Promise<Buffer> {
// Use existing CLI implementation
return editImage(prompt, images, {} as any) as Promise<Buffer>;
}
}
```
## Required Dependencies
### CLI (Existing)
```json
{
"dependencies": {
"@google/generative-ai": "^0.21.0",
"tauri": "^2.0.0"
}
}
```
### Mobile (Tauri)
```json
{
"dependencies": {
"@tauri-apps/plugin-http": "^2.0.0",
"@tauri-apps/api": "^2.0.0"
}
}
```
### Web Backend (Hono)
```json
{
"dependencies": {
"hono": "^4.0.0",
"@google/generative-ai": "^0.21.0",
"bun": "^1.0.0"
}
}
```
### Web Frontend
```json
{
"dependencies": {
"react": "^18.0.0",
"@types/react": "^18.0.0"
}
}
```
## Deployment Strategies
### CLI Desktop
- **Current**: Nexe bundling with Tauri executable
- **Distribution**: GitHub releases with platform-specific binaries
### Mobile
- **Android**: APK via Tauri build system
- **iOS**: App Store via Tauri + Xcode
- **Distribution**: App stores or direct APK/IPA
### Web App
- **Frontend**: Static hosting (Vercel, Netlify, Cloudflare Pages)
- **Backend**:
- **Option 1**: Bun/Node.js server (Railway, Render, DigitalOcean)
- **Option 2**: Serverless functions (Vercel Functions, Cloudflare Workers)
- **Option 3**: Docker containers (any cloud provider)
## Migration Path
### Phase 1: Maintain CLI (Current)
- Keep existing CLI implementation
- No changes to current workflow
### Phase 2: Add Mobile Support
- Implement `MobileImageGenerator` class
- Add HTTP client configuration
- Test on Android/iOS simulators
### Phase 3: Add Web Support
- Create Hono backend API
- Implement web frontend client
- Add configuration management
### Phase 4: Unified Interface
- Implement factory pattern
- Add platform detection
- Create unified API surface
## Security Considerations
### API Key Management
- **CLI**: Local config files, environment variables
- **Mobile**: Secure storage via Tauri
- **Web**: Backend-only, never expose to frontend
### CORS & CSP
- **Web**: Strict CORS policies, CSP headers
- **Mobile**: Tauri security policies
- **CLI**: Not applicable (local execution)
### Rate Limiting
- **All Platforms**: Implement client-side rate limiting
- **Web**: Server-side rate limiting per IP/user
## Testing Strategy
### Unit Tests
```typescript
// tests/image-generator.test.ts
import { ImageGeneratorFactory } from '../src/lib/image-generator-factory';
describe('ImageGenerator', () => {
test('CLI platform creates correct generator', () => {
const generator = ImageGeneratorFactory.create({ platform: 'cli' });
expect(generator).toBeInstanceOf(CLIImageGeneratorAdapter);
});
test('Web platform creates correct generator', () => {
const generator = ImageGeneratorFactory.create({
platform: 'web',
web: { apiEndpoint: 'http://test.com', corsEnabled: true, allowedOrigins: [] }
});
expect(generator).toBeInstanceOf(WebImageGeneratorAdapter);
});
});
```
### Integration Tests
- **CLI**: Test Tauri process spawning
- **Mobile**: Test HTTP API calls with mock server
- **Web**: Test full frontend-backend flow
## Performance Considerations
### Image Handling
- **CLI**: Direct file system access (fastest)
- **Mobile**: In-memory processing, consider caching
- **Web**: Base64 encoding overhead, consider streaming
### Network Optimization
- **Mobile**: Implement request queuing, retry logic
- **Web**: Connection pooling, request batching
### Memory Management
- **All Platforms**: Stream large images, avoid loading entire files into memory
- **Mobile**: Implement image compression before API calls
---
## Implementation Todo List
### Phase 1: Mobile Platform Support (Priority: High)
#### 1.1 Mobile HTTP Client Implementation
- [ ] **Create mobile image generator class** (`src/lib/images-mobile.ts`)
- [ ] Implement `MobileImageGenerator` class with HTTP client
- [ ] Add TypeScript fetch wrapper using `tauriApi.fetch`
- [ ] Handle Google AI API authentication and requests
- [ ] Add error handling for network failures and API errors
- [ ] Implement image creation endpoint integration
- [ ] Implement image editing endpoint integration
#### 1.2 Mobile Tauri Configuration
- [ ] **Update Tauri config for mobile HTTP access**
- [ ] Add `tauri-plugin-http` to dependencies
- [ ] Configure HTTP scope for Google AI API endpoints
- [ ] Update CSP policies for external API access
- [ ] Test HTTP plugin functionality on mobile simulators
#### 1.3 Mobile Platform Detection
- [ ] **Add mobile platform detection logic**
- [ ] Detect Android/iOS in Tauri environment
- [ ] Create mobile-specific configuration defaults
- [ ] Add mobile UI adaptations (touch-friendly controls)
- [ ] Implement mobile-specific file handling
### Phase 2: Web Platform Support (Priority: Medium)
#### 2.1 Backend API Server (Hono)
- [ ] **Create Hono.js backend server** (`src/web/image-api-server.ts`)
- [ ] Set up Hono app with CORS middleware
- [ ] Implement `/api/images/create` endpoint
- [ ] Implement `/api/images/edit` endpoint
- [ ] Add request validation and error handling
- [ ] Add rate limiting middleware
- [ ] Add API key validation
- [ ] Add logging and monitoring
#### 2.2 Web Frontend Client
- [ ] **Create web image client** (`src/web/image-client.ts`)
- [ ] Implement `WebImageGenerator` class
- [ ] Add fetch-based API communication
- [ ] Handle file uploads and base64 conversion
- [ ] Add progress tracking for large requests
- [ ] Implement retry logic for failed requests
#### 2.3 Web Configuration Management
- [ ] **Add web-specific configuration** (`src/web/config.ts`)
- [ ] Create configurable API endpoints
- [ ] Add environment variable support
- [ ] Implement CORS configuration
- [ ] Add deployment-specific settings
### Phase 3: Unified Interface (Priority: Medium)
#### 3.1 Factory Pattern Implementation
- [ ] **Create image generator factory** (`src/lib/image-generator-factory.ts`)
- [ ] Implement platform detection logic
- [ ] Create unified interface for all platforms
- [ ] Add adapter classes for each platform
- [ ] Implement configuration-based generator selection
#### 3.2 Platform Adapters
- [ ] **Create platform adapters**
- [ ] `CLIImageGeneratorAdapter` - wrap existing CLI implementation
- [ ] `MobileImageGeneratorAdapter` - wrap mobile HTTP client
- [ ] `WebImageGeneratorAdapter` - wrap web API client
- [ ] Normalize return types (Buffer vs Blob handling)
### Phase 4: Testing & Quality Assurance (Priority: High)
#### 4.1 Unit Tests
- [ ] **Write comprehensive unit tests**
- [ ] Test factory pattern and platform detection
- [ ] Test each adapter class individually
- [ ] Mock HTTP requests for mobile/web testing
- [ ] Test error handling scenarios
- [ ] Test configuration loading and validation
#### 4.2 Integration Tests
- [ ] **Create integration test suite**
- [ ] Test CLI-to-Tauri communication (existing)
- [ ] Test mobile HTTP API calls with mock server
- [ ] Test web frontend-backend communication
- [ ] Test cross-platform image format compatibility
- [ ] Test API key management across platforms
#### 4.3 Platform-Specific Testing
- [ ] **Mobile testing**
- [ ] Test on Android emulator/device
- [ ] Test on iOS simulator/device
- [ ] Test network connectivity edge cases
- [ ] Test file system permissions
- [ ] Performance testing with large images
- [ ] **Web testing**
- [ ] Test CORS configuration
- [ ] Test different browsers (Chrome, Firefox, Safari)
- [ ] Test file upload limits
- [ ] Test API server deployment
- [ ] Load testing for concurrent requests
### Phase 5: Deployment & Distribution (Priority: Low)
#### 5.1 Mobile Deployment
- [ ] **Set up mobile build pipeline**
- [ ] Configure Android build (APK/AAB)
- [ ] Configure iOS build (IPA)
- [ ] Set up code signing for both platforms
- [ ] Create app store metadata and screenshots
- [ ] Test installation and updates
#### 5.2 Web Deployment
- [ ] **Deploy web application**
- [ ] Set up frontend hosting (Vercel/Netlify)
- [ ] Deploy backend API server
- [ ] Configure domain and SSL certificates
- [ ] Set up monitoring and logging
- [ ] Configure CDN for static assets
#### 5.3 Documentation & Guides
- [ ] **Create user documentation**
- [ ] Platform-specific installation guides
- [ ] API configuration instructions
- [ ] Troubleshooting guides
- [ ] Performance optimization tips
- [ ] Security best practices
### Phase 6: Advanced Features (Priority: Low)
#### 6.1 Performance Optimizations
- [ ] **Implement performance improvements**
- [ ] Image compression before API calls
- [ ] Request batching for multiple images
- [ ] Caching layer for repeated requests
- [ ] Progressive image loading
- [ ] Background processing for large operations
#### 6.2 Enhanced Security
- [ ] **Add security enhancements**
- [ ] API key encryption at rest
- [ ] Request signing for web API
- [ ] Rate limiting per user/session
- [ ] Input sanitization and validation
- [ ] Audit logging for API calls
#### 6.3 User Experience Improvements
- [ ] **Enhance user interface**
- [ ] Drag-and-drop file uploads
- [ ] Real-time preview of edits
- [ ] Batch processing interface
- [ ] History and favorites management
- [ ] Keyboard shortcuts and accessibility
---
## Estimated Timeline
- **Phase 1 (Mobile)**: 2-3 weeks
- **Phase 2 (Web)**: 2-3 weeks
- **Phase 3 (Unified)**: 1 week
- **Phase 4 (Testing)**: 2 weeks
- **Phase 5 (Deployment)**: 1 week
- **Phase 6 (Advanced)**: 3-4 weeks
**Total Estimated Time**: 11-16 weeks
## Dependencies & Prerequisites
### Required Skills
- TypeScript/JavaScript development
- Tauri framework knowledge
- React/frontend development
- Hono.js/backend API development
- Mobile app development (Android/iOS)
- Google AI API integration
### Required Tools
- Node.js 18+
- Rust toolchain
- Android Studio (for Android builds)
- Xcode (for iOS builds)
- Bun runtime (for Hono server)
### External Services
- Google AI API access and billing
- Cloud hosting for web backend
- App store developer accounts (mobile)
- Domain registration (web)

View File

@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Isolation Secure Script</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
window.__TAURI_ISOLATION_HOOK__ = (payload) => {
return payload
}

View File

@ -17,6 +17,41 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "ahash"
version = "0.7.8"
@ -749,6 +784,16 @@ dependencies = [
"windows-link 0.2.0",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clap"
version = "4.5.48"
@ -998,6 +1043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"typenum",
]
@ -1038,6 +1084,15 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "darling"
version = "0.21.3"
@ -1853,6 +1908,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "gif"
version = "0.13.3"
@ -2449,6 +2514,15 @@ dependencies = [
"cfb",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "interpolate_name"
version = "0.2.4"
@ -3371,6 +3445,12 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "open"
version = "5.3.2"
@ -3773,6 +3853,18 @@ dependencies = [
"windows-sys 0.61.0",
]
[[package]]
name = "polyval"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "potential_utf"
version = "0.1.3"
@ -5298,6 +5390,7 @@ dependencies = [
"tray-icon",
"url",
"urlpattern",
"uuid",
"webkit2gtk",
"webview2-com",
"window-vibrancy",
@ -5355,6 +5448,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"tauri-codegen",
"tauri-utils",
"tauri-winres",
"toml 0.9.7",
@ -5744,11 +5838,13 @@ version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
dependencies = [
"aes-gcm",
"anyhow",
"brotli",
"cargo_metadata",
"ctor",
"dunce",
"getrandom 0.3.3",
"glob",
"html5ever",
"http",
@ -5767,6 +5863,7 @@ dependencies = [
"serde-untagged",
"serde_json",
"serde_with",
"serialize-to-javascript",
"swift-rs",
"thiserror 2.0.16",
"toml 0.9.7",
@ -6295,6 +6392,16 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.9.0"

View File

@ -15,10 +15,10 @@ name = "tauri_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
tauri-build = { version = "2", features = ["isolation"] }
[dependencies]
tauri = { version = "2", features = ["protocol-asset", "devtools"] }
tauri = { version = "2", features = ["isolation", "macos-private-api", "protocol-asset", "devtools"] }
tauri-plugin-opener = "2.5.0"
tauri-plugin-dialog = "2.4.0"
tauri-plugin-fs = "2.0.0"

View File

@ -7,4 +7,5 @@
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="status_bar_blue">#FF2196F3</color>
</resources>

View File

@ -10,6 +10,8 @@
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"macOSPrivateApi": true,
"windows": [
{
"title": "tauri-app",
@ -18,8 +20,15 @@
}
],
"security": {
"pattern": {
"use": "isolation",
"options": {
"dir": "../isolation-dist/"
}
},
"csp": {
"default-src": "'self' customprotocol: asset:",
"script-src": "'self' 'unsafe-inline'",
"connect-src": "ipc: http://ipc.localhost",
"font-src": ["https://fonts.gstatic.com"],
"img-src": "'self' asset: http://asset.localhost blob: data:",
@ -44,6 +53,9 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
],
"iOS": {
"minimumSystemVersion": "14.0"
}
}
}

View File

@ -41,32 +41,34 @@ function App() {
};
return (
<Router>
<Routes>
<Route
path="/"
element={
<ImageWizard
apiKey={apiKey}
setApiKey={setApiKey}
isDarkMode={isDarkMode}
toggleTheme={toggleTheme}
/>
}
/>
<Route
path="/settings"
element={
<Settings
apiKey={apiKey}
setApiKey={setApiKey}
isDarkMode={isDarkMode}
toggleTheme={toggleTheme}
/>
}
/>
</Routes>
</Router>
<div className="flex h-screen w-screen overflow-hidden">
<Router>
<Routes>
<Route
path="/"
element={
<ImageWizard
apiKey={apiKey}
setApiKey={setApiKey}
isDarkMode={isDarkMode}
toggleTheme={toggleTheme}
/>
}
/>
<Route
path="/settings"
element={
<Settings
apiKey={apiKey}
setApiKey={setApiKey}
isDarkMode={isDarkMode}
toggleTheme={toggleTheme}
/>
}
/>
</Routes>
</Router>
</div>
);
}

View File

@ -1,81 +1,81 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
interface HeaderProps {
showDebugPanel: boolean;
setShowDebugPanel: (show: boolean) => void;
isDarkMode: boolean;
toggleTheme: () => void;
}
const Header: React.FC<HeaderProps> = ({
showDebugPanel,
setShowDebugPanel,
isDarkMode,
toggleTheme,
}) => {
const navigate = useNavigate();
return (
<div className="mb-8 space-y-4">
{/* Title on its own row */}
<div className="text-center">
<h1 className="text-2xl md:text-3xl font-bold accent-text drop-shadow-sm">Image Wizard</h1>
</div>
{/* Controls row - single row layout */}
<div className="flex justify-center sm:justify-end items-center gap-2">
{/* Button group - single row */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* Debug Panel Toggle */}
<button
onClick={() => setShowDebugPanel(!showDebugPanel)}
className="glass-button p-2 sm:p-3 rounded-xl hover:shadow-lg transition-all duration-300"
title="Toggle Debug Panel"
>
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-slate-700 dark:text-slate-300" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z"
clipRule="evenodd"
/>
</svg>
</button>
{/* Theme Toggle */}
<button
onClick={toggleTheme}
className="glass-button p-2 sm:p-3 rounded-xl hover:shadow-lg transition-all duration-300"
title={isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
>
{isDarkMode ? (
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-slate-300" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
</svg>
) : (
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-slate-700" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
)}
</button>
</div>
{/* Settings Button */}
<button
onClick={() => navigate('/settings')}
className="glass-button p-2 sm:p-3 rounded-xl hover:shadow-lg transition-all duration-300"
title="Settings"
>
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-slate-700 dark:text-slate-300" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
);
};
export default Header;
import React from 'react';
import { useNavigate } from 'react-router-dom';
interface HeaderProps {
showDebugPanel: boolean;
setShowDebugPanel: (show: boolean) => void;
isDarkMode: boolean;
toggleTheme: () => void;
}
const Header: React.FC<HeaderProps> = ({
showDebugPanel,
setShowDebugPanel,
isDarkMode,
toggleTheme,
}) => {
const navigate = useNavigate();
return (
<div className="mb-8 space-y-4">
{/* Title on its own row */}
<div className="text-center">
<h1 className="text-2xl md:text-3xl font-bold accent-text drop-shadow-sm">Image Wizard</h1>
</div>
{/* Controls row - single row layout */}
<div className="flex justify-center sm:justify-end items-center gap-2">
{/* Button group - single row */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* Debug Panel Toggle */}
<button
onClick={() => setShowDebugPanel(!showDebugPanel)}
className="glass-button p-2 sm:p-3 rounded-xl hover:shadow-lg transition-all duration-300"
title="Toggle Debug Panel"
>
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-slate-700 dark:text-slate-300" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z"
clipRule="evenodd"
/>
</svg>
</button>
{/* Theme Toggle */}
<button
onClick={toggleTheme}
className="glass-button p-2 sm:p-3 rounded-xl hover:shadow-lg transition-all duration-300"
title={isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
>
{isDarkMode ? (
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-slate-300" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
</svg>
) : (
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-slate-700" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
)}
</button>
</div>
{/* Settings Button */}
<button
onClick={() => navigate('/settings')}
className="glass-button p-2 sm:p-3 rounded-xl hover:shadow-lg transition-all duration-300"
title="Settings"
>
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-slate-700 dark:text-slate-300" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
);
};
export default Header;

View File

@ -718,14 +718,14 @@ const ImageWizard: React.FC<ImageWizardProps> = ({
}
return (
<main className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-cyan-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex flex-col items-center p-6 transition-colors duration-500">
<main className="h-full w-full bg-gradient-to-br from-indigo-50 via-white to-cyan-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex flex-col items-center overflow-hidden transition-colors duration-500">
{/* Background decoration */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-gradient-to-br from-indigo-200/30 to-purple-200/30 dark:from-indigo-500/20 dark:to-purple-500/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-gradient-to-tr from-cyan-200/30 to-blue-200/30 dark:from-cyan-500/20 dark:to-blue-500/20 rounded-full blur-3xl"></div>
</div>
<div className="w-full max-w-4xl relative z-10 mt-8">
<div className="w-full max-w-4xl relative z-10 h-full flex flex-col overflow-hidden p-6">
<Header
showDebugPanel={showDebugPanel}
setShowDebugPanel={setShowDebugPanel}
@ -767,7 +767,8 @@ const ImageWizard: React.FC<ImageWizardProps> = ({
</div>
</div>
)}
<PromptForm
<div className="flex-1 overflow-y-auto">
<PromptForm
prompt={prompt}
setPrompt={setPrompt}
dst={dst}
@ -809,6 +810,7 @@ const ImageWizard: React.FC<ImageWizardProps> = ({
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
/>
</div>
{/* Debug Panel */}
{showDebugPanel && (

View File

@ -27,6 +27,7 @@
"gui:build": "cd ./gui/tauri-app/ && npm run build",
"gui:dist": "cd ./gui/tauri-app/ && sh ./scripts/build.sh",
"gui:dist:android": "cd ./gui/tauri-app/ && sh ./scripts/build-android.sh",
"gui:android:dev": "cd ./gui/tauri-app/ && npm run tauri android dev",
"register-commands": "pm-cli register-commands --config=salamand.json --group=kbot",
"test": "vitest run",
"test:basic": "vitest run tests/unit/basic.test.ts",

20
packages/kbot/src/top.txt Normal file
View File

@ -0,0 +1,20 @@
Hey everyone,
It came to our attention that once again, open source folks are being tricked with false statements and promises. This is not the first time, and over time most of the claims have been debunked as scams, also by ex-PreciousPlastic team members who left shocked.
We're currently compiling a dossier which covers most of the ongoing fraud, targeting young and vulnerable people, apparently enticed to collect grants for often overpriced and immature machines. We know dozens who lost their savings and will have to pay a bitter price for a very long time.
Currently involved and actively profiting from this scam are MadPlastic, Sustainable Design Studio, Dave Hakkens & his gang, and unfortunately The Flipflopi. Let me remind you that the v4 team introduced not just taxes and restrictions for everyone else but also kicked long-established vendors from the Bazaar with bogus reasons. During the same time, African NGOs have been charged systematically horrific prices.
The complete report will also be sent to all FabLabs, news outlets, and related organizations. In the meantime, we're doing our best to warn newcomers; hundreds by now.

BIN
packages/kbot/status.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB