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 AdminShell from '@/components/AdminShell';
import { api } from '@/lib/api'; import { api } from '@/lib/api';
import { useEffect, useState, useCallback, useRef } from 'react'; 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 = [ const MODERATION_TYPES = [
{ key: 'text', label: 'Text Moderation', icon: MessageSquare }, { key: 'text', label: 'Text Moderation', icon: MessageSquare },
@ -80,6 +80,8 @@ export default function AIModerationPage() {
const [testResponse, setTestResponse] = useState<any>(null); const [testResponse, setTestResponse] = useState<any>(null);
const [testing, setTesting] = useState(false); const [testing, setTesting] = useState(false);
const [testHistory, setTestHistory] = useState<any[]>([]); const [testHistory, setTestHistory] = useState<any[]>([]);
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const loadConfigs = useCallback(() => { const loadConfigs = useCallback(() => {
setLoading(true); 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 () => { const handleTest = async () => {
if (!testInput.trim()) return; if (!testInput.trim() && !uploadedFile) return;
setTesting(true); setTesting(true);
const startTime = Date.now(); const startTime = Date.now();
try { try {
const isImage = selectedType.includes('image') || selectedType === 'video'; const data = {
const data: any = {
moderation_type: selectedType, moderation_type: selectedType,
engine: selectedEngine, engine: selectedEngine,
}; };
const isImage = selectedType.includes('image') || selectedType === 'video';
if (isImage) { if (isImage) {
if (uploadedFile) {
data.image_file = uploadedFile;
} else {
data.image_url = testInput; data.image_url = testInput;
}
} else { } else {
data.content = testInput; 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"> <h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<Play className="w-4 h-4" /> Test Moderation <Play className="w-4 h-4" /> Test Moderation
</h3> </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"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={testInput} value={testInput}
onChange={(e) => setTestInput(e.target.value)} 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" 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()} 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 <button
onClick={handleTest} 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" 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" />} {testing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
Test Test
</button> </button>
{uploadedFile && (
<button
onClick={() => {
setUploadedFile(null);
setTestInput('');
}}
className="text-sm text-gray-500 hover:text-gray-700"
>
Clear Upload
</button>
)}
</div> </div>
</div> </div>

View file

@ -629,6 +629,7 @@ func main() {
// AI Engines Status // AI Engines Status
admin.GET("/ai-engines", adminHandler.GetAIEngines) admin.GET("/ai-engines", adminHandler.GetAIEngines)
admin.POST("/upload-test-image", adminHandler.UploadTestImage)
// Safe Domains Management // Safe Domains Management
admin.GET("/safe-domains", adminHandler.ListSafeDomains) admin.GET("/safe-domains", adminHandler.ListSafeDomains)

View file

@ -1,10 +1,13 @@
package handlers package handlers
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -3819,6 +3822,59 @@ func (h *AdminHandler) GetAIEngines(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"engines": engines}) 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) { func (h *AdminHandler) CheckURLSafety(c *gin.Context) {
urlStr := c.Query("url") urlStr := c.Query("url")
if urlStr == "" { if urlStr == "" {