690618:1444 237 #02
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
// File: frontend/components/admin/ai/AiExtractionPromptTab.tsx
|
||||
// Change Log
|
||||
// - 2026-06-17: Created AiExtractionPromptTab for AI extraction prompt management (Feature 238)
|
||||
// - 2026-06-18: Fixed linting errors (no-console, no-unused-vars, no-explicit-any)
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { adminAiPromptService, AiPromptVersion } from '@/lib/services/admin-ai-prompt.service';
|
||||
import PromptVersionHistory from './PromptVersionHistory';
|
||||
import { RefreshCw, Save, AlertCircle } from 'lucide-react';
|
||||
import { AiPrompt } from '@/types/ai-prompts';
|
||||
|
||||
/**
|
||||
* Component สำหรับจัดการ AI Extraction Prompt
|
||||
* - แสดง version history
|
||||
* - แก้ไข template (ต้องมี {{ocr_text}} placeholder)
|
||||
* - บันทึก version ใหม่
|
||||
* - เปิดใช้งาน version ที่ต้องการ
|
||||
*/
|
||||
export function AiExtractionPromptTab() {
|
||||
const [versions, setVersions] = useState<AiPromptVersion[]>([]);
|
||||
const [activeVersion, setActiveVersion] = useState<AiPromptVersion | null>(null);
|
||||
const [newTemplate, setNewTemplate] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isActivating, setIsActivating] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showRefreshDialog, setShowRefreshDialog] = useState(false);
|
||||
|
||||
const loadVersions = async () => {
|
||||
try {
|
||||
const data = await adminAiPromptService.getPrompts('ocr_extraction');
|
||||
setVersions(data);
|
||||
const active = data.find((v) => v.isActive);
|
||||
setActiveVersion(active || null);
|
||||
setNewTemplate(active?.template || '');
|
||||
setError(null);
|
||||
} catch {
|
||||
setError('Failed to load prompt versions');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadVersions();
|
||||
}, []);
|
||||
|
||||
const handleSaveNewVersion = async () => {
|
||||
if (!newTemplate.trim()) {
|
||||
setError('Template cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newTemplate.includes('{{ocr_text}}')) {
|
||||
setError('Template must include {{ocr_text}} placeholder');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await adminAiPromptService.createPrompt('ocr_extraction', newTemplate);
|
||||
await loadVersions();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.message.includes('409')) {
|
||||
setShowRefreshDialog(true);
|
||||
setError('Version conflict - data was modified by another user');
|
||||
} else {
|
||||
setError('Failed to save new version');
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivate = async (versionNumber: number) => {
|
||||
const version = versions.find(v => v.versionNumber === versionNumber);
|
||||
setIsActivating(true);
|
||||
setError(null);
|
||||
try {
|
||||
await adminAiPromptService.activatePrompt('ocr_extraction', versionNumber, version?.version);
|
||||
await loadVersions();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.message.includes('409')) {
|
||||
setShowRefreshDialog(true);
|
||||
setError('Version conflict - data was modified by another user');
|
||||
} else {
|
||||
setError('Failed to activate version');
|
||||
}
|
||||
} finally {
|
||||
setIsActivating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (versionNumber: number) => {
|
||||
setIsDeleting(true);
|
||||
setError(null);
|
||||
try {
|
||||
await adminAiPromptService.deletePrompt('ocr_extraction', versionNumber);
|
||||
await loadVersions();
|
||||
} catch {
|
||||
setError('Failed to delete version');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadTemplate = (version: AiPromptVersion) => {
|
||||
setNewTemplate(version.template);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
setShowRefreshDialog(false);
|
||||
loadVersions();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{error && (
|
||||
<Card className="border-destructive">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center gap-2 text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>AI Extraction Prompt Editor</CardTitle>
|
||||
<CardDescription>
|
||||
Extraction prompt สำหรับ LLM - ต้องมี {"{{ocr_text}}"} placeholder
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Template</label>
|
||||
<Textarea
|
||||
value={newTemplate}
|
||||
onChange={(e) => setNewTemplate(e.target.value)}
|
||||
placeholder="Enter extraction prompt template with {{ocr_text}} placeholder..."
|
||||
className="min-h-[200px] font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Template ต้องมี {"{{ocr_text}}"} placeholder สำหรับแทนที่ข้อความ OCR
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{activeVersion && (
|
||||
<Badge variant="outline">
|
||||
Active: v{activeVersion.versionNumber}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSaveNewVersion}
|
||||
disabled={isSaving || !newTemplate.trim()}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Save New Version
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Version History</CardTitle>
|
||||
<CardDescription>
|
||||
ประวัติเวอร์ชันทั้งหมดของ AI Extraction Prompt
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PromptVersionHistory
|
||||
versions={versions as unknown as AiPrompt[]}
|
||||
isLoading={false}
|
||||
onLoadTemplate={handleLoadTemplate as unknown as (version: AiPrompt) => void}
|
||||
onActivateVersion={handleActivate}
|
||||
onDeleteVersion={handleDelete}
|
||||
isActivating={isActivating}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{showRefreshDialog && (
|
||||
<Card className="border-warning">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-warning">Data Modified</CardTitle>
|
||||
<CardDescription>
|
||||
ข้อมูลถูกแก้ไขโดยผู้ใช้อื่น กรุณารีเฟรชข้อมูล
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={handleRefresh}>Refresh Data</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// File: frontend/components/admin/ai/OcrEngineSelector.tsx
|
||||
// Change Log
|
||||
// - 2026-05-30: สร้าง OcrEngineSelector สำหรับดึงและสลับ OCR Engine แบบไดนามิก (T019, T020, US1)
|
||||
// - 2026-06-17: ลบ Tesseract ออกจาก UI ตาม ADR-035 (เปลี่ยนเป็น Fast Path: PyMuPDF Text Layer)
|
||||
|
||||
'use client';
|
||||
|
||||
@@ -9,7 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
import { ScanText, Server, AlertCircle, CheckCircle2, Cpu } from 'lucide-react';
|
||||
import { ScanText, Server, CheckCircle2, Cpu } from 'lucide-react';
|
||||
import { adminAiService, OcrEngineResponse } from '@/lib/services/admin-ai.service';
|
||||
|
||||
/** Component สำหรับเลือกและจัดการ OCR Engine ในระบบ */
|
||||
@@ -116,9 +117,9 @@ export default function OcrEngineSelector() {
|
||||
<Cpu className="h-3 w-3" />
|
||||
ต้องการ VRAM: {(engine.vramRequirementMB / 1024).toFixed(1)} GB
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-amber-600 dark:text-amber-400">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
เอนจินสำรอง: Tesseract OCR
|
||||
<span className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
Fast Path: PyMuPDF Text Layer
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// File: frontend/components/admin/ai/OcrPromptTab.tsx
|
||||
// Change Log
|
||||
// - 2026-06-17: Created OcrPromptTab for OCR system prompt management (Feature 238)
|
||||
// - 2026-06-18: Fixed linting errors (no-console, no-explicit-any)
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { adminAiPromptService, AiPromptVersion } from '@/lib/services/admin-ai-prompt.service';
|
||||
import PromptVersionHistory from './PromptVersionHistory';
|
||||
import { RefreshCw, Save, AlertCircle } from 'lucide-react';
|
||||
import { AiPrompt } from '@/types/ai-prompts';
|
||||
|
||||
/**
|
||||
* Component สำหรับจัดการ OCR System Prompt
|
||||
* - แสดง version history
|
||||
* - แก้ไข template
|
||||
* - บันทึก version ใหม่
|
||||
* - เปิดใช้งาน version ที่ต้องการ
|
||||
*/
|
||||
export function OcrPromptTab() {
|
||||
const [versions, setVersions] = useState<AiPromptVersion[]>([]);
|
||||
const [activeVersion, setActiveVersion] = useState<AiPromptVersion | null>(null);
|
||||
const [newTemplate, setNewTemplate] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isActivating, setIsActivating] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showRefreshDialog, setShowRefreshDialog] = useState(false);
|
||||
|
||||
const loadVersions = async () => {
|
||||
try {
|
||||
const data = await adminAiPromptService.getPrompts('ocr_system');
|
||||
setVersions(data);
|
||||
const active = data.find((v) => v.isActive);
|
||||
setActiveVersion(active || null);
|
||||
setNewTemplate(active?.template || '');
|
||||
setError(null);
|
||||
} catch {
|
||||
setError('Failed to load prompt versions');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadVersions();
|
||||
}, []);
|
||||
|
||||
const handleSaveNewVersion = async () => {
|
||||
if (!newTemplate.trim()) {
|
||||
setError('Template cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await adminAiPromptService.createPrompt('ocr_system', newTemplate);
|
||||
await loadVersions();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.message.includes('409')) {
|
||||
setShowRefreshDialog(true);
|
||||
setError('Version conflict - data was modified by another user');
|
||||
} else {
|
||||
setError('Failed to save new version');
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivate = async (versionNumber: number) => {
|
||||
const version = versions.find(v => v.versionNumber === versionNumber);
|
||||
setIsActivating(true);
|
||||
setError(null);
|
||||
try {
|
||||
await adminAiPromptService.activatePrompt('ocr_system', versionNumber, version?.version);
|
||||
await loadVersions();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.message.includes('409')) {
|
||||
setShowRefreshDialog(true);
|
||||
setError('Version conflict - data was modified by another user');
|
||||
} else {
|
||||
setError('Failed to activate version');
|
||||
}
|
||||
} finally {
|
||||
setIsActivating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
setShowRefreshDialog(false);
|
||||
loadVersions();
|
||||
};
|
||||
|
||||
const handleDelete = async (versionNumber: number) => {
|
||||
setIsDeleting(true);
|
||||
setError(null);
|
||||
try {
|
||||
await adminAiPromptService.deletePrompt('ocr_system', versionNumber);
|
||||
await loadVersions();
|
||||
} catch {
|
||||
setError('Failed to delete version');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadTemplate = (version: AiPromptVersion) => {
|
||||
setNewTemplate(version.template);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{error && (
|
||||
<Card className="border-destructive">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center gap-2 text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>OCR System Prompt Editor</CardTitle>
|
||||
<CardDescription>
|
||||
System prompt สำหรับ OCR engine (np-dms-ocr) - ใช้สำหรับกำหนดวิธีการสกัดข้อความจาก PDF
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Template</label>
|
||||
<Textarea
|
||||
value={newTemplate}
|
||||
onChange={(e) => setNewTemplate(e.target.value)}
|
||||
placeholder="Enter OCR system prompt template..."
|
||||
className="min-h-[200px] font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
OCR system prompt เป็น free-form text ไม่ต้องมี placeholder ใดๆ
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{activeVersion && (
|
||||
<Badge variant="outline">
|
||||
Active: v{activeVersion.versionNumber}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSaveNewVersion}
|
||||
disabled={isSaving || !newTemplate.trim()}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Save New Version
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Version History</CardTitle>
|
||||
<CardDescription>
|
||||
ประวัติเวอร์ชันทั้งหมดของ OCR System Prompt
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PromptVersionHistory
|
||||
versions={versions as unknown as AiPrompt[]}
|
||||
isLoading={false}
|
||||
onLoadTemplate={handleLoadTemplate as unknown as (version: AiPrompt) => void}
|
||||
onActivateVersion={handleActivate}
|
||||
onDeleteVersion={handleDelete}
|
||||
isActivating={isActivating}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{showRefreshDialog && (
|
||||
<Card className="border-warning">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-warning">Data Modified</CardTitle>
|
||||
<CardDescription>
|
||||
ข้อมูลถูกแก้ไขโดยผู้ใช้อื่น กรุณารีเฟรชข้อมูล
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={handleRefresh}>Refresh Data</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
// - 2026-06-13: US4 — เพิ่ม project/contract selectors สำหรับ sandbox context parity
|
||||
// - 2026-06-13: US5 — เพิ่มลิงก์สลับไปยังหน้าจัดการ Prompt Version (Editor tab) จากส่วนเลือกเวอร์ชันใน Sandbox
|
||||
// - 2026-06-13: US9 — แก้ไข ESLint errors: ลบ parseInt และแก้ไข unsafe any type casting ของ projects/contracts
|
||||
// - 2026-06-17: ADR-036 Gap 5 — แก้ไขให้ Step 1 (OCR) ไม่ต้องเลือก project (OCR เป็นแค่ text extraction); Step 2 (AI Extract) เท่านั้นที่ต้องเลือก project
|
||||
|
||||
'use client';
|
||||
|
||||
@@ -343,13 +344,9 @@ export default function OcrSandboxPromptManager() {
|
||||
toast.error(error.response?.data?.message || t('ai.prompt.saveNoteError'));
|
||||
}
|
||||
};
|
||||
// Step 1: OCR-only handler
|
||||
// Step 1: OCR-only handler (ไม่ต้องเลือก project - OCR เป็นแค่ text extraction)
|
||||
const handleStep1Ocr = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!selectedProjectPublicId) {
|
||||
toast.error('Please select a project first');
|
||||
return;
|
||||
}
|
||||
if (!ocrFile) {
|
||||
toast.error(t('ai.prompt.noFile'));
|
||||
return;
|
||||
@@ -780,7 +777,7 @@ export default function OcrSandboxPromptManager() {
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={sandboxState.isRunning || !ocrFile || !selectedProjectPublicId}
|
||||
disabled={sandboxState.isRunning || !ocrFile}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{sandboxState.isRunning ? (
|
||||
|
||||
@@ -51,6 +51,8 @@ export default function PromptEditor({
|
||||
|
||||
const getFriendlyTypeName = (type: PromptType) => {
|
||||
switch (type) {
|
||||
case 'ocr_system':
|
||||
return 'คำสั่งระบบ OCR (OCR System Prompt)';
|
||||
case 'ocr_extraction':
|
||||
return 'สกัดข้อความ OCR (OCR Extraction)';
|
||||
case 'rag_query_prompt':
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// File: frontend/components/admin/ai/PromptManagementTabs.tsx
|
||||
// Change Log
|
||||
// - 2026-06-17: Created PromptManagementTabs for OCR & AI Extraction prompt separation (Feature 238)
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { OcrPromptTab } from './OcrPromptTab';
|
||||
import { AiExtractionPromptTab } from './AiExtractionPromptTab';
|
||||
|
||||
/**
|
||||
* Component หลักสำหรับจัดการ Prompt Management แบบแยก Tab
|
||||
* - OCR System Prompt Tab: จัดการ system prompt สำหรับ OCR engine
|
||||
* - AI Extraction Prompt Tab: จัดการ extraction prompt สำหรับ LLM
|
||||
*/
|
||||
export function PromptManagementTabs() {
|
||||
const [activeTab, setActiveTab] = useState('ocr-system');
|
||||
|
||||
return (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="ocr-system">OCR System Prompt</TabsTrigger>
|
||||
<TabsTrigger value="ai-extraction">AI Extraction Prompt</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="ocr-system">
|
||||
<OcrPromptTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="ai-extraction">
|
||||
<AiExtractionPromptTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -47,6 +47,9 @@ export default function PromptTypeDropdown({
|
||||
{t('prompt_management.all_types')}
|
||||
</SelectItem>
|
||||
)}
|
||||
<SelectItem value="ocr_system">
|
||||
คำสั่งระบบ OCR (OCR System Prompt)
|
||||
</SelectItem>
|
||||
<SelectItem value="ocr_extraction">
|
||||
สกัดข้อความ OCR (OCR Extraction)
|
||||
</SelectItem>
|
||||
|
||||
@@ -50,7 +50,7 @@ interface SandboxJobResult {
|
||||
status?: string;
|
||||
errorMessage?: string;
|
||||
ragChunks?: Array<{ text: string; summary: string }>;
|
||||
ragVectors?: unknown[];
|
||||
ragVectors?: number[][];
|
||||
}
|
||||
|
||||
export default function SandboxTabs({
|
||||
@@ -80,7 +80,13 @@ export default function SandboxTabs({
|
||||
const [ocrText, setOcrText] = useState<string>('');
|
||||
const [extractedMetadata, setExtractedMetadata] = useState<Record<string, unknown> | null>(null);
|
||||
const [ragChunks, setRagChunks] = useState<Array<{ text: string; summary: string }> | null>(null);
|
||||
const [ragVectorsCount, setRagVectorsCount] = useState<number>(0);
|
||||
const [ragVectors, setRagVectors] = useState<number[][] | null>(null);
|
||||
|
||||
// Track step completion status for activation gating (gap-2)
|
||||
const [step1Complete, setStep1Complete] = useState<boolean>(false);
|
||||
const [step2Complete, setStep2Complete] = useState<boolean>(false);
|
||||
const [step3Complete, setStep3Complete] = useState<boolean>(false);
|
||||
const allStepsComplete = step1Complete && step2Complete && step3Complete;
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
@@ -92,6 +98,10 @@ export default function SandboxTabs({
|
||||
setCurrentStep(1);
|
||||
setJobStatus('idle');
|
||||
setProgress(0);
|
||||
// Reset step completion flags (gap-2)
|
||||
setStep1Complete(false);
|
||||
setStep2Complete(false);
|
||||
setStep3Complete(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,6 +113,10 @@ export default function SandboxTabs({
|
||||
clearInterval(interval);
|
||||
setJobStatus('completed');
|
||||
setProgress(100);
|
||||
// Mark step as complete (gap-2)
|
||||
if (step === 1) setStep1Complete(true);
|
||||
if (step === 2) setStep2Complete(true);
|
||||
if (step === 3) setStep3Complete(true);
|
||||
onSuccess(res as SandboxJobResult);
|
||||
} else if (res.status === 'failed') {
|
||||
clearInterval(interval);
|
||||
@@ -192,7 +206,7 @@ export default function SandboxTabs({
|
||||
const res = await adminAiService.submitSandboxRagPrep(ocrText);
|
||||
pollJobStatus(res.jobId, 3, (result) => {
|
||||
setRagChunks(result.ragChunks || []);
|
||||
setRagVectorsCount(result.ragVectors ? result.ragVectors.length : 0);
|
||||
setRagVectors(result.ragVectors || null);
|
||||
toast.success('วิเคราะห์การเตรียมข้อมูล RAG สำเร็จ');
|
||||
});
|
||||
} catch (_err) {
|
||||
@@ -239,6 +253,20 @@ export default function SandboxTabs({
|
||||
<p className="text-[10px] text-muted-foreground italic">โหลดเวอร์ชันจาก Version History เพื่อดู template</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* UI fallback warning when no active OCR system prompt (gap-3) */}
|
||||
{_promptType === 'ocr_system' && !selectedTemplate && (
|
||||
<div className="rounded-lg border border-amber-500/30 bg-amber-500/[0.05] px-4 py-3 space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold text-amber-600 dark:text-amber-400">
|
||||
⚠️ คำเตือน: ไม่มี OCR System Prompt ที่เปิดใช้งาน
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-amber-700 dark:text-amber-300 leading-relaxed">
|
||||
ระบบจะใช้ค่าเริ่มต้น (default) ในการสกัดข้อความ OCR แนะนำให้สร้างและเปิดใช้งาน OCR System Prompt เพื่อปรับแต่งความแม่นยำของการสกัดข้อความ
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-4 border-b border-border/10 pb-4">
|
||||
<div className="flex-1 min-w-[200px] space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground">โครงการสำหรับสกัดบริบท</Label>
|
||||
@@ -422,10 +450,12 @@ export default function SandboxTabs({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleActivate}
|
||||
className="h-8 text-xs border-emerald-500/30 text-emerald-500 hover:bg-emerald-500/10"
|
||||
disabled={!allStepsComplete}
|
||||
className="h-8 text-xs border-emerald-500/30 text-emerald-500 hover:bg-emerald-500/10 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title={!allStepsComplete ? "ต้องทำครบทั้ง 3 ขั้นตอน (OCR → AI Extract → RAG Prep) ก่อนเปิดใช้งาน" : ""}
|
||||
>
|
||||
<CheckCircle className="mr-1.5 h-3.5 w-3.5" />
|
||||
เปิดใช้งานเวอร์ชัน v{selectedVersionNumber} ทันที
|
||||
เปิดใช้งานเวอร์ชัน v{selectedVersionNumber} {allStepsComplete ? 'ทันที' : '(ต้องทำครบ 3 ขั้นตอน)'}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex-1 text-right">
|
||||
@@ -459,7 +489,7 @@ export default function SandboxTabs({
|
||||
<div className="flex justify-between items-center bg-secondary/40 border border-border/50 px-3 py-2 rounded text-xs select-none">
|
||||
<span className="font-semibold text-foreground flex items-center gap-1">
|
||||
<CheckCircle className="h-4 w-4 text-emerald-500" />
|
||||
ทำเวกเตอร์สำเร็จ: {ragVectorsCount} เวกเตอร์
|
||||
ทำเวกเตอร์สำเร็จ: {ragVectors ? ragVectors.length : 0} เวกเตอร์
|
||||
</span>
|
||||
<Badge variant="outline" className="text-[10px] border-border/50"> chunks: {ragChunks.length}</Badge>
|
||||
</div>
|
||||
@@ -471,6 +501,13 @@ export default function SandboxTabs({
|
||||
<Badge className="text-[8px] py-0 px-1 select-none">{chunk.summary || 'หัวข้อหลัก'}</Badge>
|
||||
</div>
|
||||
<p className="leading-relaxed text-muted-foreground">{chunk.text}</p>
|
||||
{ragVectors && ragVectors[idx] && (
|
||||
<div className="mt-2 pt-2 border-t border-border/20">
|
||||
<span className="text-[9px] text-muted-foreground font-mono">
|
||||
Vector (first 5 dims): [{ragVectors[idx].slice(0, 5).map(v => v.toFixed(3)).join(', ')}...]
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// File: frontend/components/admin/ai/SandboxTestArea.tsx
|
||||
// Change Log:
|
||||
// - 2026-06-15: Created SandboxTestArea component with UI elements for 3-step sandbox testing (T038)
|
||||
// - 2026-06-17: ลบ Tesseract ออกจาก OCR Engine dropdown ตาม ADR-035 (ใช้ Typhoon OCR ผ่าน Ollama)
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
@@ -253,9 +254,8 @@ export default function SandboxTestArea({
|
||||
<SelectValue placeholder="เลือกเอนจิน..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="auto" className="text-xs">Auto (Baseline)</SelectItem>
|
||||
<SelectItem value="tesseract" className="text-xs">Tesseract (CPU)</SelectItem>
|
||||
<SelectItem value="np-dms-ocr" className="text-xs">Typhoon OCR (GPU)</SelectItem>
|
||||
<SelectItem value="auto" className="text-xs">Auto (Fast Path / Typhoon OCR)</SelectItem>
|
||||
<SelectItem value="np-dms-ocr" className="text-xs">Typhoon OCR (AI Vision)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user