feat(ai): implement unified prompt management UX/UI (ADR-037)
- Add context config endpoints (GET/PUT /api/ai/prompts/:type/:version/context-config) - Add execution profile endpoints (CRUD /api/ai/execution-profiles) - Add sandbox RAG Prep endpoint (POST /api/ai/admin/sandbox/rag-prep) - Create Prompt Management UI with multi-type support - Add ContextConfigEditor, PromptEditor, RuntimeParametersPanel components - Add SandboxTabs for 3-step workflow (OCR, Extract, RAG Prep) - Add database deltas for ai_execution_profiles and additional prompt types - Update quickstart.md with production backend URLs - Add comprehensive test coverage for new features
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
// File: frontend/components/admin/ai/ContextConfigEditor.tsx
|
||||
// Change Log:
|
||||
// - 2026-06-14: Created ContextConfigEditor component with project/contract loaders and selectors (conforming to task T028)
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { CheckCircle2, Settings } from 'lucide-react';
|
||||
import { ContextConfig } from '@/lib/types/ai-prompts';
|
||||
import { projectService } from '@/lib/services/project.service';
|
||||
import { contractService } from '@/lib/services/contract.service';
|
||||
|
||||
interface ContextConfigEditorProps {
|
||||
initialConfig: ContextConfig | null;
|
||||
onSave: (config: ContextConfig) => Promise<void>;
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
interface ProjectOption {
|
||||
publicId: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
interface ContractOption {
|
||||
publicId: string;
|
||||
contractName: string;
|
||||
project?: {
|
||||
publicId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* คอมโพเนนต์ฟอร์มสำหรับแก้ไขบริบทข้อมูล (Context Configuration)
|
||||
* จัดการตัวเลือกการกรองข้อมูลรายโครงการ (Project Filter) และรายสัญญา (Contract Filter) รวมทั้งภาษาและจำนวนประวัติการดึงข้อมูล
|
||||
*/
|
||||
export default function ContextConfigEditor({
|
||||
initialConfig,
|
||||
onSave,
|
||||
isSaving,
|
||||
}: ContextConfigEditorProps) {
|
||||
const [projects, setProjects] = useState<ProjectOption[]>([]);
|
||||
const [contracts, setContracts] = useState<ContractOption[]>([]);
|
||||
const [filteredContracts, setFilteredContracts] = useState<ContractOption[]>([]);
|
||||
|
||||
// State ฟอร์ม
|
||||
const [projectId, setProjectId] = useState<string>('all');
|
||||
const [contractId, setContractId] = useState<string>('all');
|
||||
const [pageSize, setPageSize] = useState<number>(3);
|
||||
const [language, setLanguage] = useState<string>('th');
|
||||
const [outputLanguage, setOutputLanguage] = useState<string>('th');
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const projList = await projectService.getAll();
|
||||
setProjects(
|
||||
Array.isArray(projList)
|
||||
? (projList as unknown as Record<string, unknown>[]).map((p) => ({
|
||||
publicId: String(p.publicId || ''),
|
||||
projectName: String(p.projectName || ''),
|
||||
}))
|
||||
: []
|
||||
);
|
||||
const contrList = await contractService.getAll();
|
||||
setContracts(
|
||||
Array.isArray(contrList)
|
||||
? (contrList as unknown as Record<string, unknown>[]).map((c) => ({
|
||||
publicId: String(c.publicId || ''),
|
||||
contractName: String(c.contractName || ''),
|
||||
project: c.project
|
||||
? {
|
||||
publicId: String((c.project as unknown as Record<string, unknown>).publicId || ''),
|
||||
}
|
||||
: undefined,
|
||||
}))
|
||||
: []
|
||||
);
|
||||
} catch (_err) {
|
||||
// error handling silently per rules (use NestJS Logger on backend, avoid console.log on frontend)
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
// พรีโหลดค่าตั้งต้น
|
||||
useEffect(() => {
|
||||
if (initialConfig) {
|
||||
setProjectId(initialConfig.filter?.projectId || 'all');
|
||||
setContractId(initialConfig.filter?.contractId || 'all');
|
||||
setPageSize(initialConfig.pageSize || 3);
|
||||
setLanguage(initialConfig.language || 'th');
|
||||
setOutputLanguage(initialConfig.outputLanguage || 'th');
|
||||
} else {
|
||||
setProjectId('all');
|
||||
setContractId('all');
|
||||
setPageSize(3);
|
||||
setLanguage('th');
|
||||
setOutputLanguage('th');
|
||||
}
|
||||
}, [initialConfig]);
|
||||
|
||||
// กรองรายการสัญญาตามโครงการที่เลือก
|
||||
useEffect(() => {
|
||||
if (projectId && projectId !== 'all') {
|
||||
const filtered = contracts.filter((c) => c.project?.publicId === projectId);
|
||||
setFilteredContracts(filtered);
|
||||
// รีเซ็ตสัญญาถ้าไม่ได้ผูกกับโครงการที่เลือก
|
||||
const isStillValid = filtered.some((c) => c.publicId === contractId);
|
||||
if (!isStillValid && contractId !== 'all') {
|
||||
setContractId('all');
|
||||
}
|
||||
} else {
|
||||
setFilteredContracts(contracts);
|
||||
}
|
||||
}, [projectId, contracts, contractId]);
|
||||
|
||||
const handleSave = () => {
|
||||
const config: ContextConfig = {
|
||||
filter: {
|
||||
projectId: projectId === 'all' ? null : projectId,
|
||||
contractId: contractId === 'all' ? null : contractId,
|
||||
},
|
||||
pageSize: Number(pageSize),
|
||||
language,
|
||||
outputLanguage,
|
||||
};
|
||||
onSave(config);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border border-border/50 bg-background/30 backdrop-blur-md transition-all duration-300 hover:shadow-md">
|
||||
<CardHeader className="pb-3 border-b border-border/10">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold tracking-wide text-foreground">
|
||||
<Settings className="h-4 w-4 text-primary" />
|
||||
การตั้งค่าบริบทข้อมูล (Context Configuration)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 space-y-4">
|
||||
{/* เลือกล็อคโครงการ */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
ตัวกรองโครงการ (Project Filter)
|
||||
</label>
|
||||
<Select value={projectId} onValueChange={setProjectId}>
|
||||
<SelectTrigger className="w-full bg-background/50 border-border/50 backdrop-blur-sm">
|
||||
<SelectValue placeholder="เลือกโครงการ..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">ทั้งหมด (ไม่กรอง / Global)</SelectItem>
|
||||
{projects.map((p) => (
|
||||
<SelectItem key={p.publicId} value={p.publicId}>
|
||||
{p.projectName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* เลือกล็อคสัญญา */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
ตัวกรองสัญญา (Contract Filter)
|
||||
</label>
|
||||
<Select value={contractId} onValueChange={setContractId}>
|
||||
<SelectTrigger className="w-full bg-background/50 border-border/50 backdrop-blur-sm">
|
||||
<SelectValue placeholder="เลือกสัญญา..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">ทั้งหมด (ไม่กรอง / Global)</SelectItem>
|
||||
{filteredContracts.map((c) => (
|
||||
<SelectItem key={c.publicId} value={c.publicId}>
|
||||
{c.contractName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* ปริมาณเอกสารอ้างอิงและภาษา */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3.5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
ประวัติอ้างอิง (Page Size)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
value={pageSize}
|
||||
onChange={(e) => setPageSize(Math.max(1, Number(e.target.value)))}
|
||||
className="bg-background/50 border-border/50 text-sm focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
ภาษาต้นทาง (Language)
|
||||
</label>
|
||||
<Select value={language} onValueChange={setLanguage}>
|
||||
<SelectTrigger className="bg-background/50 border-border/50 backdrop-blur-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="th">ไทย (TH)</SelectItem>
|
||||
<SelectItem value="en">English (EN)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
ภาษาปลายทาง (Output)
|
||||
</label>
|
||||
<Select value={outputLanguage} onValueChange={setOutputLanguage}>
|
||||
<SelectTrigger className="bg-background/50 border-border/50 backdrop-blur-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="th">ไทย (TH)</SelectItem>
|
||||
<SelectItem value="en">English (EN)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex items-center justify-between border-t border-border/10 pt-4 bg-muted/5 rounded-b-xl">
|
||||
<span className="text-[11px] text-muted-foreground italic flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />
|
||||
การตั้งค่านี้จะผูกกับเวอร์ชันของพรอมต์โดยตรง
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
size="sm"
|
||||
className="bg-primary hover:bg-primary/95 font-semibold text-xs"
|
||||
>
|
||||
{isSaving ? 'กำลังบันทึก...' : 'บันทึกบริบท (Save Config)'}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user