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:
109
convex/ai.ts
Normal file
109
convex/ai.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { v } from "convex/values";
|
||||
import { action } from "./_generated/server";
|
||||
import { api } from "./_generated/api";
|
||||
import {
|
||||
generateImageViaOpenRouter,
|
||||
DEFAULT_IMAGE_MODEL,
|
||||
IMAGE_MODELS,
|
||||
} from "./openrouter";
|
||||
|
||||
export const generateImage = action({
|
||||
args: {
|
||||
canvasId: v.id("canvases"),
|
||||
nodeId: v.id("nodes"),
|
||||
prompt: v.string(),
|
||||
referenceStorageId: v.optional(v.id("_storage")),
|
||||
model: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("OPENROUTER_API_KEY is not set");
|
||||
}
|
||||
|
||||
const modelId = args.model ?? DEFAULT_IMAGE_MODEL;
|
||||
const modelConfig = IMAGE_MODELS[modelId];
|
||||
if (!modelConfig) {
|
||||
throw new Error(`Unknown model: ${modelId}`);
|
||||
}
|
||||
|
||||
if (!(await ctx.runQuery(api.auth.getCurrentUser, {}))) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const reservationId = await ctx.runMutation(api.credits.reserve, {
|
||||
estimatedCost: modelConfig.estimatedCostPerImage,
|
||||
description: `Bildgenerierung — ${modelConfig.name}`,
|
||||
model: modelId,
|
||||
nodeId: args.nodeId,
|
||||
canvasId: args.canvasId,
|
||||
});
|
||||
|
||||
await ctx.runMutation(api.nodes.updateStatus, {
|
||||
nodeId: args.nodeId,
|
||||
status: "executing",
|
||||
});
|
||||
|
||||
try {
|
||||
let referenceImageUrl: string | undefined;
|
||||
if (args.referenceStorageId) {
|
||||
referenceImageUrl =
|
||||
(await ctx.storage.getUrl(args.referenceStorageId)) ?? undefined;
|
||||
}
|
||||
|
||||
const result = await generateImageViaOpenRouter(apiKey, {
|
||||
prompt: args.prompt,
|
||||
referenceImageUrl,
|
||||
model: modelId,
|
||||
});
|
||||
|
||||
const binaryString = atob(result.imageBase64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
const blob = new Blob([bytes], { type: result.mimeType });
|
||||
const storageId = await ctx.storage.store(blob);
|
||||
|
||||
const existing = await ctx.runQuery(api.nodes.get, { nodeId: args.nodeId });
|
||||
if (!existing) throw new Error("Node not found");
|
||||
const prev = (existing.data ?? {}) as Record<string, unknown>;
|
||||
|
||||
await ctx.runMutation(api.nodes.updateData, {
|
||||
nodeId: args.nodeId,
|
||||
data: {
|
||||
...prev,
|
||||
storageId,
|
||||
prompt: args.prompt,
|
||||
model: modelId,
|
||||
modelTier: modelConfig.tier,
|
||||
generatedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
await ctx.runMutation(api.nodes.updateStatus, {
|
||||
nodeId: args.nodeId,
|
||||
status: "done",
|
||||
});
|
||||
|
||||
await ctx.runMutation(api.credits.commit, {
|
||||
transactionId: reservationId,
|
||||
actualCost: modelConfig.estimatedCostPerImage,
|
||||
});
|
||||
} catch (error) {
|
||||
await ctx.runMutation(api.credits.release, {
|
||||
transactionId: reservationId,
|
||||
});
|
||||
|
||||
await ctx.runMutation(api.nodes.updateStatus, {
|
||||
nodeId: args.nodeId,
|
||||
status: "error",
|
||||
statusMessage:
|
||||
error instanceof Error ? error.message : "Generation failed",
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user