690619:0928 239 #03
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
// - 2026-06-13: ADR-036 — ใช้ canonical model constants สำหรับหน้า AI Admin Console
|
||||
// - 2026-06-18: อัปเดต OCR Sandbox tab ให้ใช้ PromptManagementTabs และ SandboxTabs ตาม spec 238 (FR-006, FR-011, FR-013)
|
||||
// - 2026-06-18: [239] ปรับ AI Console UX ให้ health/system controls แสดงทุก tab และเปลี่ยนชื่อ sandbox tab ให้ตรงกับ 3-step pipeline.
|
||||
// - 2026-06-19: [240] เพิ่มฟีเจอร์ย่อ/ขยายสำหรับกลุ่มการ์ดตรวจติดตามสุขภาพระบบ AI และการ์ดเดี่ยวพร้อมเก็บสถานะใน localStorage
|
||||
|
||||
'use client';
|
||||
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
HelpCircle,
|
||||
AlertCircle,
|
||||
ScanText,
|
||||
ChevronUp,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -131,6 +133,44 @@ export default function AiAdminConsolePage() {
|
||||
const [isSandboxPolling, setIsSandboxPolling] = useState<boolean>(false);
|
||||
const [sandboxProgress, setSandboxProgress] = useState<number>(0);
|
||||
const [sandboxStatusText, setSandboxStatusText] = useState<string>('');
|
||||
const [isSectionCollapsed, setIsSectionCollapsed] = useState<boolean>(false);
|
||||
const [collapsedCards, setCollapsedCards] = useState<{
|
||||
ollama: boolean;
|
||||
qdrant: boolean;
|
||||
ocr: boolean;
|
||||
bullmq: boolean;
|
||||
vram: boolean;
|
||||
}>({
|
||||
ollama: false,
|
||||
qdrant: false,
|
||||
ocr: false,
|
||||
bullmq: false,
|
||||
vram: false,
|
||||
});
|
||||
useEffect(() => {
|
||||
const savedSection = localStorage.getItem('ai_console_section_collapsed');
|
||||
if (savedSection !== null) {
|
||||
setIsSectionCollapsed(savedSection === 'true');
|
||||
}
|
||||
const savedCards = localStorage.getItem('ai_console_cards_collapsed');
|
||||
if (savedCards) {
|
||||
try {
|
||||
setCollapsedCards(JSON.parse(savedCards));
|
||||
} catch (_e) {
|
||||
// เงียบข้อผิดพลาด
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
const toggleSection = () => {
|
||||
const nextVal = !isSectionCollapsed;
|
||||
setIsSectionCollapsed(nextVal);
|
||||
localStorage.setItem('ai_console_section_collapsed', String(nextVal));
|
||||
};
|
||||
const toggleCard = (cardKey: keyof typeof collapsedCards) => {
|
||||
const nextCards = { ...collapsedCards, [cardKey]: !collapsedCards[cardKey] };
|
||||
setCollapsedCards(nextCards);
|
||||
localStorage.setItem('ai_console_cards_collapsed', JSON.stringify(nextCards));
|
||||
};
|
||||
|
||||
// VRAM Monitoring State (T034, T036, US2)
|
||||
const { data: vramStatus, refetch: refetchVram } = useQuery({
|
||||
@@ -276,227 +316,303 @@ export default function AiAdminConsolePage() {
|
||||
{aiEnabled ? 'AI Enabled' : 'AI Disabled'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-background/50 backdrop-blur-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Cpu className="h-4 w-4 text-primary" />
|
||||
Ollama AI Engine
|
||||
</CardTitle>
|
||||
{isHealthLoading ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
renderStatusBadge(health?.ollama?.status)
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>ความเร็วตอบสนอง</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{health?.ollama?.latencyMs !== undefined ? `${health.ollama.latencyMs} ms` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">โมเดลที่โหลดอยู่:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{healthOllamaModels.length > 0 ? (
|
||||
healthOllamaModels.map((m) => (
|
||||
<Badge key={m} variant="secondary" className="text-[10px] py-0 px-1">
|
||||
{m}
|
||||
</Badge>
|
||||
))
|
||||
<div className="flex items-center justify-between border-b pb-2 mb-4">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Activity className="h-5 w-5 text-primary" />
|
||||
AI Engine Infrastructure Monitoring
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||
onClick={toggleSection}
|
||||
>
|
||||
<ChevronUp className={`h-5 w-5 transition-transform duration-300 ${isSectionCollapsed ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`transition-all duration-300 ease-in-out ${isSectionCollapsed ? 'max-h-0 opacity-0 overflow-hidden pointer-events-none' : 'max-h-[2000px] opacity-100'}`}>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-background/50 backdrop-blur-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Cpu className="h-4 w-4 text-primary" />
|
||||
Ollama AI Engine
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{isHealthLoading ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<span className="text-[10px] text-muted-foreground italic">ไม่มีโมเดลที่โหลดอยู่</span>
|
||||
renderStatusBadge(health?.ollama?.status)
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => toggleCard('ollama')}
|
||||
>
|
||||
<ChevronUp className={`h-4 w-4 transition-transform duration-300 ${collapsedCards.ollama ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div className={`transition-all duration-300 ease-in-out ${collapsedCards.ollama ? 'max-h-0 opacity-0 overflow-hidden' : 'max-h-[500px] opacity-100'}`}>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>ความเร็วตอบสนอง</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{health?.ollama?.latencyMs !== undefined ? `${health.ollama.latencyMs} ms` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">โมเดลที่โหลดอยู่:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{healthOllamaModels.length > 0 ? (
|
||||
healthOllamaModels.map((m) => (
|
||||
<Badge key={m} variant="secondary" className="text-[10px] py-0 px-1">
|
||||
{m}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[10px] text-muted-foreground italic">ไม่มีโมเดลที่โหลดอยู่</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{health?.ollama?.error && (
|
||||
<p className="mt-1 text-[10px] text-destructive line-clamp-2">{health.ollama.error}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
{health?.ollama?.error && (
|
||||
<p className="mt-1 text-[10px] text-destructive line-clamp-2">{health.ollama.error}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-background/50 backdrop-blur-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Database className="h-4 w-4 text-primary" />
|
||||
Qdrant Vector DB
|
||||
</CardTitle>
|
||||
{isHealthLoading ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
renderStatusBadge(health?.qdrant?.status)
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>ความเร็วตอบสนอง</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{health?.qdrant?.latencyMs !== undefined ? `${health.qdrant.latencyMs} ms` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">คอลเลกชัน:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{healthQdrantCollections.length > 0 ? (
|
||||
healthQdrantCollections.map((c) => (
|
||||
<Badge key={c} variant="outline" className="text-[10px] py-0 px-1 bg-background/30">
|
||||
{c}
|
||||
</Badge>
|
||||
))
|
||||
</Card>
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-background/50 backdrop-blur-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Database className="h-4 w-4 text-primary" />
|
||||
Qdrant Vector DB
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{isHealthLoading ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<span className="text-[10px] text-muted-foreground italic">ไม่มีคอลเลกชัน</span>
|
||||
renderStatusBadge(health?.qdrant?.status)
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => toggleCard('qdrant')}
|
||||
>
|
||||
<ChevronUp className={`h-4 w-4 transition-transform duration-300 ${collapsedCards.qdrant ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{health?.qdrant?.error && (
|
||||
<p className="mt-1 text-[10px] text-destructive line-clamp-2">{health.qdrant.error}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-background/50 backdrop-blur-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<ScanText className="h-4 w-4 text-primary" />
|
||||
OCR Sidecar (np-dms-ocr)
|
||||
</CardTitle>
|
||||
{isHealthLoading ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
renderStatusBadge(health?.ocr?.status)
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>ความเร็วตอบสนอง</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{health?.ocr?.latencyMs !== undefined ? `${health.ocr.latencyMs} ms` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>URL</span>
|
||||
<span className="font-mono text-[10px] text-foreground truncate max-w-[160px]">
|
||||
{health?.ocr?.url ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
{health?.ocr?.error && <p className="mt-1 text-[10px] text-destructive line-clamp-2">{health.ocr.error}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-background/50 backdrop-blur-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Activity className="h-4 w-4 text-primary" />
|
||||
BullMQ Queue Health
|
||||
</CardTitle>
|
||||
{isHealthLoading ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{health?.timestamp ? new Date(health.timestamp).toLocaleTimeString() : 'N/A'}
|
||||
</Badge>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex items-center justify-between font-medium text-[11px] border-b pb-1 mb-1">
|
||||
<span>คิว / สถานะงาน</span>
|
||||
<span>Active / Waiting / Failed</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<span className="flex items-center gap-1 font-mono">
|
||||
realtime
|
||||
{health?.queues?.realtime?.isPaused && (
|
||||
<span className="text-[9px] text-amber-500 font-sans">(Paused)</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{health?.queues?.realtime?.active ?? 0} / {health?.queues?.realtime?.waiting ?? 0} /{' '}
|
||||
<span className={(health?.queues?.realtime?.failed ?? 0) > 0 ? 'text-destructive' : ''}>
|
||||
{health?.queues?.realtime?.failed ?? 0}
|
||||
</CardHeader>
|
||||
<div className={`transition-all duration-300 ease-in-out ${collapsedCards.qdrant ? 'max-h-0 opacity-0 overflow-hidden' : 'max-h-[500px] opacity-100'}`}>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>ความเร็วตอบสนอง</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{health?.qdrant?.latencyMs !== undefined ? `${health.qdrant.latencyMs} ms` : '-'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<span className="flex items-center gap-1 font-mono">
|
||||
batch
|
||||
{health?.queues?.batch?.isPaused && (
|
||||
<span className="text-[9px] text-amber-500 font-sans">(Paused)</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{health?.queues?.batch?.active ?? 0} / {health?.queues?.batch?.waiting ?? 0} /{' '}
|
||||
<span className={(health?.queues?.batch?.failed ?? 0) > 0 ? 'text-destructive' : ''}>
|
||||
{health?.queues?.batch?.failed ?? 0}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">คอลเลกชัน:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{healthQdrantCollections.length > 0 ? (
|
||||
healthQdrantCollections.map((c) => (
|
||||
<Badge key={c} variant="outline" className="text-[10px] py-0 px-1 bg-background/30">
|
||||
{c}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[10px] text-muted-foreground italic">ไม่มีคอลเลกชัน</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{health?.qdrant?.error && (
|
||||
<p className="mt-1 text-[10px] text-destructive line-clamp-2">{health.qdrant.error}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
{(health?.queues?.realtime?.error || health?.queues?.batch?.error) && (
|
||||
<p className="mt-1 text-[10px] text-destructive line-clamp-1">
|
||||
{health.queues.realtime.error || health.queues.batch.error}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-background/50 backdrop-blur-md md:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Cpu className="h-4 w-4 text-primary" />
|
||||
VRAM GPU Monitor
|
||||
</CardTitle>
|
||||
{vramStatus ? (
|
||||
<Badge variant={vramStatus.usagePercent > 85 ? 'destructive' : 'secondary'} className="text-[10px]">
|
||||
{vramStatus.usagePercent}% Used
|
||||
</Badge>
|
||||
) : (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{vramStatus ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">GPU VRAM Usage</span>
|
||||
</Card>
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-background/50 backdrop-blur-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<ScanText className="h-4 w-4 text-primary" />
|
||||
OCR Sidecar (np-dms-ocr)
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{isHealthLoading ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
renderStatusBadge(health?.ocr?.status)
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => toggleCard('ocr')}
|
||||
>
|
||||
<ChevronUp className={`h-4 w-4 transition-transform duration-300 ${collapsedCards.ocr ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div className={`transition-all duration-300 ease-in-out ${collapsedCards.ocr ? 'max-h-0 opacity-0 overflow-hidden' : 'max-h-[500px] opacity-100'}`}>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>ความเร็วตอบสนอง</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{health?.ocr?.latencyMs !== undefined ? `${health.ocr.latencyMs} ms` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>URL</span>
|
||||
<span className="font-mono text-[10px] text-foreground truncate max-w-[160px]">
|
||||
{health?.ocr?.url ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
{health?.ocr?.error && <p className="mt-1 text-[10px] text-destructive line-clamp-2">{health.ocr.error}</p>}
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-background/50 backdrop-blur-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Activity className="h-4 w-4 text-primary" />
|
||||
BullMQ Queue Health
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{isHealthLoading ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{health?.timestamp ? new Date(health.timestamp).toLocaleTimeString() : 'N/A'}
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => toggleCard('bullmq')}
|
||||
>
|
||||
<ChevronUp className={`h-4 w-4 transition-transform duration-300 ${collapsedCards.bullmq ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div className={`transition-all duration-300 ease-in-out ${collapsedCards.bullmq ? 'max-h-0 opacity-0 overflow-hidden' : 'max-h-[500px] opacity-100'}`}>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex items-center justify-between font-medium text-[11px] border-b pb-1 mb-1">
|
||||
<span>คิว / สถานะงาน</span>
|
||||
<span>Active / Waiting / Failed</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<span className="flex items-center gap-1 font-mono">
|
||||
realtime
|
||||
{health?.queues?.realtime?.isPaused && (
|
||||
<span className="text-[9px] text-amber-500 font-sans">(Paused)</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{vramStatus.usedVRAMMB} MB / {vramStatus.totalVRAMMB} MB
|
||||
{health?.queues?.realtime?.active ?? 0} / {health?.queues?.realtime?.waiting ?? 0} /{' '}
|
||||
<span className={(health?.queues?.realtime?.failed ?? 0) > 0 ? 'text-destructive' : ''}>
|
||||
{health?.queues?.realtime?.failed ?? 0}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={vramStatus.usagePercent} className="h-2" />
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1 text-xs">
|
||||
<span className="text-muted-foreground block">โมเดลที่โหลดบน GPU ในปัจจุบัน:</span>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{vramLoadedModels.length > 0 ? (
|
||||
vramLoadedModels.map((m) => (
|
||||
<Badge
|
||||
key={m.modelId}
|
||||
className="bg-primary/10 text-primary border-none hover:bg-primary/20 text-[10px]"
|
||||
>
|
||||
{m.modelName}
|
||||
{typeof m.vramUsageMB === 'number' ? ` (${m.vramUsageMB} MB)` : ''}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[10px] text-muted-foreground italic">
|
||||
ไม่มีโมเดลที่โหลดค้างในหน่วยความจำ
|
||||
</span>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<span className="flex items-center gap-1 font-mono">
|
||||
batch
|
||||
{health?.queues?.batch?.isPaused && (
|
||||
<span className="text-[9px] text-amber-500 font-sans">(Paused)</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs sm:text-right">
|
||||
<span className="text-muted-foreground block">ความสามารถในการโหลดโมเดลใหม่:</span>
|
||||
<Badge variant={vramStatus.canLoadModel ? 'default' : 'destructive'} className="mt-1 text-[10px]">
|
||||
{vramStatus.canLoadModel ? 'พร้อมโหลดโมเดลหลัก' : 'หน่วยความจำไม่เพียงพอ (OOM Guard)'}
|
||||
</Badge>
|
||||
</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{health?.queues?.batch?.active ?? 0} / {health?.queues?.batch?.waiting ?? 0} /{' '}
|
||||
<span className={(health?.queues?.batch?.failed ?? 0) > 0 ? 'text-destructive' : ''}>
|
||||
{health?.queues?.batch?.failed ?? 0}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground italic text-center py-4">กำลังดึงข้อมูลสถานะ GPU VRAM...</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{(health?.queues?.realtime?.error || health?.queues?.batch?.error) && (
|
||||
<p className="mt-1 text-[10px] text-destructive line-clamp-1">
|
||||
{health.queues.realtime.error || health.queues.batch.error}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="relative overflow-hidden border border-border/50 bg-background/50 backdrop-blur-md md:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Cpu className="h-4 w-4 text-primary" />
|
||||
VRAM GPU Monitor
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{vramStatus ? (
|
||||
<Badge variant={vramStatus.usagePercent > 85 ? 'destructive' : 'secondary'} className="text-[10px]">
|
||||
{vramStatus.usagePercent}% Used
|
||||
</Badge>
|
||||
) : (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => toggleCard('vram')}
|
||||
>
|
||||
<ChevronUp className={`h-4 w-4 transition-transform duration-300 ${collapsedCards.vram ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div className={`transition-all duration-300 ease-in-out ${collapsedCards.vram ? 'max-h-0 opacity-0 overflow-hidden' : 'max-h-[500px] opacity-100'}`}>
|
||||
<CardContent className="space-y-4">
|
||||
{vramStatus ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">GPU VRAM Usage</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{vramStatus.usedVRAMMB} MB / {vramStatus.totalVRAMMB} MB
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={vramStatus.usagePercent} className="h-2" />
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1 text-xs">
|
||||
<span className="text-muted-foreground block">โมเดลที่โหลดบน GPU ในปัจจุบัน:</span>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{vramLoadedModels.length > 0 ? (
|
||||
vramLoadedModels.map((m) => (
|
||||
<Badge
|
||||
key={m.modelId}
|
||||
className="bg-primary/10 text-primary border-none hover:bg-primary/20 text-[10px]"
|
||||
>
|
||||
{m.modelName}
|
||||
{typeof m.vramUsageMB === 'number' ? ` (${m.vramUsageMB} MB)` : ''}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[10px] text-muted-foreground italic">
|
||||
ไม่มีโมเดลที่โหลดค้างในหน่วยความจำ
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs sm:text-right">
|
||||
<span className="text-muted-foreground block">ความสามารถในการโหลดโมเดลใหม่:</span>
|
||||
<Badge variant={vramStatus.canLoadModel ? 'default' : 'destructive'} className="mt-1 text-[10px]">
|
||||
{vramStatus.canLoadModel ? 'พร้อมโหลดโมเดลหลัก' : 'หน่วยความจำไม่เพียงพอ (OOM Guard)'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground italic text-center py-4">กำลังดึงข้อมูลสถานะ GPU VRAM...</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
|
||||
Reference in New Issue
Block a user