From cb040c65803a90a9a96f8f15e18a36cd68f4d67a Mon Sep 17 00:00:00 2001 From: Babayaga Date: Wed, 8 Apr 2026 12:12:14 +0200 Subject: [PATCH] supabase --- packages/ui/src/lib/markdownImageTools.ts | 81 ++++--------------- packages/ui/src/lib/uploadUtils.ts | 14 ++++ packages/ui/src/modules/ai/imageTools.ts | 31 ++++++- .../ui/src/modules/posts/client-pictures.ts | 17 +++- 4 files changed, 73 insertions(+), 70 deletions(-) diff --git a/packages/ui/src/lib/markdownImageTools.ts b/packages/ui/src/lib/markdownImageTools.ts index bfbf15db..cf727036 100644 --- a/packages/ui/src/lib/markdownImageTools.ts +++ b/packages/ui/src/lib/markdownImageTools.ts @@ -6,56 +6,12 @@ */ import type { RunnableToolFunctionWithParse } from 'openai/lib/RunnableFunction'; -import { createImage as createImageRouter } from '@/lib/image-router'; -import { supabase } from '@/integrations/supabase/client'; +import { createImageTool } from '@/modules/ai/imageTools'; 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 => { - 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; - } -}; - // ── Input types ── interface GenerateMarkdownImageInput { @@ -105,42 +61,35 @@ export const generateMarkdownImageTool = (userId: string, addLog: LogFunction = prompt: args.prompt.substring(0, 100) }); - // Generate image using the image router - const result = await createImageRouter(args.prompt); - - if (!result) { + const imageTool = createImageTool(userId, addLog, args.model); + const imageResultRaw = await (imageTool.function.function as any)({ + prompt: args.prompt, + altText: args.altText, + caption: args.caption, + }); + const imageResult = typeof imageResultRaw === 'string' ? JSON.parse(imageResultRaw) : imageResultRaw; + if (!imageResult?.success || !imageResult?.imageUrl) { 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}"_` + error: imageResult?.error || 'Failed to generate or upload image', + markdown: imageResult?.markdown || `_Image generation 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}`; + const markdown = `![${altText}](${imageResult.imageUrl})${caption}`; addLog('info', 'Image generated and embedded in markdown', { - originalUrl: publicUrl, + originalUrl: imageResult.imageUrl, markdownLength: markdown.length }); return { success: true, markdown, - imageUrl: publicUrl, + imageUrl: imageResult.imageUrl, message: 'Image generated and embedded in markdown successfully' }; } catch (error: any) { @@ -203,7 +152,7 @@ export const generateTextWithImagesTool = (userId: string, addLog: LogFunction = if (args.imagePrompts && args.imagePrompts.length > 0) { const markdownImageTool = generateMarkdownImageTool(userId, addLog); for (const imagePrompt of args.imagePrompts) { - const imageResult = await markdownImageTool.function.function({ + const imageResult = await (markdownImageTool.function.function as any)({ prompt: imagePrompt.prompt, altText: imagePrompt.altText, caption: imagePrompt.caption, diff --git a/packages/ui/src/lib/uploadUtils.ts b/packages/ui/src/lib/uploadUtils.ts index a08f965c..3b481881 100644 --- a/packages/ui/src/lib/uploadUtils.ts +++ b/packages/ui/src/lib/uploadUtils.ts @@ -46,6 +46,20 @@ export const uploadImage = async (file: File, userId: string): Promise<{ publicU return { publicUrl: data.url, meta: data.meta }; }; +/** + * Uploads generated image bytes using the same configured image upload flow. + */ +export const uploadGeneratedImageData = async ( + imageData: ArrayBuffer, + prompt: string, + userId: string, +): Promise<{ publicUrl: string, meta?: any }> => { + const ts = Date.now(); + const slug = prompt.slice(0, 20).replace(/[^a-zA-Z0-9]/g, '-'); + const file = new File([imageData], `${ts}-${slug}.png`, { type: 'image/png' }); + return uploadImage(file, userId); +}; + /** * Creates a picture record in the database. */ diff --git a/packages/ui/src/modules/ai/imageTools.ts b/packages/ui/src/modules/ai/imageTools.ts index 5c60d56e..60386141 100644 --- a/packages/ui/src/modules/ai/imageTools.ts +++ b/packages/ui/src/modules/ai/imageTools.ts @@ -9,6 +9,7 @@ import { z } from 'zod'; import { createImage as createImageRouter } from '@/lib/image-router'; import { supabase } from '@/integrations/supabase/client'; +import { uploadGeneratedImageData } from '@/lib/uploadUtils'; import type { RunnableToolFunctionWithParse } from 'openai/lib/RunnableFunction'; type LogFunction = (level: 'info' | 'warn' | 'error' | 'debug', message: string, data?: any) => void; @@ -16,7 +17,11 @@ const defaultLog: LogFunction = (level, message, data) => console.log(`[IMAGE-TO // ── Upload helper ──────────────────────────────────────────────────────── -const uploadToTempBucket = async ( +const AI_IMAGE_UPLOAD_BACKEND = (import.meta.env.VITE_AI_IMAGE_UPLOAD_BACKEND || 'vfs').toLowerCase() === 'supabase' + ? 'supabase' + : 'vfs'; + +const uploadToSupabaseTempBucket = async ( imageData: ArrayBuffer, prompt: string, userId: string, @@ -49,6 +54,26 @@ const uploadToTempBucket = async ( } }; +const uploadGeneratedImage = async ( + imageData: ArrayBuffer, + prompt: string, + userId: string, + addLog: LogFunction, +): Promise => { + if (AI_IMAGE_UPLOAD_BACKEND === 'vfs') { + try { + const { publicUrl } = await uploadGeneratedImageData(imageData, prompt, userId); + addLog('info', 'Image uploaded via VFS flow', { publicUrl }); + return publicUrl; + } catch (err) { + addLog('error', 'VFS upload failed', err); + return null; + } + } + + return uploadToSupabaseTempBucket(imageData, prompt, userId, addLog); +}; + // ── Zod schemas ────────────────────────────────────────────────────────── const generateImageSchema = z.object({ @@ -69,7 +94,7 @@ export const createImageTool = ( function: { name: 'generate_image', description: - 'Generate an image from a text prompt. The image is uploaded to storage and ' + + 'Generate an image from a text prompt. The image is uploaded to configured storage and ' + 'returned as a markdown image tag you can embed in your response. ' + 'Use descriptive, detailed prompts for best results.', parameters: { @@ -106,7 +131,7 @@ export const createImageTool = ( }); } - const publicUrl = await uploadToTempBucket(result.imageData, args.prompt, userId, addLog); + const publicUrl = await uploadGeneratedImage(result.imageData, args.prompt, userId, addLog); if (!publicUrl) { return JSON.stringify({ success: false, diff --git a/packages/ui/src/modules/posts/client-pictures.ts b/packages/ui/src/modules/posts/client-pictures.ts index b19ff519..a9c0563f 100644 --- a/packages/ui/src/modules/posts/client-pictures.ts +++ b/packages/ui/src/modules/posts/client-pictures.ts @@ -2,6 +2,7 @@ import { PostMediaItem } from "@/modules/posts/views/types"; import { MediaItem } from "@/types"; import { SupabaseClient } from "@supabase/supabase-js"; import { fetchWithDeduplication, apiClient, getAuthHeaders } from "@/lib/db"; +import { uploadImage } from "@/lib/uploadUtils"; import { FetchMediaOptions } from "@/utils/mediaUtils"; /* { @@ -78,8 +79,22 @@ export const fetchUserPictures = async (userId: string) => (await fetchPictures( /** Convenience alias: fetch N recent pictures */ export const fetchRecentPictures = async (limit: number = 50) => (await fetchPictures({ limit })).data; +const POSTS_UPLOAD_BACKEND = (import.meta.env.VITE_POSTS_UPLOAD_BACKEND || 'vfs').toLowerCase() === 'supabase' + ? 'supabase' + : 'vfs'; + export const uploadFileToStorage = async (userId: string, file: File | Blob, fileName?: string, client?: SupabaseClient) => { - // Keep direct Supabase storage call for large files / efficiency + if (POSTS_UPLOAD_BACKEND === 'vfs') { + const uploadFile = file instanceof File + ? file + : new File([file], fileName || `${userId}/${Date.now()}-${Math.random().toString(36).substring(7)}`, { + type: file.type || 'application/octet-stream' + }); + const { publicUrl } = await uploadImage(uploadFile, userId); + return publicUrl; + } + + // Legacy fallback: direct Supabase storage upload. const { supabase } = await import("@/integrations/supabase/client"); const db = client || supabase; const name = fileName || `${userId}/${Date.now()}-${Math.random().toString(36).substring(7)}`;