Add image upload functionality to AI moderation testing

This commit is contained in:
Patrick Britton 2026-02-16 10:13:27 -06:00
parent 4fcab45b83
commit e5640ac98c
3 changed files with 160 additions and 15 deletions

View file

@ -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<any>(null);
const [testing, setTesting] = useState(false);
const [testHistory, setTestHistory] = useState<any[]>([]);
const [uploadedFile, setUploadedFile] = useState<File | null>(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) {
if (uploadedFile) {
data.image_file = uploadedFile;
} else {
data.image_url = testInput;
}
} else {
data.content = testInput;
}
@ -402,23 +434,79 @@ export default function AIModerationPage() {
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<Play className="w-4 h-4" /> Test Moderation
</h3>
{(selectedType.includes('image') || selectedType === 'video') ? (
<div className="space-y-3">
{/* File Upload */}
<div className="flex gap-2">
<label className="flex-1">
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
handleFileUpload(file);
}
}}
className="hidden"
/>
<div className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-brand-500 cursor-pointer hover:bg-gray-50 flex items-center justify-center gap-2">
<Upload className="w-4 h-4" />
{uploading ? 'Uploading...' : (uploadedFile ? `Uploaded: ${uploadedFile.name}` : 'Click to upload image...')}
</div>
</label>
</div>
{/* URL Input */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<span>OR</span>
</div>
<div className="flex gap-2">
<input
type="text"
value={testInput}
onChange={(e) => setTestInput(e.target.value)}
placeholder={selectedType.includes('image') || selectedType === 'video' ? 'Image URL...' : 'Test text...'}
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()}
/>
</div>
</div>
) : (
<div className="flex gap-2">
<input
type="text"
value={testInput}
onChange={(e) => 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()}
/>
</div>
)}
<div className="flex gap-2 mt-3">
<button
onClick={handleTest}
disabled={testing || !testInput.trim()}
disabled={testing || (!testInput.trim() && !uploadedFile)}
className="btn-primary text-sm flex items-center gap-1.5 disabled:opacity-40"
>
{testing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
Test
</button>
{uploadedFile && (
<button
onClick={() => {
setUploadedFile(null);
setTestInput('');
}}
className="text-sm text-gray-500 hover:text-gray-700"
>
Clear Upload
</button>
)}
</div>
</div>

View file

@ -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)

View file

@ -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 == "" {