// File: lib/api/ai.ts // Change Log // - 2026-05-14: เพิ่ม hooks สำหรับ AI staging queue ตาม ADR-023. 'use client'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import apiClient from '@/lib/api/client'; export enum AiStagingStatus { PENDING = 'PENDING', IMPORTED = 'IMPORTED', REJECTED = 'REJECTED', } export interface AiStagingRecord { publicId: string; batchId: string; originalFileName: string; sourceAttachmentPublicId?: string; extractedMetadata?: Record; confidenceScore?: number; status: AiStagingStatus; errorReason?: string; createdAt: string; updatedAt: string; } export interface AiStagingQueueResponse { items: AiStagingRecord[]; total: number; page: number; limit: number; totalPages: number; } export interface ApproveAiStagingPayload { documentNumber: string; subject: string; categoryCode: string; projectPublicId: string; senderOrganizationPublicId?: string; receiverOrganizationPublicId?: string; issuedDate?: string; receivedDate?: string; body?: string; finalMetadata?: Record; } interface WrappedData { data?: T; } const extractData = (value: unknown): T => { let current: unknown = value; for (let index = 0; index < 5; index += 1) { if (!current || typeof current !== 'object' || !('data' in current)) { return current as T; } current = (current as WrappedData).data; } return current as T; }; export const aiStagingKeys = { all: ['ai-staging'] as const, queue: (status?: AiStagingStatus) => [...aiStagingKeys.all, 'queue', status ?? 'ALL'] as const, }; export function useAiStagingQueue(status?: AiStagingStatus) { return useQuery({ queryKey: aiStagingKeys.queue(status), queryFn: async (): Promise => { const response = await apiClient.get('/ai/legacy-migration/queue', { params: { status, page: 1, limit: 50 }, }); return extractData(response.data); }, staleTime: 30 * 1000, }); } // ─── RAG Query Hooks (Phase 4) ──────────────────────────────────────────────── export type RagJobStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'not_found'; export interface AiRagCitation { pointId: string | number; score: number; docType?: string; docNumber?: string; snippet?: string; } export interface AiRagJobResult { requestPublicId: string; status: RagJobStatus; answer?: string; citations?: AiRagCitation[]; confidence?: number; usedFallbackModel?: boolean; errorMessage?: string; completedAt?: string; } export interface SubmitRagQueryPayload { question: string; projectPublicId: string; } export const ragQueryKeys = { all: ['ai-rag'] as const, job: (requestPublicId: string) => [...ragQueryKeys.all, 'job', requestPublicId] as const, }; export function useSubmitRagQuery() { return useMutation({ mutationFn: async (payload: SubmitRagQueryPayload): Promise<{ requestPublicId: string; jobId: string; status: string }> => { const response = await apiClient.post('/ai/rag/query', payload, { headers: { 'Idempotency-Key': `rag-${Date.now()}` }, }); return extractData<{ requestPublicId: string; jobId: string; status: string }>(response.data); }, }); } export function useRagJobStatus(requestPublicId: string | null, enabled: boolean) { return useQuery({ queryKey: ragQueryKeys.job(requestPublicId ?? ''), queryFn: async (): Promise => { const response = await apiClient.get(`/ai/rag/jobs/${requestPublicId}`); return extractData(response.data); }, enabled: enabled && !!requestPublicId, refetchInterval: (query) => { const status = query.state.data?.status; if (status === 'completed' || status === 'failed' || status === 'cancelled') return false; return 2000; }, staleTime: 0, }); } export function useCancelRagJob() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (requestPublicId: string): Promise => { await apiClient.delete(`/ai/rag/jobs/${requestPublicId}`); }, onSuccess: (_data, requestPublicId) => { void queryClient.invalidateQueries({ queryKey: ragQueryKeys.job(requestPublicId) }); }, }); } export function useApproveAiStagingRecord() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ publicId, payload, }: { publicId: string; payload: ApproveAiStagingPayload; }) => { const response = await apiClient.post( `/ai/legacy-migration/queue/${publicId}/approve`, payload ); return extractData<{ record: AiStagingRecord; importResult: unknown }>( response.data ); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: aiStagingKeys.all }); }, }); } // ─── Phase 6: AI Monitoring & Analytics Hooks (T036, T037) ─────────────────── export interface AiAnalyticsSummary { byDocumentType: Array<{ documentType: string; avgConfidence: number; overrideRate: number; rejectedRate: number; total: number; }>; overall: { avgConfidence: number; overrideRate: number; rejectedRate: number; total: number; }; } export const aiAnalyticsKeys = { all: ['ai-analytics'] as const, summary: () => [...aiAnalyticsKeys.all, 'summary'] as const, }; export function useAiAnalyticsSummary() { return useQuery({ queryKey: aiAnalyticsKeys.summary(), queryFn: async (): Promise => { const response = await apiClient.get('/ai/analytics/summary'); return extractData(response.data); }, staleTime: 5 * 60 * 1000, // Analytics can be cached longer }); } export function useDeleteAiAuditLog() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (publicId: string): Promise<{ deleted: boolean; publicId: string }> => { const response = await apiClient.delete(`/ai/audit-logs/${publicId}`); return extractData<{ deleted: boolean; publicId: string }>(response.data); }, onSuccess: () => { void queryClient.invalidateQueries({ queryKey: aiAnalyticsKeys.all }); }, }); }