690519:1631 224 to 226 AI #01
CI / CD Pipeline / build (push) Failing after 3m57s
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-05-19 16:31:50 +07:00
parent 3e25097470
commit ea5499123e
127 changed files with 12387 additions and 42 deletions
@@ -0,0 +1,76 @@
// File: frontend/hooks/__tests__/use-ai-chat.test.ts
// Change Log:
// - 2026-05-19: สร้าง Unit Test สำหรับ useAiChat Hook
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor, act } from '@testing-library/react';
import { createTestQueryClient } from '@/lib/test-utils';
import { useAiChat } from '../use-ai-chat';
import axios from 'axios';
vi.mock('axios');
describe('useAiChat hook', () => {
const mockContext = { type: 'rfa', publicId: '019505a1-7c3e-7000-8000-abc123def456' };
beforeEach(() => {
vi.clearAllMocks();
if (typeof window !== 'undefined') {
sessionStorage.clear();
}
});
it('ควรตั้งค่าสถานะเริ่มต้นให้ถูกต้อง', () => {
const { wrapper } = createTestQueryClient();
const { result } = renderHook(() => useAiChat(mockContext), { wrapper });
expect(result.current.messages).toEqual([]);
expect(result.current.isOpen).toBe(false);
expect(result.current.isLoading).toBe(false);
});
it('ควรสามารถส่งข้อความและรับคำตอบจาก AI สำเร็จ', async () => {
const mockResponse = {
data: {
content: 'สวัสดีครับ ผมคือผู้ช่วย AI RFA',
messageId: 'assistant-1',
suggestedActions: [{ label: 'ปุ่มแนะนำ', query: 'คำสั่งแนะนำ' }],
},
};
vi.mocked(axios.post).mockResolvedValue(mockResponse);
const { wrapper } = createTestQueryClient();
const { result } = renderHook(() => useAiChat(mockContext), { wrapper });
await act(async () => {
void result.current.sendMessage('สวัสดีครับ');
});
expect(result.current.messages.length).toBe(2);
expect(result.current.messages[0].role).toBe('user');
expect(result.current.messages[0].content).toBe('สวัสดีครับ');
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.messages.length).toBe(2);
expect(result.current.messages[1].role).toBe('assistant');
expect(result.current.messages[1].content).toBe('สวัสดีครับ ผมคือผู้ช่วย AI RFA');
expect(result.current.messages[1].suggestedActions).toEqual([{ label: 'ปุ่มแนะนำ', query: 'คำสั่งแนะนำ' }]);
});
it('ควรทำงานถูกต้องเมื่อเกิดข้อผิดพลาดในการเรียก API', async () => {
vi.mocked(axios.post).mockRejectedValue(new Error('Network error'));
const { wrapper } = createTestQueryClient();
const { result } = renderHook(() => useAiChat(mockContext), { wrapper });
await act(async () => {
void result.current.sendMessage('สวัสดี');
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.messages[1].content).toContain('ไม่สามารถเชื่อมต่อ AI ได้');
});
it('ควรสามารถล้างประวัติการสนทนาได้', async () => {
const { wrapper } = createTestQueryClient();
const { result } = renderHook(() => useAiChat(mockContext), { wrapper });
act(() => {
result.current.sendMessage('สวัสดี');
});
act(() => {
result.current.clearHistory();
});
expect(result.current.messages).toEqual([]);
});
});
@@ -0,0 +1,239 @@
// File: hooks/ai/__tests__/use-intent-classification.test.ts
// Change Log
// - 2026-05-19: สร้าง Unit tests สำหรับ Intent Classification hooks (T038).
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { createTestQueryClient } from '@/lib/test-utils';
import {
useIntentDefinitions,
useIntentDefinition,
useIntentPatterns,
useCreateIntentDefinition,
useClassifyIntent,
} from '../use-intent-classification';
import { aiIntentService } from '@/lib/services/ai-intent.service';
// Mock service
vi.mock('@/lib/services/ai-intent.service', () => ({
aiIntentService: {
getDefinitions: vi.fn(),
getDefinition: vi.fn(),
getPatterns: vi.fn(),
createDefinition: vi.fn(),
updateDefinition: vi.fn(),
createPattern: vi.fn(),
updatePattern: vi.fn(),
deletePattern: vi.fn(),
classify: vi.fn(),
},
}));
describe('use-intent-classification hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useIntentDefinitions', () => {
it('ควรดึง definitions สำเร็จ', async () => {
const mockData = [
{ publicId: 'uuid-1', intentCode: 'GET_RFA', category: 'read' },
{ publicId: 'uuid-2', intentCode: 'SUMMARIZE_DOCUMENT', category: 'read' },
];
vi.mocked(aiIntentService.getDefinitions).mockResolvedValue(mockData);
const { wrapper } = createTestQueryClient();
const { result } = renderHook(() => useIntentDefinitions(), { wrapper });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockData);
expect(aiIntentService.getDefinitions).toHaveBeenCalledWith(undefined);
});
it('ควรส่ง filter params ไปด้วย', async () => {
vi.mocked(aiIntentService.getDefinitions).mockResolvedValue([]);
const { wrapper } = createTestQueryClient();
renderHook(
() => useIntentDefinitions({ category: 'read', isActive: true }),
{ wrapper },
);
await waitFor(() => {
expect(aiIntentService.getDefinitions).toHaveBeenCalledWith({
category: 'read',
isActive: true,
});
});
});
});
describe('useIntentDefinition', () => {
it('ควรดึง definition ตาม intentCode', async () => {
const mockDef = {
publicId: 'uuid-1',
intentCode: 'GET_RFA',
descriptionTh: 'ดึง RFA',
descriptionEn: 'Get RFA',
category: 'read',
isActive: true,
};
vi.mocked(aiIntentService.getDefinition).mockResolvedValue(mockDef);
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useIntentDefinition('GET_RFA'),
{ wrapper },
);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockDef);
expect(aiIntentService.getDefinition).toHaveBeenCalledWith('GET_RFA');
});
it('ควรไม่ fetch เมื่อ intentCode เป็นค่าว่าง', () => {
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useIntentDefinition(''),
{ wrapper },
);
// enabled: !!intentCode → false → ไม่ fetch
expect(result.current.fetchStatus).toBe('idle');
expect(aiIntentService.getDefinition).not.toHaveBeenCalled();
});
});
describe('useIntentPatterns', () => {
it('ควรดึง patterns ตาม intentCode', async () => {
const mockPatterns = [
{ publicId: 'p-1', intentCode: 'GET_RFA', patternType: 'keyword', patternValue: 'rfa' },
];
vi.mocked(aiIntentService.getPatterns).mockResolvedValue(mockPatterns);
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useIntentPatterns('GET_RFA'),
{ wrapper },
);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockPatterns);
});
});
describe('useCreateIntentDefinition', () => {
it('ควรเรียก createDefinition สำเร็จ', async () => {
const newDef = {
intentCode: 'TEST_INTENT',
descriptionTh: 'ทดสอบ',
descriptionEn: 'Test',
category: 'utility' as const,
};
vi.mocked(aiIntentService.createDefinition).mockResolvedValue({
publicId: 'new-uuid',
...newDef,
isActive: true,
});
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useCreateIntentDefinition(),
{ wrapper },
);
result.current.mutate(newDef);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(aiIntentService.createDefinition).toHaveBeenCalledWith(newDef);
});
});
describe('useClassifyIntent', () => {
it('ควร classify query สำเร็จ', async () => {
const mockResult = {
intentCode: 'SUMMARIZE_DOCUMENT',
confidence: 1.0,
method: 'pattern',
latencyMs: 3,
};
vi.mocked(aiIntentService.classify).mockResolvedValue(mockResult);
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useClassifyIntent(),
{ wrapper },
);
result.current.mutate({ query: 'สรุปเอกสาร' });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockResult);
expect(aiIntentService.classify).toHaveBeenCalledWith('สรุปเอกสาร', undefined);
});
it('ควรส่ง projectPublicId ไปด้วย (ถ้ามี)', async () => {
vi.mocked(aiIntentService.classify).mockResolvedValue({
intentCode: 'GET_RFA',
confidence: 0.9,
method: 'llm_fallback',
latencyMs: 500,
});
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useClassifyIntent(),
{ wrapper },
);
result.current.mutate({
query: 'show rfa',
projectPublicId: 'proj-uuid-123',
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(aiIntentService.classify).toHaveBeenCalledWith('show rfa', 'proj-uuid-123');
});
it('ควร handle error state', async () => {
vi.mocked(aiIntentService.classify).mockRejectedValue(new Error('Network error'));
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useClassifyIntent(),
{ wrapper },
);
result.current.mutate({ query: 'test' });
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBeInstanceOf(Error);
});
});
});
@@ -0,0 +1,133 @@
// File: hooks/ai/use-intent-classification.ts
// Change Log
// - 2026-05-19: สร้าง TanStack Query hooks สำหรับ Intent Classification (ADR-024).
// Hooks สำหรับ Intent Classification — ใช้ TanStack Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
aiIntentService,
IntentCategory,
CreateIntentDefinitionDto,
UpdateIntentDefinitionDto,
CreateIntentPatternDto,
UpdateIntentPatternDto,
ClassificationAnalytics,
} from '@/lib/services/ai-intent.service';
// === Query Keys ===
const KEYS = {
definitions: ['ai', 'intent-definitions'] as const,
definition: (code: string) => ['ai', 'intent-definitions', code] as const,
patterns: (code: string) => ['ai', 'intent-patterns', code] as const,
analytics: ['ai', 'intent-analytics'] as const,
};
// === Query Hooks ===
/** ดึง Intent Definitions ทั้งหมด */
export function useIntentDefinitions(params?: {
category?: IntentCategory;
isActive?: boolean;
}) {
return useQuery({
queryKey: [...KEYS.definitions, params],
queryFn: () => aiIntentService.getDefinitions(params),
});
}
/** ดึง Intent Definition ตาม intentCode */
export function useIntentDefinition(intentCode: string) {
return useQuery({
queryKey: KEYS.definition(intentCode),
queryFn: () => aiIntentService.getDefinition(intentCode),
enabled: !!intentCode,
});
}
/** ดึง Patterns ตาม intentCode */
export function useIntentPatterns(intentCode: string) {
return useQuery({
queryKey: KEYS.patterns(intentCode),
queryFn: () => aiIntentService.getPatterns(intentCode),
enabled: !!intentCode,
});
}
// === Mutation Hooks ===
/** สร้าง Intent Definition ใหม่ */
export function useCreateIntentDefinition() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: CreateIntentDefinitionDto) =>
aiIntentService.createDefinition(dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.definitions });
},
});
}
/** อัปเดต Intent Definition */
export function useUpdateIntentDefinition(intentCode: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: UpdateIntentDefinitionDto) =>
aiIntentService.updateDefinition(intentCode, dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.definitions });
queryClient.invalidateQueries({ queryKey: KEYS.definition(intentCode) });
},
});
}
/** สร้าง Pattern ใหม่ */
export function useCreateIntentPattern(intentCode: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: CreateIntentPatternDto) =>
aiIntentService.createPattern(intentCode, dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.patterns(intentCode) });
},
});
}
/** อัปเดต Pattern */
export function useUpdateIntentPattern(intentCode: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { publicId: string; dto: UpdateIntentPatternDto }) =>
aiIntentService.updatePattern(data.publicId, data.dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.patterns(intentCode) });
},
});
}
/** ลบ Pattern (soft delete) */
export function useDeleteIntentPattern(intentCode: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (publicId: string) => aiIntentService.deletePattern(publicId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.patterns(intentCode) });
},
});
}
/** ดึง Classification Analytics */
export function useIntentAnalytics(params?: { from?: string; to?: string }) {
return useQuery<ClassificationAnalytics>({
queryKey: [...KEYS.analytics, params],
queryFn: () => aiIntentService.getAnalytics(params),
staleTime: 60_000, // 1 นาที cache
});
}
/** Classify query (สำหรับ Test Console) */
export function useClassifyIntent() {
return useMutation({
mutationFn: (data: { query: string; projectPublicId?: string }) =>
aiIntentService.classify(data.query, data.projectPublicId),
});
}
+96
View File
@@ -0,0 +1,96 @@
// File: frontend/hooks/use-ai-chat.ts
// Change Log:
// - 2026-05-19: พัฒนา Hook useAiChat สำหรับระบบแชท AI ในหน้าเอกสาร
import { useState, useEffect, useCallback } from 'react';
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
import { ChatMessage, ChatContext, ChatResponseDto } from '@/types/ai-chat';
export function useAiChat(context: ChatContext) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isOpen, setIsOpen] = useState<boolean>(false);
const storageKey = `ai_chat_session_${context.type}_${context.publicId}`;
useEffect(() => {
if (typeof window !== 'undefined') {
const stored = sessionStorage.getItem(storageKey);
if (stored) {
try {
setMessages(JSON.parse(stored));
} catch (_) {
setMessages([]);
}
} else {
setMessages([]);
}
}
}, [storageKey]);
const saveMessages = useCallback((newMsgs: ChatMessage[]) => {
setMessages(newMsgs);
if (typeof window !== 'undefined') {
sessionStorage.setItem(storageKey, JSON.stringify(newMsgs));
}
}, [storageKey]);
const chatMutation = useMutation({
mutationFn: async (queryText: string): Promise<ChatResponseDto> => {
const response = await axios.post('/api/ai/chat', {
query: queryText,
context,
});
return response.data;
},
});
const sendMessage = useCallback(async (queryText: string) => {
if (!queryText.trim()) return;
const userMsg: ChatMessage = {
id: crypto.randomUUID(),
role: 'user',
content: queryText,
timestamp: new Date().toISOString(),
};
const currentMsgs = [...messages, userMsg];
saveMessages(currentMsgs);
const systemLoadingMsg: ChatMessage = {
id: crypto.randomUUID(),
role: 'assistant',
content: '',
timestamp: new Date().toISOString(),
isStreaming: true,
};
setMessages([...currentMsgs, systemLoadingMsg]);
try {
const result = await chatMutation.mutateAsync(queryText);
const assistantMsg: ChatMessage = {
id: result.messageId || crypto.randomUUID(),
role: 'assistant',
content: result.content,
timestamp: new Date().toISOString(),
suggestedActions: result.suggestedActions,
};
saveMessages([...currentMsgs, assistantMsg]);
} catch (_err) {
const errorMsg: ChatMessage = {
id: crypto.randomUUID(),
role: 'assistant',
content: 'ไม่สามารถเชื่อมต่อ AI ได้ กรุณาลองใหม่',
timestamp: new Date().toISOString(),
};
saveMessages([...currentMsgs, errorMsg]);
}
}, [messages, saveMessages, chatMutation]);
const clearHistory = useCallback(() => {
saveMessages([]);
}, [saveMessages]);
const toggleOpen = useCallback(() => {
setIsOpen((prev) => !prev);
}, []);
return {
messages,
sendMessage,
clearHistory,
isLoading: chatMutation.isPending,
isOpen,
setIsOpen,
toggleOpen,
};
}