// src/index.js - Updated Image Upload Worker with AI Vision Validation // Features: // 1. AI Vision validation (is this a property photo?) // 2. Get image name from AIDA // 3. Handle add/replace operations // 4. Duplicate name numbering export default { async fetch(request, env) { // CORS preflight if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "*" } }); } if (request.method !== "POST") { return new Response("Only POST allowed", { status: 405, headers: { "Access-Control-Allow-Origin": "*" } }); } const contentType = request.headers.get("Content-Type") || ""; if (!contentType.includes("multipart/form-data")) { return new Response("Unsupported Media Type", { status: 415, headers: { "Access-Control-Allow-Origin": "*" } }); } const contentLength = request.headers.get("content-length"); if (contentLength && parseInt(contentLength) > 5 * 1024 * 1024) { return new Response("Image too large. Max 5MB.", { status: 400, headers: { "Access-Control-Allow-Origin": "*" } }); } // Configuration - MOVE THESE TO ENVIRONMENT VARIABLES IN PRODUCTION const ACCOUNT_ID = env.CF_ACCOUNT_ID || "375f7ecab9677eb4e6362cbed2e09358"; const API_TOKEN = env.CF_API_TOKEN || "Kyx_WSRHASBktvx6GbI5W-HRxPiBuO6J867uegXw"; const ACCOUNT_HASH = env.CF_ACCOUNT_HASH || "0utJlkqgAVuawL5OpMWxgw"; const AIDA_BASE_URL = env.AIDA_BASE_URL || "https://destinyebuka-aida.hf.space"; try { // Parse form data const formData = await request.formData(); const imageFile = formData.get("file"); const userMessage = formData.get("message") || ""; const userId = formData.get("user_id") || ""; const sessionId = formData.get("session_id") || ""; const operation = formData.get("operation") || "add"; // "add" or "replace" const replaceIndex = formData.get("replace_index"); // For replace operations const existingImageId = formData.get("existing_image_id"); // ID of image to replace if (!imageFile) { return jsonResponse({ success: false, error: "no_image", message: "No image file provided" }, 400); } // Convert image to bytes for AI validation const imageBytes = await imageFile.arrayBuffer(); const imageArray = [...new Uint8Array(imageBytes)]; // ============================================================ // STEP 1: AI Vision Validation // ============================================================ let isPropertyImage = false; let validationReason = ""; try { const aiResult = await env.AI.run('@cf/llava-hf/llava-1.5-7b-hf', { image: imageArray, prompt: "Is this image showing a real estate property such as a house, apartment, room, building, or property exterior/interior? Answer with ONLY 'YES' or 'NO' followed by a brief reason.", max_tokens: 50 }); const response = aiResult.description || aiResult.response || String(aiResult); isPropertyImage = response.toUpperCase().includes("YES"); validationReason = response; } catch (aiError) { // If AI fails, allow the image through (fail-open for better UX) console.error("AI validation error:", aiError); isPropertyImage = true; validationReason = "AI validation skipped due to error"; } // If not a property image, return error (AIDA will handle friendly message) if (!isPropertyImage) { return jsonResponse({ success: false, error: "not_property_image", reason: validationReason, message: userMessage, user_id: userId, session_id: sessionId }, 400); } // ============================================================ // STEP 2: Get Image Name from AIDA (if new image) // ============================================================ let imageName = ""; if (operation === "add") { try { const nameResponse = await fetch(`${AIDA_BASE_URL}/ai/get-image-name`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, session_id: sessionId }) }); if (nameResponse.ok) { const nameData = await nameResponse.json(); imageName = nameData.name || `property-${Date.now()}`; } else { imageName = `property-${Date.now()}`; } } catch (nameError) { console.error("Failed to get image name from AIDA:", nameError); imageName = `property-${Date.now()}`; } } // ============================================================ // STEP 3: Handle Replace Operation (delete old image) // ============================================================ if (operation === "replace" && existingImageId) { try { // Get existing image name before deleting const listResponse = await fetch( `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/images/v1/${existingImageId}`, { method: "GET", headers: { Authorization: `Bearer ${API_TOKEN}` } } ); if (listResponse.ok) { const existingData = await listResponse.json(); if (existingData.result?.filename) { imageName = existingData.result.filename.replace(/\.[^/.]+$/, ""); // Remove extension } } // Delete old image await fetch( `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/images/v1/${existingImageId}`, { method: "DELETE", headers: { Authorization: `Bearer ${API_TOKEN}` } } ); } catch (deleteError) { console.error("Failed to delete old image:", deleteError); // Continue with upload anyway } } // ============================================================ // STEP 4: Upload to Cloudflare Images // ============================================================ // Clean the image name for use as filename const cleanName = imageName .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') || `property-${Date.now()}`; // Create new FormData for Cloudflare upload const uploadFormData = new FormData(); uploadFormData.append("file", new Blob([imageBytes], { type: imageFile.type }), `${cleanName}.jpg`); const cloudflareUploadURL = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/images/v1`; const uploadResponse = await fetch(cloudflareUploadURL, { method: "POST", headers: { Authorization: `Bearer ${API_TOKEN}` }, body: uploadFormData }); const uploadResponseBody = await uploadResponse.json(); if (uploadResponse.ok && uploadResponseBody.success && uploadResponseBody.result) { const imageId = uploadResponseBody.result.id; const imageUrl = `https://imagedelivery.net/${ACCOUNT_HASH}/${imageId}/public`; // Return success with all context for AIDA return jsonResponse({ success: true, id: imageId, url: imageUrl, filename: cleanName, message: userMessage, operation: operation, replace_index: replaceIndex, user_id: userId, session_id: sessionId }, 200); } else { const errorDetails = uploadResponseBody.errors || []; return jsonResponse({ success: false, error: "upload_failed", details: errorDetails, message: userMessage, user_id: userId, session_id: sessionId }, uploadResponse.status); } } catch (error) { console.error("Worker error:", error); return jsonResponse({ success: false, error: "worker_error", details: error.message }, 500); } } }; // Helper function for JSON responses function jsonResponse(data, status) { return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }); }