mono/packages/ui/src/lib/markdownImageTools.ts
2026-01-20 10:34:09 +01:00

238 lines
9.1 KiB
TypeScript

/**
* Markdown Image Tools
* Tools for generating and embedding images in markdown content
*/
import { z } from 'zod';
import { zodFunction } from '@/lib/openai';
import { createImage as createImageRouter } from '@/lib/image-router';
import { supabase } from '@/integrations/supabase/client';
type LogFunction = (level: 'info' | 'warn' | 'error' | 'debug', message: string, data?: any) => void;
const defaultLog: LogFunction = (level, message, data) => console.log(`[${level}] ${message}`, data);
/**
* Upload image data to temp-images bucket
*/
const uploadToTempBucket = async (
imageData: ArrayBuffer,
prompt: string,
userId: string,
addLog: LogFunction = defaultLog
): Promise<string | null> => {
try {
// Create filename with timestamp and user ID
const timestamp = Date.now();
const fileName = `${userId}/${timestamp}-${prompt.slice(0, 20).replace(/[^a-zA-Z0-9]/g, '-')}.png`;
// Convert ArrayBuffer to Uint8Array for upload
const uint8Array = new Uint8Array(imageData);
// Upload to temp-images bucket
const { data, error } = await supabase.storage
.from('temp-images')
.upload(fileName, uint8Array, {
contentType: 'image/png',
cacheControl: '3600', // Cache for 1 hour
});
if (error) {
addLog('error', 'Failed to upload to temp-images bucket:', error);
return null;
}
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('temp-images')
.getPublicUrl(fileName);
addLog('info', 'Image uploaded to temp bucket:', { fileName, publicUrl });
return publicUrl;
} catch (error) {
addLog('error', 'Error uploading to temp bucket:', error);
return null;
}
};
/**
* Tool: Generate Markdown Image
* Generates an image and returns markdown with embedded image
*/
export const generateMarkdownImageTool = (userId: string, addLog: LogFunction = defaultLog) =>
zodFunction({
name: 'generate_markdown_image',
description: 'Generate an image from a text prompt and return markdown code with the embedded image. The image will be uploaded to temporary storage and embedded using proper markdown image syntax.',
schema: z.object({
prompt: z.string().describe('The text prompt describing the image to generate'),
altText: z.string().optional().describe('Alt text for the image (defaults to prompt)'),
caption: z.string().optional().describe('Optional caption to display below the image'),
model: z.string().optional().describe('Image generation model in format "provider/model-name". Default: "google/gemini-3-pro-image-preview"'),
}),
function: async (args) => {
try {
addLog('info', 'Tool::GenerateMarkdownImage called', {
prompt: args.prompt.substring(0, 100)
});
// Generate image using the image router
const result = await createImageRouter(args.prompt);
if (!result) {
return {
success: false,
error: 'Failed to generate image',
markdown: `_Image generation failed for: "${args.prompt}"_`
};
}
// Upload to temp-images bucket
const publicUrl = await uploadToTempBucket(result.imageData, args.prompt, userId, addLog);
if (!publicUrl) {
return {
success: false,
error: 'Failed to upload image to storage',
markdown: `_Image upload failed for: "${args.prompt}"_`
};
}
// Build markdown with proper image syntax
const altText = args.altText || args.prompt;
const caption = args.caption ? `\n*${args.caption}*` : '';
const markdown = `![${altText}](${publicUrl})${caption}`;
addLog('info', 'Image generated and embedded in markdown', {
originalUrl: publicUrl,
markdownLength: markdown.length
});
return {
success: true,
markdown,
imageUrl: publicUrl,
message: 'Image generated and embedded in markdown successfully'
};
} catch (error: any) {
addLog('error', 'Tool::GenerateMarkdownImage failed', error);
return {
success: false,
error: error.message,
markdown: `_Error generating image: ${error.message}_`
};
}
},
});
/**
* Tool: Generate Text with Images
* Generates markdown content that can include both text and embedded images
*/
export const generateTextWithImagesTool = (userId: string, addLog: LogFunction = defaultLog) =>
zodFunction({
name: 'generate_text_with_images',
description: 'Generate markdown content that includes both text and images. When you need to illustrate concepts or enhance the content, use this tool to generate relevant images and embed them in the markdown.',
schema: z.object({
content: z.string().describe('The main text content in markdown format'),
imagePrompts: z.array(z.object({
prompt: z.string().describe('Image generation prompt'),
position: z.enum(['top', 'middle', 'bottom', 'inline']).describe('Where to place the image in the content'),
altText: z.string().optional().describe('Alt text for the image'),
caption: z.string().optional().describe('Caption for the image'),
})).optional().describe('Optional array of images to generate and embed'),
model: z.string().optional().describe('Image generation model. Default: "google/gemini-3-pro-image-preview"'),
}),
function: async (args) => {
try {
addLog('info', 'Tool::GenerateTextWithImages called', {
contentLength: args.content.length,
imageCount: args.imagePrompts?.length || 0
});
let finalContent = args.content;
// Generate and embed images if requested
if (args.imagePrompts && args.imagePrompts.length > 0) {
const markdownImageTool = generateMarkdownImageTool(userId, addLog);
for (const imagePrompt of args.imagePrompts) {
const imageResult = await (markdownImageTool as any).function.function({
prompt: imagePrompt.prompt,
altText: imagePrompt.altText,
caption: imagePrompt.caption,
model: args.model,
});
if (imageResult.success && imageResult.markdown) {
// Embed image based on position
switch (imagePrompt.position) {
case 'top':
finalContent = `${imageResult.markdown}\n\n${finalContent}`;
break;
case 'bottom':
finalContent = `${finalContent}\n\n${imageResult.markdown}`;
break;
case 'middle':
// Insert in the middle of content
const lines = finalContent.split('\n');
const middleIndex = Math.floor(lines.length / 2);
lines.splice(middleIndex, 0, '', imageResult.markdown, '');
finalContent = lines.join('\n');
break;
case 'inline':
default:
// Just append to content
finalContent = `${finalContent}\n\n${imageResult.markdown}`;
break;
}
}
}
}
return {
success: true,
content: finalContent,
imageCount: args.imagePrompts?.length || 0,
message: 'Content with images generated successfully'
};
} catch (error: any) {
addLog('error', 'Tool::GenerateTextWithImages failed', error);
return {
success: false,
error: error.message,
content: args.content // Return original content on failure
};
}
},
});
/**
* Create markdown-specific tool preset
*/
export const createMarkdownToolPreset = (userId: string, model: string = 'gpt-4o-mini', apiKey?: string, addLog: LogFunction = defaultLog) => ({
name: 'Markdown with Images',
description: 'Generate rich markdown content with embedded images',
model,
tools: [
generateMarkdownImageTool(userId, addLog),
generateTextWithImagesTool(userId, addLog),
],
systemPrompt: `You are a creative markdown content generator with image generation capabilities.
When generating content:
1. Create engaging, well-formatted markdown text
2. Use proper markdown syntax (headers, lists, emphasis, etc.)
3. When the content would benefit from visual illustration, use the generate_markdown_image tool
4. For comprehensive content pieces, use generate_text_with_images to create rich content with embedded images
5. Always provide helpful alt text and captions for images
6. Place images strategically to enhance the content
Image generation guidelines:
- Use descriptive, detailed prompts for better image quality
- Consider the content context when generating images
- Provide meaningful captions that add value
- Use appropriate positioning (top, middle, bottom, inline)
Return ONLY raw markdown content with embedded images. Do NOT wrap in code blocks.`,
});