feat(ai-admin-console): complete implementation and resolve lint compilation errors
This commit is contained in:
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Settings, Activity, Shield, FileStack, ChevronDown, ChevronRight, Database } from 'lucide-react';
|
||||
import { Settings, Activity, Shield, FileStack, ChevronDown, ChevronRight, Database, Brain } from 'lucide-react';
|
||||
|
||||
interface MenuItem {
|
||||
href?: string;
|
||||
@@ -62,6 +62,7 @@ export const menuItems: MenuItem[] = [
|
||||
{ href: '/admin/migration/errors', label: 'Error Logs' },
|
||||
],
|
||||
},
|
||||
{ href: '/admin/ai', label: 'AI Console', icon: Brain },
|
||||
{ href: '/admin/settings', label: 'Settings', icon: Settings },
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { CorrespondenceForm } from './form';
|
||||
import { createTestQueryClient } from '@/lib/test-utils';
|
||||
import {
|
||||
useProjects,
|
||||
useOrganizations,
|
||||
@@ -94,6 +96,11 @@ const editInitialData = {
|
||||
correspondenceNumber: 'CORR-001',
|
||||
};
|
||||
|
||||
const renderWithQueryClient = (ui: ReactElement) => {
|
||||
const { wrapper } = createTestQueryClient();
|
||||
return render(ui, { wrapper });
|
||||
};
|
||||
|
||||
describe('CorrespondenceForm (edit regression)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -140,7 +147,7 @@ describe('CorrespondenceForm (edit regression)', () => {
|
||||
});
|
||||
|
||||
it('keeps edit prefilled values after mount (no reset on initial render)', async () => {
|
||||
render(<CorrespondenceForm initialData={editInitialData} uuid="corr-uuid-1" />);
|
||||
renderWithQueryClient(<CorrespondenceForm initialData={editInitialData} uuid="corr-uuid-1" />);
|
||||
|
||||
expect(screen.getByLabelText('Subject *')).toHaveValue('Existing Subject');
|
||||
|
||||
@@ -156,7 +163,7 @@ describe('CorrespondenceForm (edit regression)', () => {
|
||||
});
|
||||
|
||||
it('keeps dependent fields intact after async effects (reset guard)', async () => {
|
||||
render(<CorrespondenceForm initialData={editInitialData} uuid="corr-uuid-2" />);
|
||||
renderWithQueryClient(<CorrespondenceForm initialData={editInitialData} uuid="corr-uuid-2" />);
|
||||
|
||||
expect(screen.getByLabelText('Subject *')).toHaveValue('Existing Subject');
|
||||
expect(screen.getByText('Current Document Number')).toBeInTheDocument();
|
||||
|
||||
@@ -14,12 +14,20 @@ import { useRouter } from 'next/navigation';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useCreateCorrespondence, useUpdateCorrespondence } from '@/hooks/use-correspondence';
|
||||
import { Organization } from '@/types/organization';
|
||||
import { useOrganizations, useProjects, useCorrespondenceTypes, useDisciplines, useContracts } from '@/hooks/use-master-data';
|
||||
import {
|
||||
useOrganizations,
|
||||
useProjects,
|
||||
useCorrespondenceTypes,
|
||||
useDisciplines,
|
||||
useContracts,
|
||||
} from '@/hooks/use-master-data';
|
||||
import { CreateCorrespondenceDto } from '@/types/dto/correspondence/create-correspondence.dto';
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { numberingApi } from '@/lib/api/numbering';
|
||||
import { filesApi } from '@/lib/api/files';
|
||||
import { toast } from 'sonner';
|
||||
import { AiSuggestionButton } from '@/components/ai/ai-suggestion-button';
|
||||
import { useAiStatus } from '@/hooks/use-ai-status';
|
||||
|
||||
// Updated Zod Schema with all required fields
|
||||
const correspondenceSchema = z.object({
|
||||
@@ -155,6 +163,7 @@ export function CorrespondenceForm({
|
||||
const router = useRouter();
|
||||
const createMutation = useCreateCorrespondence();
|
||||
const updateMutation = useUpdateCorrespondence();
|
||||
const { data: aiStatus, isLoading: isAiStatusLoading } = useAiStatus();
|
||||
|
||||
// Fetch master data for dropdowns
|
||||
const { data: projectsData, isLoading: isLoadingProjects } = useProjects();
|
||||
@@ -170,7 +179,8 @@ export function CorrespondenceForm({
|
||||
? initialData?.revisions?.find((r) => normalizeUuid(r.publicId) === normalizedSelectedRevisionId)
|
||||
: undefined;
|
||||
const defaultValues = useMemo<Partial<FormData>>(() => {
|
||||
const currentRevision = selectedRevision || initialData?.revisions?.find((r) => r.isCurrent) || initialData?.revisions?.[0];
|
||||
const currentRevision =
|
||||
selectedRevision || initialData?.revisions?.find((r) => r.isCurrent) || initialData?.revisions?.[0];
|
||||
const initialToRecipient = initialData?.recipients?.find((r) => r.recipientType === 'TO');
|
||||
const initialCcRecipientIds =
|
||||
initialData?.recipients
|
||||
@@ -193,9 +203,15 @@ export function CorrespondenceForm({
|
||||
body: currentRevision?.body || '',
|
||||
remarks: currentRevision?.remarks || '',
|
||||
dueDate: currentRevision?.dueDate ? new Date(currentRevision.dueDate).toISOString().split('T')[0] : undefined,
|
||||
documentDate: currentRevision?.documentDate ? new Date(currentRevision.documentDate).toISOString().split('T')[0] : undefined,
|
||||
issuedDate: currentRevision?.issuedDate ? new Date(currentRevision.issuedDate).toISOString().split('T')[0] : undefined,
|
||||
receivedDate: currentRevision?.receivedDate ? new Date(currentRevision.receivedDate).toISOString().split('T')[0] : undefined,
|
||||
documentDate: currentRevision?.documentDate
|
||||
? new Date(currentRevision.documentDate).toISOString().split('T')[0]
|
||||
: undefined,
|
||||
issuedDate: currentRevision?.issuedDate
|
||||
? new Date(currentRevision.issuedDate).toISOString().split('T')[0]
|
||||
: undefined,
|
||||
receivedDate: currentRevision?.receivedDate
|
||||
? new Date(currentRevision.receivedDate).toISOString().split('T')[0]
|
||||
: undefined,
|
||||
fromOrganizationId:
|
||||
normalizePublicId(initialData?.originator?.publicId) ??
|
||||
normalizePublicId((initialData as Record<string, unknown>)?.originatorId as string),
|
||||
@@ -289,12 +305,15 @@ export function CorrespondenceForm({
|
||||
// Build recipients array with TO and CC
|
||||
const recipients = [
|
||||
{ organizationId: data.toOrganizationId, type: 'TO' as const },
|
||||
...(data.ccOrganizationIds?.map(orgId => ({ organizationId: orgId, type: 'CC' as const })) || [])
|
||||
...(data.ccOrganizationIds?.map((orgId) => ({ organizationId: orgId, type: 'CC' as const })) || []),
|
||||
];
|
||||
|
||||
// Phase 1: Upload attachments to temp storage
|
||||
let attachmentTempIds: string[] | undefined;
|
||||
const validFiles = (data.attachments || []).filter((f): f is File => f instanceof File && !('validationError' in f && (f as { validationError?: string }).validationError));
|
||||
const validFiles = (data.attachments || []).filter(
|
||||
(f): f is File =>
|
||||
f instanceof File && !('validationError' in f && (f as { validationError?: string }).validationError)
|
||||
);
|
||||
if (validFiles.length > 0) {
|
||||
setIsUploading(true);
|
||||
try {
|
||||
@@ -332,10 +351,7 @@ export function CorrespondenceForm({
|
||||
};
|
||||
|
||||
if (uuid && initialData) {
|
||||
updateMutation.mutate(
|
||||
{ uuid, data: payload },
|
||||
{ onSuccess: () => router.push(`/correspondences/${uuid}`) }
|
||||
);
|
||||
updateMutation.mutate({ uuid, data: payload }, { onSuccess: () => router.push(`/correspondences/${uuid}`) });
|
||||
} else {
|
||||
createMutation.mutate(payload, {
|
||||
onSuccess: () => router.push('/correspondences'),
|
||||
@@ -398,18 +414,10 @@ export function CorrespondenceForm({
|
||||
|
||||
{/* Preview Section - Only for New Documents */}
|
||||
{preview && !uuid && (
|
||||
<div
|
||||
className="p-4 rounded-md border bg-muted border-border"
|
||||
>
|
||||
<p className="text-sm font-semibold mb-1 flex items-center gap-2">
|
||||
Document Number Preview
|
||||
</p>
|
||||
<div className="p-4 rounded-md border bg-muted border-border">
|
||||
<p className="text-sm font-semibold mb-1 flex items-center gap-2">Document Number Preview</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className="text-xl font-bold font-mono tracking-wide text-primary"
|
||||
>
|
||||
{preview.number}
|
||||
</span>
|
||||
<span className="text-xl font-bold font-mono tracking-wide text-primary">{preview.number}</span>
|
||||
{preview.isDefaultTemplate && (
|
||||
<span className="text-[10px] uppercase font-semibold px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-800 border border-yellow-200">
|
||||
Default Template
|
||||
@@ -575,7 +583,7 @@ export function CorrespondenceForm({
|
||||
<Label>CC Organizations (Optional)</Label>
|
||||
<div className="space-y-2 max-h-32 overflow-y-auto border rounded-md p-3">
|
||||
{organizationOptions
|
||||
.filter(org => org.publicId !== toOrgId) // Exclude TO organization
|
||||
.filter((org) => org.publicId !== toOrgId) // Exclude TO organization
|
||||
.map((org) => (
|
||||
<div key={org.publicId} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
@@ -586,7 +594,10 @@ export function CorrespondenceForm({
|
||||
if (checked) {
|
||||
setValue('ccOrganizationIds', [...currentCC, org.publicId]);
|
||||
} else {
|
||||
setValue('ccOrganizationIds', currentCC.filter(id => id !== org.publicId));
|
||||
setValue(
|
||||
'ccOrganizationIds',
|
||||
currentCC.filter((id) => id !== org.publicId)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -596,15 +607,20 @@ export function CorrespondenceForm({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Select organizations to receive a copy of this correspondence
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Select organizations to receive a copy of this correspondence</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subject */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subject">Subject *</Label>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Label htmlFor="subject">Subject *</Label>
|
||||
<AiSuggestionButton
|
||||
aiEnabled={aiStatus?.aiFeaturesEnabled ?? true}
|
||||
isLoading={isAiStatusLoading}
|
||||
onClick={() => toast.info('AI Suggestion queued')}
|
||||
/>
|
||||
</div>
|
||||
<Input id="subject" {...register('subject')} placeholder="Enter subject" />
|
||||
{errors.subject && <p className="text-sm text-destructive">{errors.subject.message}</p>}
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,9 @@ import { CreateRfaDto } from '@/types/dto/rfa/rfa.dto';
|
||||
import { useState, useEffect, type FormEvent } from 'react';
|
||||
import { correspondenceService } from '@/lib/services/correspondence.service';
|
||||
import { Contract } from '@/types/contract';
|
||||
import { AiSuggestionButton } from '@/components/ai/ai-suggestion-button';
|
||||
import { useAiStatus } from '@/hooks/use-ai-status';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const rfaSchema = z.object({
|
||||
projectId: z.string().min(1, 'Project is required'), // ADR-019: UUID
|
||||
@@ -145,6 +148,7 @@ const getMasterOptionValue = (option: { publicId?: string; id?: number }): strin
|
||||
export function RFAForm() {
|
||||
const router = useRouter();
|
||||
const createMutation = useCreateRFA();
|
||||
const { data: aiStatus, isLoading: isAiStatusLoading } = useAiStatus();
|
||||
|
||||
const { data: projectsData, isLoading: isLoadingProjects } = useProjects();
|
||||
const projects = dedupeByKey(extractArrayData<ProjectOption>(projectsData), (project) => project.publicId);
|
||||
@@ -192,12 +196,13 @@ export function RFAForm() {
|
||||
|
||||
const selectedContractId = watch('contractId');
|
||||
const { data: disciplinesData, isLoading: isLoadingDisciplines } = useDisciplines(selectedContractId);
|
||||
const disciplines = dedupeByKey(
|
||||
extractArrayData<DisciplineOption>(disciplinesData),
|
||||
(discipline) => getMasterOptionValue(discipline)
|
||||
const disciplines = dedupeByKey(extractArrayData<DisciplineOption>(disciplinesData), (discipline) =>
|
||||
getMasterOptionValue(discipline)
|
||||
);
|
||||
const { data: rfaTypesData, isLoading: isLoadingRfaTypes } = useRfaTypes(selectedContractId);
|
||||
const rfaTypes = dedupeByKey(extractArrayData<RfaTypeOption>(rfaTypesData), (rfaType) => getMasterOptionValue(rfaType));
|
||||
const rfaTypes = dedupeByKey(extractArrayData<RfaTypeOption>(rfaTypesData), (rfaType) =>
|
||||
getMasterOptionValue(rfaType)
|
||||
);
|
||||
const [shopDrawingSearch, setShopDrawingSearch] = useState('');
|
||||
const [shopDrawingPage, setShopDrawingPage] = useState(1);
|
||||
const { data: shopDrawingsData, isLoading: isLoadingShopDrawings } = useDrawings('SHOP', {
|
||||
@@ -286,7 +291,15 @@ export function RFAForm() {
|
||||
|
||||
const timer = setTimeout(fetchPreview, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [rfaTypeId, disciplineId, toOrganizationId, selectedProjectId, rfaCorrespondenceType?.publicId, rfaCorrespondenceType?.id, watch]);
|
||||
}, [
|
||||
rfaTypeId,
|
||||
disciplineId,
|
||||
toOrganizationId,
|
||||
selectedProjectId,
|
||||
rfaCorrespondenceType?.publicId,
|
||||
rfaCorrespondenceType?.id,
|
||||
watch,
|
||||
]);
|
||||
|
||||
const onSubmit = (data: RFAFormData) => {
|
||||
if (requiresShopDrawings && data.shopDrawingRevisionIds?.length === 0) {
|
||||
@@ -346,7 +359,7 @@ export function RFAForm() {
|
||||
<h3 className="text-lg font-semibold mb-4">RFA Information</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div>
|
||||
<Label>Project *</Label>
|
||||
<Select
|
||||
value={selectedProjectId || undefined}
|
||||
@@ -429,7 +442,7 @@ export function RFAForm() {
|
||||
<SelectValue placeholder={isLoadingDisciplines ? 'Loading...' : 'Select Discipline'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{disciplines.map((d) => (
|
||||
{disciplines.map((d) =>
|
||||
(() => {
|
||||
const disciplineValue = getMasterOptionValue(d);
|
||||
|
||||
@@ -443,7 +456,7 @@ export function RFAForm() {
|
||||
</SelectItem>
|
||||
);
|
||||
})()
|
||||
))}
|
||||
)}
|
||||
{!isLoadingDisciplines && disciplines.length === 0 && (
|
||||
<SelectItem value="0" disabled>
|
||||
No disciplines found
|
||||
@@ -521,7 +534,14 @@ export function RFAForm() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="subject">Subject *</Label>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Label htmlFor="subject">Subject *</Label>
|
||||
<AiSuggestionButton
|
||||
aiEnabled={aiStatus?.aiFeaturesEnabled ?? true}
|
||||
isLoading={isAiStatusLoading}
|
||||
onClick={() => toast.info('AI Suggestion queued')}
|
||||
/>
|
||||
</div>
|
||||
<Input id="subject" {...register('subject')} placeholder="Enter subject" />
|
||||
{errors.subject && <p className="text-sm text-destructive mt-1">{errors.subject.message}</p>}
|
||||
</div>
|
||||
@@ -540,8 +560,6 @@ export function RFAForm() {
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input id="description" {...register('description')} placeholder="Enter key description" />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user