690525:1320 ADR-028-228-migration #06
CI / CD Pipeline / build (push) Successful in 4m18s
CI / CD Pipeline / deploy (push) Successful in 7m41s

This commit is contained in:
2026-05-25 13:20:17 +07:00
parent dcd1a9855e
commit 001237ea35
18 changed files with 967 additions and 128 deletions
+146 -2
View File
@@ -6,10 +6,11 @@
// - 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 } from 'lucide-react';
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';
@@ -20,7 +21,7 @@ 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 } from '@/lib/services/admin-ai.service';
import { adminAiService, AiSandboxJobResult, AiAvailableModel } from '@/lib/services/admin-ai.service';
import { toast } from 'sonner';
interface SandboxProject {
@@ -48,6 +49,17 @@ export default function AiAdminConsolePage() {
const [isOcrPolling, setIsOcrPolling] = useState<boolean>(false);
const [ocrProgress, setOcrProgress] = useState<number>(0);
const [ocrStatusText, setOcrStatusText] = useState<string>('');
// 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<SandboxProject[]>({
queryKey: ['admin-sandbox-projects'],
queryFn: async () => {
@@ -58,6 +70,37 @@ export default function AiAdminConsolePage() {
const handleToggle = async (enabled: boolean): Promise<void> => {
await toggleMutation.mutateAsync(enabled);
};
const handleModelChange = async (modelName: string): Promise<void> => {
try {
await adminAiService.setActiveModel(modelName);
toast.success(`เปลี่ยนโมเดลเป็น ${modelName} สำเร็จ`);
await refetchModels();
} catch {
toast.error('ไม่สามารถเปลี่ยนโมเดลได้');
}
};
const handleToggleModel = async (modelName: string): Promise<void> => {
try {
await adminAiService.toggleModelActive(modelName);
toast.success(`เปลี่ยนสถานะโมเดล ${modelName} สำเร็จ`);
await refetchModels();
} catch {
toast.error('ไม่สามารถเปลี่ยนสถานะโมเดลได้');
}
};
const handleRemoveModel = async (modelName: string): Promise<void> => {
if (!confirm(`ต้องการลบโมเดล ${modelName} ใช่หรือไม่?`)) return;
try {
await adminAiService.removeModel(modelName);
toast.success(`ลบโมเดล ${modelName} สำเร็จ`);
await refetchModels();
} catch {
toast.error('ไม่สามารถลบโมเดลได้');
}
};
const handleRefreshAll = async (): Promise<void> => {
await Promise.all([refetch(), refetchHealth()]);
};
@@ -389,6 +432,107 @@ export default function AiAdminConsolePage() {
)}
</CardContent>
</Card>
{/* AI Model Management Card (ADR-027) */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Settings2 className="h-5 w-5" />
AI Model Management
<Badge variant="outline" className="text-[10px]">ADR-027</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div className="space-y-2 flex-1">
<label htmlFor="model-select" className="text-sm font-medium text-foreground">
AI (Global)
</label>
<Select
value={activeModel}
onValueChange={handleModelChange}
>
<SelectTrigger id="model-select" className="w-full sm:w-[300px]">
<SelectValue placeholder="-- เลือกโมเดล --" />
</SelectTrigger>
<SelectContent>
{availableModels
.filter((m) => m.isActive)
.map((model) => (
<SelectItem key={model.modelName} value={model.modelName}>
{model.modelName}
{model.isDefault && (
<Badge variant="secondary" className="ml-2 text-[10px]">Default</Badge>
)}
{model.vramGb && (
<span className="ml-1 text-muted-foreground">({model.vramGb}GB)</span>
)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="text-sm text-muted-foreground">
: <Badge variant="default">{activeModel || 'Loading...'}</Badge>
</div>
</div>
<div className="border-t pt-4">
<h4 className="text-sm font-medium mb-3"></h4>
<div className="space-y-2">
{availableModels.length === 0 ? (
<p className="text-sm text-muted-foreground"></p>
) : (
availableModels.map((model) => (
<div
key={model.modelName}
className="flex items-center justify-between p-2 rounded border bg-background/50"
>
<div className="flex items-center gap-2">
<Badge
variant={model.isActive ? 'default' : 'secondary'}
className="text-[10px]"
>
{model.isActive ? 'Active' : 'Inactive'}
</Badge>
<span className="text-sm font-medium">{model.modelName}</span>
{model.isDefault && (
<Badge variant="outline" className="text-[10px]">Default</Badge>
)}
{activeModel === model.modelName && (
<Badge variant="default" className="text-[10px] bg-emerald-500">Current</Badge>
)}
</div>
<div className="flex items-center gap-2">
{!model.isDefault && (
<>
<Button
variant="ghost"
size="sm"
onClick={() => handleToggleModel(model.modelName)}
disabled={activeModel === model.modelName && model.isActive}
>
{model.isActive ? 'Deactivate' : 'Activate'}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveModel(model.modelName)}
disabled={model.isDefault || activeModel === model.modelName}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</>
)}
</div>
</div>
))
)}
</div>
</div>
</CardContent>
</Card>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
+55
View File
@@ -4,6 +4,7 @@
// - 2026-05-21: เพิ่ม service method `getHealth` สำหรับดึงข้อมูลสุขภาพของระบบ AI (T028).
// - 2026-05-21: เพิ่ม API service สำหรับ Superadmin Sandbox RAG (T037).
// - 2026-05-21: เพิ่ม service method `submitSandboxExtract` สำหรับอัปโหลดไฟล์ใน OCR Sandbox (T043).
// - 2026-05-25: เพิ่ม methods สำหรับจัดการโมเดล AI แบบไดนามิก (ADR-027).
import api from '../api/client';
@@ -59,6 +60,27 @@ export interface AiSandboxJobResult {
completedAt?: string;
}
export interface AiAvailableModel {
id: number;
modelName: string;
modelVersion: string;
description?: string;
vramGb?: number;
isActive: boolean;
isDefault: boolean;
createdAt: string;
updatedAt: string;
}
export interface AiModelsResponse {
models: AiAvailableModel[];
activeModel: string;
}
export interface AiActiveModelResponse {
activeModel: string;
}
const extractData = <T>(value: unknown): T => {
if (value && typeof value === 'object' && 'data' in value) {
return (value as { data: T }).data;
@@ -110,4 +132,37 @@ export const adminAiService = {
});
return extractData<{ requestPublicId: string; jobId: string; status: string }>(data);
},
// --- AI Model Management (ADR-027) ---
getAvailableModels: async (): Promise<AiModelsResponse> => {
const { data } = await api.get('/ai/admin/models');
return extractData<AiModelsResponse>(data);
},
getActiveModel: async (): Promise<AiActiveModelResponse> => {
const { data } = await api.get('/ai/admin/models/active');
return extractData<AiActiveModelResponse>(data);
},
setActiveModel: async (modelName: string): Promise<AiActiveModelResponse> => {
const { data } = await api.post('/ai/admin/models/active', { modelName });
return extractData<AiActiveModelResponse>(data);
},
addModel: async (
model: Omit<AiAvailableModel, 'id' | 'createdAt' | 'updatedAt'>
): Promise<{ model: AiAvailableModel }> => {
const { data } = await api.post('/ai/admin/models', model);
return extractData<{ model: AiAvailableModel }>(data);
},
toggleModelActive: async (modelName: string): Promise<{ model: AiAvailableModel }> => {
const { data } = await api.patch(`/ai/admin/models/${encodeURIComponent(modelName)}/toggle`);
return extractData<{ model: AiAvailableModel }>(data);
},
removeModel: async (modelName: string): Promise<void> => {
await api.delete(`/ai/admin/models/${encodeURIComponent(modelName)}`);
},
};