// File: frontend/components/admin/ai/SandboxTabs.tsx // Change Log: // - 2026-06-14: Created SandboxTabs component with 3-step testing (OCR -> AI Extract -> RAG Prep) (conforming to task T037) import React, { useState } from 'react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { adminAiService } from '@/lib/services/admin-ai.service'; import { useProjects, useContracts } from '@/hooks/use-master-data'; import { toast } from 'sonner'; import { Upload, Play, FileText, FileJson, Database, ArrowRight, Loader2, CheckCircle, } from 'lucide-react'; interface SandboxTabsProps { promptType: string; selectedVersionNumber?: number; onActivateVersion?: (versionNumber: number) => void; } interface ProjectOption { publicId: string; projectCode: string; projectName: string; } interface ContractOption { publicId: string; contractCode: string; contractName: string; } interface SandboxJobResult { ocrText?: string; answer?: string; status?: string; errorMessage?: string; ragChunks?: Array<{ text: string; summary: string }>; ragVectors?: unknown[]; } export default function SandboxTabs({ promptType: _promptType, selectedVersionNumber, onActivateVersion, }: SandboxTabsProps) { // Master data state const [selectedProject, setSelectedProject] = useState(''); const [selectedContract, setSelectedContract] = useState(''); const { data: projectsData } = useProjects(); const projects = Array.isArray(projectsData) ? (projectsData as ProjectOption[]) : []; const { data: contractsData } = useContracts(selectedProject); const contracts = Array.isArray(contractsData) ? (contractsData as ContractOption[]) : []; // Sandbox states const [file, setFile] = useState(null); const [ocrEngine, setOcrEngine] = useState('auto'); const [currentStep, setCurrentStep] = useState(1); const [jobStatus, setJobStatus] = useState<'idle' | 'running' | 'completed' | 'failed'>('idle'); const [progress, setProgress] = useState(0); const [statusText, setStatusText] = useState(''); // Results cache const [requestPublicId, setRequestPublicId] = useState(null); const [ocrText, setOcrText] = useState(''); const [extractedMetadata, setExtractedMetadata] = useState | null>(null); const [ragChunks, setRagChunks] = useState | null>(null); const [ragVectorsCount, setRagVectorsCount] = useState(0); const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { setFile(e.target.files[0]); setOcrText(''); setExtractedMetadata(null); setRagChunks(null); setRequestPublicId(null); setCurrentStep(1); setJobStatus('idle'); setProgress(0); } }; const pollJobStatus = (id: string, step: number, onSuccess: (result: SandboxJobResult) => void) => { let interval = setInterval(async () => { try { const res = await adminAiService.getSandboxJobStatus(id); if (res.status === 'completed') { clearInterval(interval); setJobStatus('completed'); setProgress(100); onSuccess(res as SandboxJobResult); } else if (res.status === 'failed') { clearInterval(interval); setJobStatus('failed'); setProgress(0); toast.error(res.errorMessage || 'การประมวลผลล้มเหลว'); } else if (res.status === 'processing') { setProgress(step === 1 ? 50 : 60); setStatusText('กำลังประมวลผล...'); } } catch (_err) { clearInterval(interval); setJobStatus('failed'); setProgress(0); toast.error('ไม่สามารถดึงสถานะงานได้'); } }, 2000); }; const handleRunOcr = async () => { if (!file) { toast.error('กรุณาเลือกไฟล์ PDF สำหรับทดสอบ'); return; } setJobStatus('running'); setProgress(15); setStatusText('กำลังอัปโหลดและส่งเอกสารเข้าคิว OCR...'); try { const res = await adminAiService.submitSandboxOcr(file, ocrEngine); setRequestPublicId(res.requestPublicId); pollJobStatus(res.requestPublicId, 1, (result) => { setOcrText(result.ocrText || ''); setCurrentStep(2); toast.success('ทำ OCR สำเร็จแล้ว สามารถทำการสกัดข้อมูลต่อได้'); }); } catch (_err) { setJobStatus('failed'); toast.error('เกิดข้อผิดพลาดในการรัน OCR'); } }; const handleRunExtract = async () => { if (!requestPublicId) { toast.error('กรุณาทำ OCR ก่อน'); return; } if (!selectedProject) { toast.error('กรุณาเลือกโครงการสำหรับทดสอบ'); return; } setJobStatus('running'); setProgress(20); setStatusText('กำลังประมวลผลการสกัดข้อมูลเมตาดาต้า...'); try { const res = await adminAiService.submitSandboxAiExtract( requestPublicId, selectedVersionNumber, selectedProject, selectedContract || undefined ); pollJobStatus(res.requestPublicId, 2, (result) => { let parsed = null; try { parsed = result.answer ? JSON.parse(result.answer) : null; } catch { parsed = { error: 'ผลลัพธ์ไม่ใช่ JSON ที่ถูกต้อง', raw: result.answer }; } setExtractedMetadata(parsed); setCurrentStep(3); toast.success('สกัดข้อมูลเมตาดาต้าสำเร็จ สามารถทดสอบ RAG Prep ต่อได้'); }); } catch (_err) { setJobStatus('failed'); toast.error('เกิดข้อผิดพลาดในการสกัดข้อมูล'); } }; const handleRunRagPrep = async () => { if (!ocrText) { toast.error('ไม่มีข้อความ OCR สำหรับทดสอบ'); return; } setJobStatus('running'); setProgress(30); setStatusText('กำลังประมวลผลการทำ Semantic Chunking และสร้างเวกเตอร์ RAG...'); try { const res = await adminAiService.submitSandboxRagPrep(ocrText); pollJobStatus(res.jobId, 3, (result) => { setRagChunks(result.ragChunks || []); setRagVectorsCount(result.ragVectors ? result.ragVectors.length : 0); toast.success('วิเคราะห์การเตรียมข้อมูล RAG สำเร็จ'); }); } catch (_err) { setJobStatus('failed'); toast.error('เกิดข้อผิดพลาดในการทำ RAG Prep'); } }; const handleActivate = () => { if (selectedVersionNumber && onActivateVersion) { onActivateVersion(selectedVersionNumber); } }; return ( รันบอร์ดทดลองการทำงาน (3-Step Sandbox Testing) ทดสอบความถูกต้องของเวอร์ชันพรอมต์จำลองกระบวนการจริง (OCR → AI Extract → RAG Prep)

