690519:1631 224 to 226 AI #01
This commit is contained in:
@@ -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),
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user