Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions frontend/src/api/chatbot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,35 @@ import { API_BASE_URL } from '../config';
import { getChatbotText } from '../data/chatbotTexts';
import { v4 as uuidv4 } from 'uuid';

/**
* Send a request to the backend to create a new chat session and returns the id of the
* chat session created.
*
* @returns A Promise resolving to the id of the new chat session
*/
export const createChatSession = async (): Promise<string> => {
try{
const response = await fetch(`${API_BASE_URL}/api/chatbot/sessions`, { method: 'POST' });
const data = await response.json();

return data.session_id;
} catch (error) {
console.error('Chatbot API error while creating a new chat session:', error);
return "";
}
};

/**
* Sends the user's message to the backend chatbot API and returns the bot's response.
* If the API call fails or returns an invalid response, a fallback error message is used.
*
* @param sessionId - The session id of the chat
* @param userMessage - The message input from the user
* @returns A Promise resolving to a bot-generated Message
*/
export const fetchChatbotReply = async (userMessage: string): Promise<Message> => {
export const fetchChatbotReply = async (sessionId: string, userMessage: string): Promise<Message> => {
try {
const response = await fetch(`${API_BASE_URL}/api/chatbot/reply`, {
const response = await fetch(`${API_BASE_URL}/api/chatbot/sessions/${sessionId}/message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -28,11 +47,24 @@ export const fetchChatbotReply = async (userMessage: string): Promise<Message> =

Comment thread
giovanni-vaccarino marked this conversation as resolved.
Outdated
return createBotMessage(data.reply || getChatbotText('noResponseMessage'));
} catch (error) {
console.error('Chatbot API error:', error);
console.error('Chatbot API error while sending a message:', error);
return createBotMessage(getChatbotText('errorMessage'));
}
};

/**
* Sends a request to the backend to delete the chat session with session id sessionId.
*
* @param sessionId - The session id of the chat to delete
*/
export const deleteChatSession = async (sessionId: string): Promise<void> => {
try{
await fetch(`${API_BASE_URL}/api/chatbot/sessions/${sessionId}`, { method: 'DELETE' });
} catch(error) {
console.error('Chatbot API error while deleting a chat session:', error);
}
};

/**
Comment thread
giovanni-vaccarino marked this conversation as resolved.
* Utility function to create a Message object from the bot,
* using a UUID as the message ID.
Expand Down
223 changes: 198 additions & 25 deletions frontend/src/components/Chatbot.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useState, useEffect } from 'react';
import { type Message } from '../model/Message';
import { fetchChatbotReply } from '../api/chatbot';
import { type ChatSession } from '../model/ChatSession';
import { fetchChatbotReply, createChatSession, deleteChatSession } from '../api/chatbot';
import { Header } from './Header';
import { Messages } from './Messages';
import { Sidebar } from './Sidebar';
import { Input } from './Input';
import { chatbotStyles } from '../styles/styles';
import { getChatbotText } from '../data/chatbotTexts';
import { loadChatbotSessions, loadChatbotLastSessionId } from '../utils/chatbotStorage';
import { v4 as uuidv4 } from 'uuid';

/**
Expand All @@ -14,45 +17,197 @@ import { v4 as uuidv4 } from 'uuid';
export const Chatbot = () => {
const [isOpen, setIsOpen] = useState(false);
const [input, setInput] = useState('');
const [sessions, setSessions] = useState<ChatSession[]>(loadChatbotSessions);
const [currentSessionId, setCurrentSessionId] = useState<string | null>(loadChatbotLastSessionId);
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
const [isPopupOpen, setIsPopupOpen] = useState<boolean>(false);
const [sessionIdToDelete, setSessionIdToDelete] = useState<string | null>(null);

/**
* Messages shown in the chat.
* Initialized from sessionStorage to keep messages between refreshes.
* Saving the chat sessions in the session storage only
* when the component umounts to avoid continuos savings.
Comment thread
giovanni-vaccarino marked this conversation as resolved.
Outdated
*/
const [messages, setMessages] = useState<Message[]>(() => {
const saved = sessionStorage.getItem('chatbot-messages');
return saved ? JSON.parse(saved) : [];
});
const [loading, setLoading] = useState(false);

useEffect(() => {
sessionStorage.setItem('chatbot-messages', JSON.stringify(messages));
}, [messages]);
const handleBeforeUnload = () => {
sessionStorage.setItem('chatbot-sessions', JSON.stringify(sessions));
sessionStorage.setItem('chatbot-last-session-id', currentSessionId || '');
};

window.addEventListener('beforeunload', handleBeforeUnload);

return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [sessions, currentSessionId]);

/**
* Returns the messages of a chat session.
* @param sessionId The sessionId of the chat session.
* @returns The messages of the chat with id equals to sessionId
*/
const getSessionMessages = (sessionId: string | null) => {
if (currentSessionId === null){
console.error("No current session")
return []
}
const chatSession = sessions.find(item => item.id === sessionId);

if (chatSession){
return chatSession.messages;
}

console.error(`No session found with sessionId ${sessionId}`)
return []
}

/**
* Handles the delete process of a chat session.
*/
const handleDeleteChat = async () => {
if (sessionIdToDelete === null){
console.error("No current selected to delete")
return
}

await deleteChatSession(sessionIdToDelete);
const updatedSessions = sessions.filter(s => s.id !== sessionIdToDelete);
setSessions(updatedSessions);
setIsPopupOpen(false);
if (updatedSessions.length === 0) {
setCurrentSessionId(null);
} else {
setCurrentSessionId(updatedSessions[0].id);
}
};

/**
* Handles the creation process of a chat session.
*/
const handleNewChat = async () => {
const id = await createChatSession();

if (id === "") {
console.error("Add error showage for a couple of seconds.")
return
}
const newSession: ChatSession = { id, messages: [], createdAt: new Date().toISOString(), isLoading: false };
setSessions(prev => [newSession, ...prev]);
setCurrentSessionId(id);
};

const clearMessages = () => {
setMessages([]);
// Once backend is ready, add logic to clear history chat
const appendMessageToCurrentSession = (message: Message) => {
setSessions(prevSessions =>
prevSessions.map(session =>
session.id === currentSessionId
? { ...session, messages: [...session.messages, message] }
: session
)
);
};

/**
* Handles the send process in a chat session.
*/
const sendMessage = async () => {
const trimmed = input.trim();
if (!trimmed) return;

if (!trimmed || !currentSessionId) {
console.error("No sessions available.")
return;
}
if (!trimmed) {
console.error("Empty message provided.")
return;
}
const userMessage: Message = {
id: uuidv4(),
sender: 'user',
text: trimmed,
};

setMessages(prev => [...prev, userMessage]);
setInput('');
setLoading(true);
setSessions(prevSessions =>
prevSessions.map(session =>
session.id === currentSessionId
? { ...session, isLoading: true }
: session
)
);
appendMessageToCurrentSession(userMessage);

const botReply = await fetchChatbotReply(trimmed);
setLoading(false);
setMessages(prev => [...prev, botReply]);
const botReply = await fetchChatbotReply(currentSessionId!, trimmed);
setSessions(prevSessions =>
prevSessions.map(session =>
session.id === currentSessionId
? { ...session, isLoading: false }
: session
)
);
appendMessageToCurrentSession(botReply);
};

const getChatLoading = () : boolean => {
const currentChat = sessions.find(chat => chat.id === currentSessionId);

return currentChat ? currentChat.isLoading : false;
}

const openSideBar = () => {
setIsSidebarOpen(!isSidebarOpen)
}

const onSwitchChat = (chatSessionId: string) => {
openSideBar();
setCurrentSessionId(chatSessionId);
}

const openConfirmDeleteChatPopup = (chatSessionId: string) => {
setSessionIdToDelete(chatSessionId);
setIsPopupOpen(true);
}

const getWelcomePage = () => {
return(
<div
style={chatbotStyles.containerWelcomePage}
>
<div style={chatbotStyles.boxWelcomePage}>
<h2 style={chatbotStyles.welcomePageH2}>{getChatbotText("welcomeMessage")}</h2>
<p>{getChatbotText("welcomeDescription")}</p>
<button style={chatbotStyles.welcomePageNewChatButton}
onClick={handleNewChat}
>
{getChatbotText("createNewChat")}
</button>
</div>
</div>
)
}

const getDeletePopup = () => {
return(

<div
style={chatbotStyles.popupContainer}
>
<h2 style={chatbotStyles.popupTitle}>{getChatbotText("popupTitle")}</h2>
<p style={chatbotStyles.popupMessage}>
{getChatbotText("popupMessage")}
</p>
<div style={chatbotStyles.popupButtonsContainer}>
<button style={chatbotStyles.popupDeleteButton} onClick={handleDeleteChat}>
{getChatbotText("popupDeleteButton")}
</button>
<button style={chatbotStyles.popupCancelButton} onClick={() => {
setIsPopupOpen(false);
setSessionIdToDelete(null);
}}>
{getChatbotText("popupCancelButton")}
</button>
</div>
</div>
)
}

return (
<>
<button
Expand All @@ -64,11 +219,29 @@ export const Chatbot = () => {

{isOpen && (
<div
style={chatbotStyles.container}
style={{...chatbotStyles.container, pointerEvents: isPopupOpen ? 'none' : 'auto'}}
>
<Header clearMessages={clearMessages} />
<Messages messages={messages} loading={loading} />
<Input input={input} setInput={setInput} onSend={sendMessage} />
{isSidebarOpen && (
<Sidebar
onClose={() => setIsSidebarOpen(false)}
onCreateChat={handleNewChat}
onSwitchChat={onSwitchChat}
chatList={sessions}
activeChatId={currentSessionId}
openConfirmDeleteChatPopup={openConfirmDeleteChatPopup}
/>
)}
{isPopupOpen && (
getDeletePopup()
)}
<Header currentSessionId={currentSessionId} openSideBar={openSideBar} clearMessages={openConfirmDeleteChatPopup} />
{(currentSessionId !== null) ?
<>
<Messages messages={getSessionMessages(currentSessionId)} loading={getChatLoading()} />
<Input input={input} setInput={setInput} onSend={sendMessage} />
</>
: getWelcomePage()
}
</div>
)}
</>
Expand Down
23 changes: 17 additions & 6 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,35 @@ import { chatbotStyles } from '../styles/styles';
* Props for the Header component.
*/
interface HeaderProps {
clearMessages: () => void;
currentSessionId: string | null;
clearMessages: (chatSessionId: string) => void;
openSideBar: () => void;
}

/**
* Header renders the top section of the chatbot panel, including the title and
* a button to clear the current conversation. It receives a callback to handle
* message clearing, typically triggered by user interaction.
*/
export const Header = ({ clearMessages }: HeaderProps) => (
export const Header = ({ currentSessionId, clearMessages, openSideBar }: HeaderProps) => {
return(
<div
style={chatbotStyles.chatbotHeader}
>
<p>{getChatbotText('title')}</p>
<button
onClick={clearMessages}
onClick={openSideBar}
style={chatbotStyles.openSidebarButton}
aria-label="Toggle sidebar"
>
{getChatbotText("sidebarLabel")}
</button>
{(currentSessionId !== null) &&
<button
onClick={() => clearMessages(currentSessionId)}
style={chatbotStyles.clearButton}
>
{getChatbotText("clearChat")}
</button>
</button>
}
</div>
);
)};
Loading