เลือกไฟล์ PDF วิศวกรรม/ก่อสร้าง ขนาดไม่เกิน 50MB

เลือกไฟล์เอกสาร...
{file && (
{file.name} ({(file.size / (1024 * 1024)).toFixed(2)} MB)
)} {/* Status indicator */} {jobStatus === 'running' && (
{statusText} {progress}%
)} {/* Steps navigation and panels */}
{/* Step buttons */}
{/* Step detail views */}
{currentStep === 1 && (

Step 1: สกัดข้อความ OCR (OCR Extraction)

รันเอนจินสกัดข้อความเพื่อดึงตัวหนังสือดิบออกมาจากหน้าไฟล์ PDF ที่ส่งขึ้นไป สามารถดูผลลัพธ์ข้อความดิบเพื่อประเมินความคมชัดของ OCR

{ocrText ? (
{ocrText}
) : (
ยังไม่มีข้อมูล OCR คลิก "เริ่มรัน OCR" ด้านล่าง
)}
)} {currentStep === 2 && (

Step 2: สกัดข้อมูลอัจฉริยะ (AI Metadata Extraction)

ส่งข้อความ OCR พร้อมบริบท Master data (โครงการ/สัญญา) เข้าไปประมวลผลร่วมกับโมเดลหลักและเวอร์ชันพรอมต์ที่เลือก เพื่อแปลงเป็นโครงสร้างข้อมูล JSON อัจฉริยะ

{extractedMetadata ? (
{JSON.stringify(extractedMetadata, null, 2)}
) : (
ยังไม่มีผลลัพธ์การสกัดข้อมูล คลิก "เริ่มรันสกัดข้อมูล" ด้านล่าง
)}
{selectedVersionNumber && onActivateVersion && ( )}
)} {currentStep === 3 && (

Step 3: เตรียมฐานข้อมูลค้นหา (RAG Prep Sandbox)

จำลองกระบวนการแบ่งข้อความออกเป็นส่วนๆ (Semantic Chunking) ตามความเหมาะสมทางภาษาและความหมายของเอกสาร พร้อมแสดงขนาดเวกเตอร์ Dense/Sparse ที่สกัดสำหรับใช้ใน Qdrant

{ragChunks ? (
ทำเวกเตอร์สำเร็จ: {ragVectorsCount} เวกเตอร์ chunks: {ragChunks.length}
{ragChunks.map((chunk, idx) => (
#Chunk {idx + 1} {chunk.summary || 'หัวข้อหลัก'}

{chunk.text}

))}
) : (
ยังไม่มีผลลัพธ์ RAG Prep คลิก "เริ่มทดสอบ RAG Prep" ด้านล่าง
)}
)}
); }