feat: enhance AI image generation and prompt handling in canvas components
- Introduced shimmer animation for loading states in AI image nodes. - Updated prompt node to handle image generation with improved error handling and user feedback. - Refactored AI image node to manage generation status and display loading indicators. - Enhanced data handling in canvas components to include canvasId for better context management. - Improved status message handling in Convex mutations for clearer user feedback.
This commit is contained in:
112
convex/openrouter.ts
Normal file
112
convex/openrouter.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
||||
|
||||
export interface OpenRouterModel {
|
||||
id: string;
|
||||
name: string;
|
||||
tier: "budget" | "standard" | "premium";
|
||||
estimatedCostPerImage: number; // in Euro-Cent (for credit reservation)
|
||||
}
|
||||
|
||||
// Phase 1: Gemini 2.5 Flash Image only.
|
||||
// Add more models here in Phase 2 when the model selector UI is built.
|
||||
export const IMAGE_MODELS: Record<string, OpenRouterModel> = {
|
||||
"google/gemini-2.5-flash-image": {
|
||||
id: "google/gemini-2.5-flash-image",
|
||||
name: "Gemini 2.5 Flash",
|
||||
tier: "standard",
|
||||
estimatedCostPerImage: 4, // ~€0.04 in Euro-Cent
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_IMAGE_MODEL = "google/gemini-2.5-flash-image";
|
||||
|
||||
export interface GenerateImageParams {
|
||||
prompt: string;
|
||||
referenceImageUrl?: string; // optional image-to-image input
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface OpenRouterImageResponse {
|
||||
imageBase64: string; // base64-encoded PNG/JPEG
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the OpenRouter API to generate an image.
|
||||
* Uses the chat/completions endpoint with a vision-capable model that returns
|
||||
* an inline image in the response (base64).
|
||||
*
|
||||
* Must be called from a Convex Action (has access to fetch + env vars).
|
||||
*/
|
||||
export async function generateImageViaOpenRouter(
|
||||
apiKey: string,
|
||||
params: GenerateImageParams
|
||||
): Promise<OpenRouterImageResponse> {
|
||||
const modelId = params.model ?? DEFAULT_IMAGE_MODEL;
|
||||
|
||||
// Build message content — text prompt, optionally with a reference image
|
||||
const userContent: object[] = [];
|
||||
|
||||
if (params.referenceImageUrl) {
|
||||
userContent.push({
|
||||
type: "image_url",
|
||||
image_url: { url: params.referenceImageUrl },
|
||||
});
|
||||
}
|
||||
|
||||
userContent.push({
|
||||
type: "text",
|
||||
text: params.prompt,
|
||||
});
|
||||
|
||||
const body = {
|
||||
model: modelId,
|
||||
modalities: ["image", "text"],
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: userContent,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await fetch(`${OPENROUTER_BASE_URL}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://app.lemonspace.io",
|
||||
"X-Title": "LemonSpace",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`OpenRouter API error ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// OpenRouter returns generated images in message.images (separate from content)
|
||||
const images = data?.choices?.[0]?.message?.images;
|
||||
|
||||
if (!images || images.length === 0) {
|
||||
throw new Error("No image found in OpenRouter response");
|
||||
}
|
||||
|
||||
const imageUrl = images[0]?.image_url?.url;
|
||||
if (!imageUrl) {
|
||||
throw new Error("Image block missing image_url.url");
|
||||
}
|
||||
|
||||
// The URL is a data URI: "data:image/png;base64,<data>"
|
||||
const dataUri: string = imageUrl;
|
||||
const [meta, base64Data] = dataUri.split(",");
|
||||
const mimeType = meta.replace("data:", "").replace(";base64", "");
|
||||
|
||||
return {
|
||||
imageBase64: base64Data,
|
||||
mimeType: mimeType || "image/png",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user