images interface
This commit is contained in:
parent
649c91ec35
commit
9284894589
135
packages/kbot/docs/images-tauri-5.md
Normal file
135
packages/kbot/docs/images-tauri-5.md
Normal 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 Tauri’s HTTP plugin to hit Google’s 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.
|
||||
|
||||
603
packages/kbot/docs/images-tauri-gem.md
Normal file
603
packages/kbot/docs/images-tauri-gem.md
Normal 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.
|
||||
871
packages/kbot/docs/images-tauri.md
Normal file
871
packages/kbot/docs/images-tauri.md
Normal 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)
|
||||
Binary file not shown.
10
packages/kbot/gui/tauri-app/isolation-dist/index.html
Normal file
10
packages/kbot/gui/tauri-app/isolation-dist/index.html
Normal 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>
|
||||
7
packages/kbot/gui/tauri-app/isolation-dist/index.js
Normal file
7
packages/kbot/gui/tauri-app/isolation-dist/index.js
Normal 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
|
||||
}
|
||||
107
packages/kbot/gui/tauri-app/src-tauri/Cargo.lock
generated
107
packages/kbot/gui/tauri-app/src-tauri/Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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
20
packages/kbot/src/top.txt
Normal 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
BIN
packages/kbot/status.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
Loading…
Reference in New Issue
Block a user