diff --git a/src/components/Chatbot/Chatbot.tsx b/src/components/Chatbot/Chatbot.tsx index 3fdc8ea9..92b3f81c 100644 --- a/src/components/Chatbot/Chatbot.tsx +++ b/src/components/Chatbot/Chatbot.tsx @@ -10,6 +10,7 @@ import Image from 'next/image'; import { useEffect, useRef, useState } from 'react'; import TypingText from '@/components/TypingText/TypingText'; +import container from '@/container'; import { CHAT_TITLE, DATA_SOURCES, @@ -21,15 +22,17 @@ import { TYPING_PLACEHOLDER, WELCOME_MESSAGE, } from '@/domain/constant/chatbot/Chatbot'; +import { APIError } from '@/domain/entities/chatbot/BackendCommunication'; import { IChat } from '@/domain/entities/chatbot/Chatbot'; import { SenderRole } from '@/domain/enums/SenderRole'; -import { APIError, chatService, formatChatResponse } from '@/services/api/chatbot'; +import ChatbotRepository from '@/domain/repositories/ChatbotRepository'; import { useMediaQuery } from '@/utils/resolution'; import TypingDots from '../TypingText/TypingDot'; import ChatbotSidebar from './ChatbotSidebar'; export default function HungerMapChatbot() { + const chatbot = container.resolve('ChatbotRepository'); const [isOpen, setIsOpen] = useState(false); const [isFullScreen, setIsFullScreen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false); @@ -90,8 +93,8 @@ export default function HungerMapChatbot() { const previousMessages = chats[currentChatIndex].messages; let aiResponse = ''; try { - const response = await chatService.sendMessage(text, { previous_messages: previousMessages }); - aiResponse = formatChatResponse(response).text; + const response = await chatbot.sendMessage(text, { previous_messages: previousMessages }); + aiResponse = response.response; } catch (err) { if (err instanceof APIError) { aiResponse = `Ups! Unfortunately, it seems like there was a problem connecting to the server...\n ${err.status}: ${err.message}`; diff --git a/src/container.tsx b/src/container.tsx index 5ae5281c..370d8ebf 100644 --- a/src/container.tsx +++ b/src/container.tsx @@ -1,4 +1,5 @@ import { AlertRepositoryImpl } from './infrastructure/repositories/AlertRepositoryImpl'; +import ChatbotRepositoryImpl from './infrastructure/repositories/ChatbotRepositoryImpl'; import CountryRepositoryImpl from './infrastructure/repositories/CountryRepositoryImpl'; import GlobalDataRepositoryImpl from './infrastructure/repositories/GlobalDataRepositoryImpl'; @@ -22,5 +23,6 @@ const container = new Container(); container.register('CountryRepository', new CountryRepositoryImpl()); container.register('AlertRepository', new AlertRepositoryImpl()); container.register('GlobalDataRepository', new GlobalDataRepositoryImpl()); +container.register('ChatbotRepository', new ChatbotRepositoryImpl()); export default container; diff --git a/src/domain/entities/chatbot/BackendCommunication.ts b/src/domain/entities/chatbot/BackendCommunication.ts new file mode 100644 index 00000000..ba598722 --- /dev/null +++ b/src/domain/entities/chatbot/BackendCommunication.ts @@ -0,0 +1,26 @@ +import { IMessage } from './Chatbot'; + +export interface QueryRequest { + query: string; + version?: number; + chatbot_type?: string; + limit?: number; + previous_messages?: IMessage[]; +} + +export interface QueryResponse { + response: string; + prompt_tokens: number; + total_tokens: number; +} + +// TO-DO: not sure if it belongs here. consider moving to another place +export class APIError extends Error { + constructor( + public status: number, + message: string + ) { + super(message); + this.name = 'APIError'; + } +} diff --git a/src/domain/repositories/ChatbotRepository.ts b/src/domain/repositories/ChatbotRepository.ts new file mode 100644 index 00000000..8fe0bf44 --- /dev/null +++ b/src/domain/repositories/ChatbotRepository.ts @@ -0,0 +1,17 @@ +import { QueryRequest, QueryResponse } from '@/domain/entities/chatbot/BackendCommunication.ts'; + +export default interface ChatbotRepository { + /** + * Tests the health of the chatbot service by sending a test message. + * @returns A promise that resolves to a boolean indicating the health status. + */ + testHealth(): Promise; + + /** + * Sends a message to the chatbot service with optional parameters. + * @param message The message to be sent to the chatbot. + * @param options Optional parameters for the message, excluding the query. + * @returns A promise that resolves to the response from the chatbot. + */ + sendMessage(message: string, options: Partial>): Promise; +} diff --git a/src/infrastructure/repositories/ChatbotRepositoryImpl.ts b/src/infrastructure/repositories/ChatbotRepositoryImpl.ts new file mode 100644 index 00000000..16a43b55 --- /dev/null +++ b/src/infrastructure/repositories/ChatbotRepositoryImpl.ts @@ -0,0 +1,45 @@ +import { APIError, QueryRequest, QueryResponse } from '@/domain/entities/chatbot/BackendCommunication.ts'; +import ChatbotRepository from '@/domain/repositories/ChatbotRepository'; + +export default class ChatbotRepositoryImpl implements ChatbotRepository { + async testHealth(): Promise { + try { + const response = await fetch(`${process.env.BACKEND_CHATBOT_API_URL}/health`); + return response.ok; + } catch { + return false; + } + } + + async sendMessage(message: string, options: Partial>): Promise { + try { + const payload: QueryRequest = { + query: message, + version: options.version || 1, + chatbot_type: options.chatbot_type || 'gpt-4', + limit: options.limit || 5, + previous_messages: options.previous_messages || [], + }; + + const response = await fetch(`${process.env.BACKEND_CHATBOT_API_URL}/query`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new APIError(response.status, errorData.error || `Server responded with status: ${response.status}`); + } + const data = await response.json(); + return data; + } catch (error) { + if (error instanceof APIError) { + throw error; + } + throw new APIError(500, 'Failed to connect to server'); + } + } +} diff --git a/src/services/api/chatbot.ts b/src/services/api/chatbot.ts deleted file mode 100644 index 1461f4e2..00000000 --- a/src/services/api/chatbot.ts +++ /dev/null @@ -1,87 +0,0 @@ -// @author: Ahmet Selman Güclü -// IMPORTANT: chatbot requests work only when back-end is running locally since the back-end is not deployed yet - -import { IMessage } from '@/domain/entities/chatbot/Chatbot'; - -// Types according to the back-end are like this: -export interface QueryRequest { - query: string; - version?: number; - chatbot_type?: string; - limit?: number; - previous_messages?: IMessage[]; -} - -export interface QueryResponse { - response: string; - prompt_tokens: number; - total_tokens: number; -} - -// Error type for handling API errors -export class APIError extends Error { - constructor( - public status: number, - message: string - ) { - super(message); - this.name = 'APIError'; - } -} - -// Main chat service -export const chatService = { - // Test the connection to the backend - testHealth: async (): Promise => { - try { - const response = await fetch(`${process.env.NEXT_PUBLIC_API_KEY}/health`); - return response.ok; - } catch { - return false; - } - }, - - // Send message to the backend - sendMessage: async (message: string, options: Partial> = {}): Promise => { - try { - // Construct the request payload - const payload: QueryRequest = { - query: message, - version: options.version || 1, - chatbot_type: options.chatbot_type || 'gpt-4', - limit: options.limit || 5, - previous_messages: options.previous_messages || [], - }; - - const response = await fetch(`${API_BASE_URL}/query`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new APIError(response.status, errorData.error || `Server responded with status: ${response.status}`); - } - const data = await response.json(); - return data; - } catch (error) { - if (error instanceof APIError) { - throw error; - } - throw new APIError(500, 'Failed to connect to server'); - } - }, -}; - -// Helper function to format the response for the chat interface -export const formatChatResponse = (response: QueryResponse) => ({ - text: response.response, - sender: 'ai' as const, - metadata: { - promptTokens: response.prompt_tokens, - totalTokens: response.total_tokens, - }, -});