feat(ai-admin-console): complete implementation and resolve lint compilation errors

This commit is contained in:
2026-05-21 21:42:25 +07:00
parent 1580ab2c18
commit 91e9c714df
39 changed files with 3724 additions and 72 deletions
+7 -5
View File
@@ -1,6 +1,7 @@
// File: components/ai/AiStatusBanner.tsx
// Change Log
// - 2026-05-14: เพิ่ม banner สำหรับ graceful degradation ของ AI staging.
// - 2026-05-21: รองรับ global banner เมื่อ Superadmin ปิด AI features.
'use client';
import { AlertTriangle, CheckCircle2 } from 'lucide-react';
@@ -8,19 +9,20 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { useTranslations } from '@/hooks/use-translations';
interface AiStatusBannerProps {
isOffline: boolean;
isOffline?: boolean;
aiEnabled?: boolean;
queuePaused?: boolean;
}
export function AiStatusBanner({ isOffline, queuePaused = false }: AiStatusBannerProps) {
export function AiStatusBanner({ isOffline = false, aiEnabled = true, queuePaused = false }: AiStatusBannerProps) {
const t = useTranslations();
if (isOffline) {
if (isOffline || !aiEnabled) {
return (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>{t('ai.service_unavailable')}</AlertTitle>
<AlertDescription>{t('ai.status.offlineDescription')}</AlertDescription>
<AlertTitle>{t('ai.status.offlineTitle')}</AlertTitle>
<AlertDescription>{t('ai.status.disabledDescription')}</AlertDescription>
</Alert>
);
}
@@ -0,0 +1,29 @@
// File: components/ai/__tests__/ai-suggestion-button.test.tsx
// Change Log
// - 2026-05-21: เพิ่ม unit tests สำหรับ soft fallback ของปุ่ม AI suggestion.
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { AiSuggestionButton } from '../ai-suggestion-button';
describe('AiSuggestionButton', () => {
it('ควร disable และแสดงข้อความ fallback เมื่อ AI ถูกปิด', () => {
const onClick = vi.fn();
render(<AiSuggestionButton aiEnabled={false} onClick={onClick} />);
const button = screen.getByRole('button', { name: /AI Suggestion/i });
expect(button).toBeDisabled();
expect(screen.getByText('ระบบ AI ไม่พร้อมใช้งานชั่วคราว กรุณากรอกข้อมูลด้วยตนเอง')).toBeInTheDocument();
fireEvent.click(button);
expect(onClick).not.toHaveBeenCalled();
});
it('ควรเรียก onClick เมื่อ AI เปิดใช้งาน', () => {
const onClick = vi.fn();
render(<AiSuggestionButton aiEnabled={true} onClick={onClick} />);
fireEvent.click(screen.getByRole('button', { name: /AI Suggestion/i }));
expect(onClick).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,28 @@
// File: components/ai/ai-status-banner-host.tsx
// Change Log
// - 2026-05-21: เพิ่ม host สำหรับ global AI disabled banner เฉพาะผู้ใช้ที่มีสิทธิ์ AI.
'use client';
import { useEffect, useState } from 'react';
import { AiStatusBanner } from './AiStatusBanner';
import { useCurrentUserAiStatus } from '@/hooks/use-ai-status';
import { AI_FEATURES_UNAVAILABLE_EVENT } from '@/lib/api/client';
/** แสดง global banner เมื่อ AI ถูกปิดสำหรับผู้ใช้ที่มีสิทธิ์ AI */
export function AiStatusBannerHost() {
const [serviceUnavailable, setServiceUnavailable] = useState(false);
const { data, isLoading } = useCurrentUserAiStatus();
useEffect(() => {
const handleAiUnavailable = () => setServiceUnavailable(true);
window.addEventListener(AI_FEATURES_UNAVAILABLE_EVENT, handleAiUnavailable);
return () => window.removeEventListener(AI_FEATURES_UNAVAILABLE_EVENT, handleAiUnavailable);
}, []);
if (isLoading || (data?.shouldShowBanner !== true && !serviceUnavailable)) return null;
return (
<div className="sticky top-0 z-40 border-b bg-background px-4 py-2">
<AiStatusBanner aiEnabled={serviceUnavailable ? false : data?.aiFeaturesEnabled} />
</div>
);
}
@@ -0,0 +1,59 @@
// File: components/ai/ai-suggestion-button.tsx
// Change Log
// - 2026-05-21: เพิ่มปุ่ม AI Suggestion พร้อม soft fallback เมื่อ AI ถูกปิด.
'use client';
import { Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
const DEFAULT_DISABLED_MESSAGE = 'ระบบ AI ไม่พร้อมใช้งานชั่วคราว กรุณากรอกข้อมูลด้วยตนเอง';
interface AiSuggestionButtonProps {
aiEnabled: boolean;
isLoading?: boolean;
label?: string;
disabledMessage?: string;
onClick?: () => void;
}
/** ปุ่มเรียก AI suggestion ที่แสดง fallback ชัดเจนเมื่อระบบ AI ปิด */
export function AiSuggestionButton({
aiEnabled,
isLoading = false,
label = 'AI Suggestion',
disabledMessage = DEFAULT_DISABLED_MESSAGE,
onClick,
}: AiSuggestionButtonProps) {
const disabled = !aiEnabled || isLoading;
const button = (
<Button
type="button"
variant="outline"
size="sm"
disabled={disabled}
aria-label={label}
onClick={disabled ? undefined : onClick}
className="gap-2"
>
<Sparkles className="h-4 w-4" />
{label}
</Button>
);
if (aiEnabled) return button;
return (
<HoverCard openDelay={100}>
<HoverCardTrigger asChild>
<span className="inline-flex cursor-not-allowed">
{button}
<span className="sr-only">{disabledMessage}</span>
</span>
</HoverCardTrigger>
<HoverCardContent className="border-amber-200 bg-amber-50 text-amber-900">
<p className="text-sm">{disabledMessage}</p>
</HoverCardContent>
</HoverCard>
);
}