Files
lcbp3/frontend/lib/api/ai.ts
T
admin 1a162bf320
CI / CD Pipeline / build (push) Successful in 4m54s
CI / CD Pipeline / deploy (push) Failing after 12m9s
feat(rfa-ai): Complete RFA Approval Refactor and AI Model Revision
2026-05-16 10:59:53 +07:00

226 lines
6.4 KiB
TypeScript

// 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<string, unknown>;
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<string, unknown>;
}
interface WrappedData<T> {
data?: T;
}
const extractData = <T>(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<unknown>).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<AiStagingQueueResponse> => {
const response = await apiClient.get('/ai/legacy-migration/queue', {
params: { status, page: 1, limit: 50 },
});
return extractData<AiStagingQueueResponse>(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<AiRagJobResult> => {
const response = await apiClient.get(`/ai/rag/jobs/${requestPublicId}`);
return extractData<AiRagJobResult>(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<void> => {
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<AiAnalyticsSummary> => {
const response = await apiClient.get('/ai/analytics/summary');
return extractData<AiAnalyticsSummary>(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 });
},
});
}