Add image upload functionality to AI moderation testing
This commit is contained in:
parent
4fcab45b83
commit
e5640ac98c
|
|
@ -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) {
|
||||||
data.image_url = testInput;
|
if (uploadedFile) {
|
||||||
|
data.image_file = uploadedFile;
|
||||||
|
} else {
|
||||||
|
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>
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
{(selectedType.includes('image') || selectedType === 'video') ? (
|
||||||
type="text"
|
<div className="space-y-3">
|
||||||
value={testInput}
|
{/* File Upload */}
|
||||||
onChange={(e) => setTestInput(e.target.value)}
|
<div className="flex gap-2">
|
||||||
placeholder={selectedType.includes('image') || selectedType === 'video' ? 'Image URL...' : 'Test text...'}
|
<label className="flex-1">
|
||||||
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"
|
<input
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleTest()}
|
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="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
|
<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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 == "" {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue