// File: frontend/app/(admin)/admin/ai/page.tsx 'use client'; // Change Log // - 2026-05-21: เพิ่มหน้า AI Admin Console สำหรับเปิด/ปิด AI features. // - 2026-05-21: เพิ่มส่วนแสดงผลสถานะสุขภาพของระบบ AI (Ollama, Qdrant, queues) แบบ real-time polling 30s (T030, T031). // - 2026-05-21: เพิ่ม RAG Playground Sandbox tab สำหรับ Superadmin (T037, T038). // - 2026-05-21: เพิ่ม OCR Sandbox tab พร้อมการอัปเดตสถานะและการแสดงผล JSON แบบมีสีสำหรับ Superadmin (T043-T045). // - 2026-05-21: แก้ไข ESLint error เกี่ยวกับ any type และ console.error statement ให้ตรงตามมาตรฐาน Tier 1/2 // - 2026-05-25: เพิ่ม AI Model Management UI สำหรับเลือกโมเดลแบบไดนามิก (ADR-027). import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Brain, Loader2, Power, ShieldCheck, Cpu, Database, Activity, Search, Info, HelpCircle, AlertCircle, Settings2, Trash2 } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Switch } from '@/components/ui/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { Progress } from '@/components/ui/progress'; import { useAiStatus, useToggleAiFeatures, useAiHealth } from '@/hooks/use-ai-status'; import { projectService } from '@/lib/services/project.service'; import { adminAiService, AiSandboxJobResult, AiAvailableModel } from '@/lib/services/admin-ai.service'; import { toast } from 'sonner'; interface SandboxProject { publicId: string; projectName: string; projectCode: string; } export default function AiAdminConsolePage() { const { data, isLoading, isError, refetch, isFetching } = useAiStatus(); const { data: health, isLoading: isHealthLoading, refetch: refetchHealth } = useAiHealth(); const toggleMutation = useToggleAiFeatures(); const aiEnabled = data?.aiFeaturesEnabled ?? false; const busy = isLoading || toggleMutation.isPending; const [selectedProject, setSelectedProject] = useState(''); const [question, setQuestion] = useState(''); const [sandboxJobId, setSandboxJobId] = useState(null); const [sandboxJobResult, setSandboxJobResult] = useState(null); const [isSandboxPolling, setIsSandboxPolling] = useState(false); const [sandboxProgress, setSandboxProgress] = useState(0); const [sandboxStatusText, setSandboxStatusText] = useState(''); const [ocrFile, setOcrFile] = useState(null); const [ocrJobId, setOcrJobId] = useState(null); const [ocrJobResult, setOcrJobResult] = useState(null); const [isOcrPolling, setIsOcrPolling] = useState(false); const [ocrProgress, setOcrProgress] = useState(0); const [ocrStatusText, setOcrStatusText] = useState(''); // AI Model Management State (ADR-027) const { data: aiModelsData, refetch: refetchModels } = useQuery<{ models: AiAvailableModel[]; activeModel: string }>({ queryKey: ['ai-available-models'], queryFn: async () => { return await adminAiService.getAvailableModels(); }, }); const availableModels = aiModelsData?.models ?? []; const activeModel = aiModelsData?.activeModel ?? ''; const { data: projects = [], isLoading: isProjectsLoading } = useQuery({ queryKey: ['admin-sandbox-projects'], queryFn: async () => { const res = await projectService.getAll({ isActive: true, limit: 100 }); return res as SandboxProject[]; }, }); const handleToggle = async (enabled: boolean): Promise => { await toggleMutation.mutateAsync(enabled); }; const handleModelChange = async (modelName: string): Promise => { try { await adminAiService.setActiveModel(modelName); toast.success(`เปลี่ยนโมเดลเป็น ${modelName} สำเร็จ`); await refetchModels(); } catch { toast.error('ไม่สามารถเปลี่ยนโมเดลได้'); } }; const handleToggleModel = async (modelName: string): Promise => { try { await adminAiService.toggleModelActive(modelName); toast.success(`เปลี่ยนสถานะโมเดล ${modelName} สำเร็จ`); await refetchModels(); } catch { toast.error('ไม่สามารถเปลี่ยนสถานะโมเดลได้'); } }; const handleRemoveModel = async (modelName: string): Promise => { if (!confirm(`ต้องการลบโมเดล ${modelName} ใช่หรือไม่?`)) return; try { await adminAiService.removeModel(modelName); toast.success(`ลบโมเดล ${modelName} สำเร็จ`); await refetchModels(); } catch { toast.error('ไม่สามารถลบโมเดลได้'); } }; const handleRefreshAll = async (): Promise => { await Promise.all([refetch(), refetchHealth()]); }; const handleSubmitSandbox = async (e: React.FormEvent): Promise => { e.preventDefault(); if (!selectedProject) { toast.error('กรุณาเลือกโครงการ'); return; } if (!question.trim()) { toast.error('กรุณากรอกคำถาม'); return; } try { setSandboxJobResult(null); setSandboxProgress(10); setSandboxStatusText('กำลังส่งคำถาม RAG เข้าสู่ระบบคิว...'); const response = await adminAiService.submitSandboxRag(selectedProject, question); setSandboxJobId(response.requestPublicId); setIsSandboxPolling(true); toast.success('ส่งคำถามเข้าสู่คิว sandbox สำเร็จ'); } catch (err) { const error = err as { response?: { data?: { message?: string } } }; toast.error(error.response?.data?.message || 'เกิดข้อผิดพลาดในการส่งคำถาม RAG'); setSandboxProgress(0); setSandboxStatusText(''); } }; useEffect(() => { if (!sandboxJobId) return; let timer: NodeJS.Timeout; const pollSandboxJob = async () => { try { const res = await adminAiService.getSandboxJobStatus(sandboxJobId); setSandboxJobResult(res); if (res.status === 'pending') { setSandboxProgress(20); setSandboxStatusText('อยู่ระหว่างเข้าคิวรอประมวลผล (Pending in BullMQ)...'); } else if (res.status === 'processing') { setSandboxProgress(60); setSandboxStatusText('กำลังค้นหาเอกสารผ่าน Qdrant และประมวลผล RAG ด้วย Local LLM...'); } else if (res.status === 'completed') { setSandboxProgress(100); setSandboxStatusText('ประมวลผลคำตอบเสร็จสิ้น'); setIsSandboxPolling(false); setSandboxJobId(null); toast.success('RAG Sandbox ตอบคำถามสำเร็จ'); } else if (res.status === 'failed') { setSandboxProgress(100); setSandboxStatusText('การประมวลผลล้มเหลว'); setIsSandboxPolling(false); setSandboxJobId(null); toast.error(res.errorMessage || 'เกิดข้อผิดพลาดในการรัน RAG Playground'); } else if (res.status === 'cancelled') { setSandboxProgress(100); setSandboxStatusText('การประมวลผลถูกยกเลิก'); setIsSandboxPolling(false); setSandboxJobId(null); toast.error('Sandbox job ถูกยกเลิก'); } else if (res.status === 'not_found') { setSandboxProgress(15); setSandboxStatusText('กำลังเตรียมการจัดคิว...'); } } catch { // เงียบข้อผิดพลาดตามนโยบาย UI } }; pollSandboxJob(); timer = setInterval(pollSandboxJob, 5000); return () => { clearInterval(timer); }; }, [sandboxJobId]); const handleSubmitOcr = async (e: React.FormEvent): Promise => { e.preventDefault(); if (!ocrFile) { toast.error('กรุณาเลือกไฟล์ PDF สำหรับทำ OCR'); return; } if (ocrFile.size > 50 * 1024 * 1024) { toast.error('ขนาดไฟล์เกินกว่า 50MB'); return; } if (ocrFile.type !== 'application/pdf' && !ocrFile.name.toLowerCase().endsWith('.pdf')) { toast.error('กรุณาอัปโหลดไฟล์ในรูปแบบ PDF เท่านั้น'); return; } try { setOcrJobResult(null); setOcrProgress(10); setOcrStatusText('กำลังอัปโหลดไฟล์ไปยังระบบเซิร์ฟเวอร์...'); const response = await adminAiService.submitSandboxExtract(ocrFile); setOcrJobId(response.requestPublicId); setIsOcrPolling(true); toast.success('อัปโหลดไฟล์สำเร็จและเข้าสู่คิว sandbox OCR'); } catch (err) { const error = err as { response?: { data?: { message?: string } } }; toast.error(error.response?.data?.message || 'เกิดข้อผิดพลาดในการทำ OCR Sandbox'); setOcrProgress(0); setOcrStatusText(''); } }; useEffect(() => { if (!ocrJobId) return; let timer: NodeJS.Timeout; const pollOcrJob = async () => { try { const res = await adminAiService.getSandboxJobStatus(ocrJobId); setOcrJobResult(res); if (res.status === 'pending') { setOcrProgress(30); setOcrStatusText('อยู่ในคิวรอดำเนินการ (Pending in BullMQ)...'); } else if (res.status === 'processing') { setOcrProgress(70); setOcrStatusText('กำลังอ่านไฟล์ PDF และสกัดข้อความด้วย OCR & LLM...'); } else if (res.status === 'completed') { setOcrProgress(100); setOcrStatusText('การทำ OCR และสกัดข้อมูลเมตาดาต้าเสร็จสิ้น'); setIsOcrPolling(false); setOcrJobId(null); toast.success('ทำ OCR Sandbox สำเร็จ'); } else if (res.status === 'failed') { setOcrProgress(100); setOcrStatusText('การทำ OCR ล้มเหลว'); setIsOcrPolling(false); setOcrJobId(null); toast.error(res.errorMessage || 'การทำ OCR Sandbox เกิดข้อผิดพลาด'); } else if (res.status === 'cancelled') { setOcrProgress(100); setOcrStatusText('การทำ OCR ถูกยกเลิก'); setIsOcrPolling(false); setOcrJobId(null); toast.error('OCR sandbox job ถูกยกเลิก'); } else if (res.status === 'not_found') { setOcrProgress(20); setOcrStatusText('กำลังตรวจสอบสถานะคิวงาน...'); } } catch { // เงียบข้อผิดพลาดตามนโยบาย UI } }; pollOcrJob(); timer = setInterval(pollOcrJob, 5000); return () => { clearInterval(timer); }; }, [ocrJobId]); const renderStatusBadge = (status?: 'HEALTHY' | 'DEGRADED' | 'DOWN') => { if (!status) return Unknown; switch (status) { case 'HEALTHY': return Healthy; case 'DEGRADED': return Degraded; default: return Down; } }; return (

