// File: frontend/components/admin/ai/OcrSandboxPromptManager.tsx // Change Log // - 2026-05-25: Created OcrSandboxPromptManager component for dynamic prompt editing, version control, and sandbox testing (ADR-029) // - 2026-05-25: Extracted inline strings to i18n keys via useTranslations() (Obs #1 fix) // - 2026-05-25: Refactored sandbox polling to useSandboxRun hook (Obs #2 fix) 'use client'; import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Progress } from '@/components/ui/progress'; import { Badge } from '@/components/ui/badge'; import { toast } from 'sonner'; import { Brain, Save, AlertCircle, Upload, Play, FileJson, ScrollText, Loader2, StickyNote, } from 'lucide-react'; import { useAiPrompts, useSandboxRun } from '@/hooks/use-ai-prompts'; import { useTranslations } from '@/hooks/use-translations'; import PromptVersionHistory from './PromptVersionHistory'; import { cn } from '@/lib/utils'; import { AiPrompt } from '@/types/ai-prompts'; /** * Component หลักสำหรับจัดการ Prompt versions ของ OCR sandbox และ Migration * ประกอบไปด้วยตัวแก้ไข Prompt, รายการเวอร์ชัน, และส่วนสกัดทดสอบ (Sandbox run) */ export default function OcrSandboxPromptManager() { const t = useTranslations(); const promptType = 'ocr_extraction'; const { versionsQuery, createMutation, activateMutation, deleteMutation, updateNoteMutation, } = useAiPrompts(promptType); const versions = versionsQuery.data ?? []; const activePrompt = versions.find((v) => v.isActive); const [templateText, setTemplateText] = useState(''); const [ocrFile, setOcrFile] = useState(null); const [manualNote, setManualNote] = useState(''); const [activeTab, setActiveTab] = useState<'editor' | 'sandbox'>('editor'); const { state: sandboxState, jobId: sandboxJobId, submit: submitSandbox, reset: resetSandbox } = useSandboxRun(() => { // เมื่อ sandbox เสร็จสิ้น: รีเฟรชรายการเวอร์ชัน versionsQuery.refetch(); toast.success(t('ai.prompt.sandboxSuccess')); }); useEffect(() => { if (activePrompt && !templateText) { setTemplateText(activePrompt.template); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activePrompt]); const handleSaveVersion = async () => { if (!templateText.includes('{{ocr_text}}')) { toast.error(t('ai.prompt.placeholderError')); return; } if (templateText.length > 4000) { toast.error(t('ai.prompt.charLimitError')); return; } try { await createMutation.mutateAsync(templateText); toast.success(t('ai.prompt.saveVersionSuccess')); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; toast.error(error.response?.data?.message || t('ai.prompt.saveVersionError')); } }; const handleLoadTemplate = (version: AiPrompt) => { setTemplateText(version.template); setActiveTab('editor'); toast.success(t('ai.prompt.loadSuccess', { version: String(version.versionNumber) })); }; const handleActivateVersion = async (versionNumber: number) => { try { await activateMutation.mutateAsync(versionNumber); toast.success(t('ai.prompt.activateSuccess', { version: String(versionNumber) })); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; toast.error(error.response?.data?.message || t('ai.prompt.activateError')); } }; const handleDeleteVersion = async (versionNumber: number) => { if (!confirm(t('ai.prompt.deleteConfirm', { version: String(versionNumber) }))) return; try { await deleteMutation.mutateAsync(versionNumber); toast.success(t('ai.prompt.deleteSuccess', { version: String(versionNumber) })); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; toast.error(error.response?.data?.message || t('ai.prompt.deleteError')); } }; const handleSaveManualNote = async (versionNumber: number) => { try { await updateNoteMutation.mutateAsync({ versionNumber, note: manualNote }); toast.success(t('ai.prompt.saveNoteSuccess')); setManualNote(''); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; toast.error(error.response?.data?.message || t('ai.prompt.saveNoteError')); } }; const handleSubmitOcr = async (e: React.FormEvent) => { e.preventDefault(); if (!activePrompt) { toast.error(t('ai.prompt.noActivePrompt')); return; } if (!ocrFile) { toast.error(t('ai.prompt.noFile')); return; } try { resetSandbox(); await submitSandbox(ocrFile); toast.success(t('ai.prompt.uploadSuccess')); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; toast.error(error.response?.data?.message || t('ai.prompt.uploadError')); } }; // แปล status key เป็นข้อความตาม locale ปัจจุบัน const statusLabel = sandboxState.statusText ? t(sandboxState.statusText) : ''; return (
{activeTab === 'editor' ? ( {t('ai.prompt.cardTitle')} {activePrompt && ( {t('ai.prompt.activeLabel', { version: String(activePrompt.versionNumber) })} )}