From ca0454a0436fdd5d3e1a9b5d9ea1ff47f5ce5a36 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 9 Apr 2026 10:12:23 +0700 Subject: [PATCH] 690409:1012 Done Task-FE-AI-03 --- frontend/app/(admin)/admin/migration/page.tsx | 600 ++++++++++++++---- .../components/ai/ai-suggestion-field.tsx | 171 +++++ .../ai/document-comparison-view.tsx | 158 +++++ .../components/ai/processing-indicator.tsx | 19 + frontend/lib/services/ai.service.ts | 71 +++ frontend/types/ai.ts | 76 +++ 6 files changed, 971 insertions(+), 124 deletions(-) create mode 100644 frontend/components/ai/ai-suggestion-field.tsx create mode 100644 frontend/components/ai/document-comparison-view.tsx create mode 100644 frontend/components/ai/processing-indicator.tsx create mode 100644 frontend/lib/services/ai.service.ts create mode 100644 frontend/types/ai.ts diff --git a/frontend/app/(admin)/admin/migration/page.tsx b/frontend/app/(admin)/admin/migration/page.tsx index 9b7c4d9..26f484d 100644 --- a/frontend/app/(admin)/admin/migration/page.tsx +++ b/frontend/app/(admin)/admin/migration/page.tsx @@ -1,7 +1,9 @@ 'use client'; import { useEffect, useState, useCallback } from 'react'; +import { aiService } from '@/lib/services/ai.service'; import { migrationService } from '@/lib/services/migration.service'; +import { AiMigrationLog, AiMigrationLogStatus } from '@/types/ai'; import { MigrationReviewQueueItem, MigrationReviewStatus } from '@/types/migration'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; @@ -9,13 +11,348 @@ import { Badge } from '@/components/ui/badge'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { format } from 'date-fns'; -import { EyeIcon, FileXIcon, CheckSquareIcon } from 'lucide-react'; +import { EyeIcon, FileXIcon, CheckCircleIcon, XCircleIcon, RefreshCwIcon } from 'lucide-react'; import Link from 'next/link'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { getApiErrorMessage } from '@/types/api-error'; +import { v4 as uuidv4 } from 'uuid'; -export default function MigrationReviewQueuePage() { +// --- AI Migration Tab --- + +function AiMigrationTab() { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [statusFilter, setStatusFilter] = useState('PENDING_REVIEW'); + // ADR-019: ใช้ publicId (string) สำหรับ selection + const [selectedPublicIds, setSelectedPublicIds] = useState([]); + const [errorMessage, setErrorMessage] = useState(null); + // Sheet สำหรับ inline review + const [reviewItem, setReviewItem] = useState(null); + const [adminFeedback, setAdminFeedback] = useState(''); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setErrorMessage(null); + const res = await aiService.getMigrationList({ + status: statusFilter === 'ALL' ? undefined : (statusFilter as AiMigrationLogStatus), + limit: 50, + }); + setItems(Array.isArray(res.items) ? res.items : []); + setSelectedPublicIds([]); + } catch (error: unknown) { + setItems([]); + setErrorMessage(getApiErrorMessage(error, 'ไม่สามารถโหลดข้อมูล AI Migration Logs ได้')); + } finally { + setLoading(false); + } + }, [statusFilter]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // ADR-019: toggle โดยใช้ publicId (string) ไม่ใช่ numeric id + const handleToggleSelectAll = () => { + if (selectedPublicIds.length === items.length) { + setSelectedPublicIds([]); + } else { + setSelectedPublicIds(items.map((i) => i.publicId)); + } + }; + + const handleToggleSelect = (publicId: string) => { + setSelectedPublicIds((prev) => + prev.includes(publicId) ? prev.filter((id) => id !== publicId) : [...prev, publicId] + ); + }; + + // Bulk verify รายการที่เลือก (ADR-019: ใช้ publicId) + const handleBulkVerify = async () => { + if (selectedPublicIds.length === 0) return; + try { + setSubmitting(true); + await Promise.all( + selectedPublicIds.map((publicId) => + aiService.updateMigration( + publicId, // ADR-019: UUID เท่านั้น + { status: AiMigrationLogStatus.VERIFIED }, + `bulk-verify-${publicId}-${uuidv4()}` + ) + ) + ); + toast.success(`ยืนยัน ${selectedPublicIds.length} รายการเรียบร้อย`); + await fetchData(); + } catch (_error) { + toast.error('การยืนยันแบบกลุ่มล้มเหลว'); + } finally { + setSubmitting(false); + } + }; + + // อัปเดตสถานะ item เดี่ยว (ADR-019: ใช้ publicId) + const handleUpdateStatus = async (status: AiMigrationLogStatus) => { + if (!reviewItem) return; + try { + setSubmitting(true); + await aiService.updateMigration( + reviewItem.publicId, // ADR-019: UUID เท่านั้น + { status, adminFeedback: adminFeedback || undefined }, + `review-${reviewItem.publicId}-${uuidv4()}` + ); + const label = status === AiMigrationLogStatus.VERIFIED ? 'ยืนยัน' : 'ปฏิเสธ'; + toast.success(`${label}เอกสารเรียบร้อย`); + setReviewItem(null); + setAdminFeedback(''); + await fetchData(); + } catch (_error) { + toast.error('ไม่สามารถอัปเดตสถานะได้'); + } finally { + setSubmitting(false); + } + }; + + // สีของ confidence badge + const getConfidenceVariant = (score?: number): 'default' | 'secondary' | 'destructive' | 'outline' => { + if (!score) return 'destructive'; + if (score >= 0.95) return 'default'; + if (score >= 0.75) return 'secondary'; + return 'destructive'; + }; + + // สีของ status badge + const getStatusVariant = (status: AiMigrationLogStatus): 'default' | 'secondary' | 'destructive' | 'outline' => { + switch (status) { + case AiMigrationLogStatus.VERIFIED: + case AiMigrationLogStatus.IMPORTED: + return 'default'; + case AiMigrationLogStatus.FAILED: + return 'destructive'; + default: + return 'outline'; + } + }; + + const statusLabels: Record = { + PENDING_REVIEW: 'รอตรวจสอบ', + VERIFIED: 'ผ่านการตรวจสอบ', + IMPORTED: 'นำเข้าแล้ว', + FAILED: 'ล้มเหลว', + }; + + return ( + <> + + +
+ AI Migration Logs +
+ {selectedPublicIds.length > 0 && ( + + )} + + +
+
+
+ + {errorMessage && ( +
+ {errorMessage} +
+ )} + {loading ? ( +
กำลังโหลด...
+ ) : items.length === 0 ? ( +
ไม่มีรายการ
+ ) : ( +
+ + + + + 0 && selectedPublicIds.length === items.length} + onCheckedChange={handleToggleSelectAll} + aria-label="เลือกทั้งหมด" + /> + + ไฟล์ต้นทาง + ความมั่นใจ AI + สถานะ + วันที่สร้าง + การดำเนินการ + + + + {items.map((item) => ( + // ADR-019: ใช้ publicId เป็น key + + + handleToggleSelect(item.publicId)} + aria-label={`เลือก ${item.publicId}`} + /> + + + {item.sourceFile} + + + + {item.confidenceScore + ? (item.confidenceScore * 100).toFixed(1) + '%' + : 'N/A'} + + + + + {statusLabels[item.status] ?? item.status} + + + + {format(new Date(item.createdAt), 'dd MMM yyyy, HH:mm')} + + + + + + ))} + +
+
+ )} +
+
+ + {/* Inline Review Sheet */} + { + if (!open) { + setReviewItem(null); + setAdminFeedback(''); + } + }} + > + + + ตรวจสอบ AI Migration Log + + {reviewItem && ( +
+
+

Public ID (ADR-019)

+

{reviewItem.publicId}

+
+
+

ไฟล์ต้นทาง

+

{reviewItem.sourceFile}

+
+
+

ความมั่นใจ AI

+ + {reviewItem.confidenceScore + ? (reviewItem.confidenceScore * 100).toFixed(1) + '%' + : 'N/A'} + +
+ {reviewItem.aiExtractedMetadata && + Object.keys(reviewItem.aiExtractedMetadata).length > 0 && ( +
+

+ ข้อมูลที่ AI สกัดได้ +

+
+ {Object.entries(reviewItem.aiExtractedMetadata).map(([k, v]) => ( +
+ {k}: + {String(v)} +
+ ))} +
+
+ )} +
+ +