AI Console

ควบคุมสถานะ AI features สำหรับผู้ใช้ทั่วไป

{aiEnabled ? 'AI Enabled' : 'AI Disabled'}
Overview & Health RAG Playground OCR Sandbox
Ollama AI Engine {isHealthLoading ? : renderStatusBadge(health?.ollama?.status)}
ความเร็วตอบสนอง {health?.ollama?.latencyMs !== undefined ? `${health.ollama.latencyMs} ms` : '-'}
โมเดลที่โหลดอยู่:
{health?.ollama?.models && health.ollama.models.length > 0 ? ( health.ollama.models.map((m) => ( {m} )) ) : ( ไม่มีโมเดลที่โหลดอยู่ )}
{health?.ollama?.error && (

{health.ollama.error}

)}
Qdrant Vector DB {isHealthLoading ? : renderStatusBadge(health?.qdrant?.status)}
ความเร็วตอบสนอง {health?.qdrant?.latencyMs !== undefined ? `${health.qdrant.latencyMs} ms` : '-'}
คอลเลกชัน:
{health?.qdrant?.collections && health.qdrant.collections.length > 0 ? ( health.qdrant.collections.map((c) => ( {c} )) ) : ( ไม่มีคอลเลกชัน )}
{health?.qdrant?.error && (

{health.qdrant.error}

)}
BullMQ Queue Health {isHealthLoading ? ( ) : ( {health?.timestamp ? new Date(health.timestamp).toLocaleTimeString() : 'N/A'} )}
คิว / สถานะงาน Active / Waiting / Failed
realtime {health?.queues?.realtime?.isPaused && (Paused)} {health?.queues?.realtime?.active ?? 0} / {health?.queues?.realtime?.waiting ?? 0} /{' '} 0 ? 'text-destructive' : ''}> {health?.queues?.realtime?.failed ?? 0}
batch {health?.queues?.batch?.isPaused && (Paused)} {health?.queues?.batch?.active ?? 0} / {health?.queues?.batch?.waiting ?? 0} /{' '} 0 ? 'text-destructive' : ''}> {health?.queues?.batch?.failed ?? 0}
{(health?.queues?.realtime?.error || health?.queues?.batch?.error) && (

{health.queues.realtime.error || health.queues.batch.error}

)}
System Toggle
{aiEnabled ? 'AI พร้อมให้ผู้ใช้ทั่วไปใช้งาน' : 'AI ถูกปิดสำหรับผู้ใช้ทั่วไป'}
Superadmin ยังสามารถเข้าถึงส่วนทดสอบและดูแลระบบได้ตามสิทธิ์
{busy && }
{isError && (
ไม่สามารถโหลดสถานะ AI ได้ กรุณาลองใหม่อีกครั้ง
)}
{/* AI Model Management Card (ADR-027) */} AI Model Management ADR-027
โมเดลปัจจุบัน: {activeModel || 'Loading...'}

รายการโมเดลทั้งหมด

{availableModels.length === 0 ? (

ไม่มีโมเดลในระบบ

) : ( availableModels.map((model) => (
{model.isActive ? 'Active' : 'Inactive'} {model.modelName} {model.isDefault && ( Default )} {activeModel === model.modelName && ( Current )}
{!model.isDefault && ( <> )}
)) )}
Protection เมื่อปิด AI ระบบจะบล็อก AI inference endpoints สำหรับผู้ใช้ทั่วไปด้วย HTTP 503 และให้ผู้ใช้กรอกข้อมูลเองชั่วคราว Polling อัปเดตสถานะทุก 30 วินาที {(isFetching || isHealthLoading) && !(isLoading || isHealthLoading) ? ' (กำลังรีเฟรช)' : ''}
RAG Sandbox Playground (isolated)

พื้นที่ทดสอบสืบค้นเอกสารและสรุปผลด้วย Retrieval-Augmented Generation (RAG) คิวงานใช้ระดับความสำคัญพิเศษ (Priority 1)

{isProjectsLoading ? (
กำลังโหลดรายการโครงการ...
) : ( )}