diff --git a/admin/src/app/ai-moderation/page.tsx b/admin/src/app/ai-moderation/page.tsx index 4b47719..f87cdd2 100644 --- a/admin/src/app/ai-moderation/page.tsx +++ b/admin/src/app/ai-moderation/page.tsx @@ -3,7 +3,7 @@ import AdminShell from '@/components/AdminShell'; import { api } from '@/lib/api'; import { useEffect, useState, useCallback, useRef } from 'react'; -import { Brain, Search, Check, ChevronDown, Play, Loader2, Eye, MessageSquare, Video, Shield, MapPin, Users, AlertTriangle, Server, Cloud, Cpu, Terminal } from 'lucide-react'; +import { Brain, Search, Check, ChevronDown, Play, Loader2, Eye, MessageSquare, Video, Shield, MapPin, Users, AlertTriangle, Server, Cloud, Cpu, Terminal, Upload } from 'lucide-react'; const MODERATION_TYPES = [ { key: 'text', label: 'Text Moderation', icon: MessageSquare }, @@ -80,6 +80,8 @@ export default function AIModerationPage() { const [testResponse, setTestResponse] = useState(null); const [testing, setTesting] = useState(false); const [testHistory, setTestHistory] = useState([]); + const [uploadedFile, setUploadedFile] = useState(null); + const [uploading, setUploading] = useState(false); const loadConfigs = useCallback(() => { setLoading(true); @@ -141,18 +143,48 @@ export default function AIModerationPage() { } }; + const handleFileUpload = async (file: File) => { + setUploading(true); + try { + // Create FormData and upload to get a URL + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch('/api/v1/admin/upload-test-image', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error('Upload failed'); + } + + const result = await response.json(); + setTestInput(result.url); + setUploadedFile(file); + } catch (e: any) { + alert('Upload failed: ' + e.message); + } finally { + setUploading(false); + } + }; + const handleTest = async () => { - if (!testInput.trim()) return; + if (!testInput.trim() && !uploadedFile) return; setTesting(true); const startTime = Date.now(); try { - const isImage = selectedType.includes('image') || selectedType === 'video'; - const data: any = { + const data = { moderation_type: selectedType, engine: selectedEngine, }; + const isImage = selectedType.includes('image') || selectedType === 'video'; if (isImage) { - data.image_url = testInput; + if (uploadedFile) { + data.image_file = uploadedFile; + } else { + data.image_url = testInput; + } } else { data.content = testInput; } @@ -402,23 +434,79 @@ export default function AIModerationPage() {

Test Moderation

-
- setTestInput(e.target.value)} - placeholder={selectedType.includes('image') || selectedType === 'video' ? 'Image URL...' : 'Test text...'} - className="flex-1 text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-brand-500" - onKeyDown={(e) => e.key === 'Enter' && handleTest()} - /> + + {(selectedType.includes('image') || selectedType === 'video') ? ( +
+ {/* File Upload */} +
+ +
+ + {/* URL Input */} +
+ OR +
+ +
+ setTestInput(e.target.value)} + placeholder="Image URL..." + className="flex-1 text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-brand-500" + onKeyDown={(e) => e.key === 'Enter' && handleTest()} + /> +
+
+ ) : ( +
+ setTestInput(e.target.value)} + placeholder="Test text..." + className="flex-1 text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-brand-500" + onKeyDown={(e) => e.key === 'Enter' && handleTest()} + /> +
+ )} + +
+ {uploadedFile && ( + + )}
diff --git a/go-backend/cmd/api/main.go b/go-backend/cmd/api/main.go index e9b7818..01bcb62 100644 --- a/go-backend/cmd/api/main.go +++ b/go-backend/cmd/api/main.go @@ -629,6 +629,7 @@ func main() { // AI Engines Status admin.GET("/ai-engines", adminHandler.GetAIEngines) + admin.POST("/upload-test-image", adminHandler.UploadTestImage) // Safe Domains Management admin.GET("/safe-domains", adminHandler.ListSafeDomains) diff --git a/go-backend/internal/handlers/admin_handler.go b/go-backend/internal/handlers/admin_handler.go index 60ac021..df3d53a 100644 --- a/go-backend/internal/handlers/admin_handler.go +++ b/go-backend/internal/handlers/admin_handler.go @@ -1,10 +1,13 @@ package handlers import ( + "bytes" "context" "encoding/json" "fmt" + "io" "net/http" + "path/filepath" "strconv" "strings" "time" @@ -3819,6 +3822,59 @@ func (h *AdminHandler) GetAIEngines(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"engines": engines}) } +func (h *AdminHandler) UploadTestImage(c *gin.Context) { + file, header, err := c.Request.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"}) + return + } + defer file.Close() + + // Validate file type + if !strings.HasPrefix(header.Header.Get("Content-Type"), "image/") { + c.JSON(http.StatusBadRequest, gin.H{"error": "Only image files are allowed"}) + return + } + + // Validate file size (5MB limit) + if header.Size > 5*1024*1024 { + c.JSON(http.StatusBadRequest, gin.H{"error": "File too large (max 5MB)"}) + return + } + + // Generate unique filename + ext := filepath.Ext(header.Filename) + filename := fmt.Sprintf("test-%s%s", uuid.New().String()[:8], ext) + + // Upload to R2 + key := fmt.Sprintf("test-images/%s", filename) + + // Read file content + fileData, err := io.ReadAll(file) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read file"}) + return + } + + // Upload to R2 + contentType := header.Header.Get("Content-Type") + _, err = h.s3Client.PutObject(c.Request.Context(), &s3.PutObjectInput{ + Bucket: aws.String(h.mediaBucket), + Key: aws.String(key), + Body: bytes.NewReader(fileData), + ContentType: aws.String(contentType), + }) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Upload failed"}) + return + } + + // Return the URL + url := fmt.Sprintf("https://%s/%s", h.imgDomain, key) + c.JSON(http.StatusOK, gin.H{"url": url, "filename": filename}) +} + func (h *AdminHandler) CheckURLSafety(c *gin.Context) { urlStr := c.Query("url") if urlStr == "" {