238 lines
9.1 KiB
TypeScript
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 = `${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.`,
|
|
});
|