diff --git a/src/chat/adapters/assistant_integration_adapter.py b/src/chat/adapters/assistant_integration_adapter.py new file mode 100644 index 0000000..a7f31a6 --- /dev/null +++ b/src/chat/adapters/assistant_integration_adapter.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import Optional + +from assistants.assistants import Assistants +from chat.entities.message import Message +from chat.interfaces import AssistantAdapterProtocol + + +class AssistantIntegrationAdapter(AssistantAdapterProtocol): + def __init__(self, room_id: str = "programador", assistant_name: str = "Programador"): + self.room_id = room_id + self.assistant = Assistants(nome=assistant_name) + + def should_handle(self, message: Message) -> bool: + return message.room_id == self.room_id and message.message_type == "chat_message" + + def process_message(self, message: Message) -> Optional[Message]: + return self.assistant.process_message(message) diff --git a/src/chat/chat_interface.py b/src/chat/chat_interface.py index 2b19392..e92dff4 100644 --- a/src/chat/chat_interface.py +++ b/src/chat/chat_interface.py @@ -1,15 +1,18 @@ -import os import flet as ft + +from chat.adapters.assistant_integration_adapter import AssistantIntegrationAdapter from chat.chat_app import ChatApp from chat.chat_message import ChatMessage +from chat.components.chat_drawer import ChatDrawer +from chat.components.chat_timeline import ChatTimeline +from chat.components.message_timeline_renderer import MessageTimelineRenderer from chat.entities.message import Message -from assistants.assistants import Assistants -from chat.use_cases.dialogs import WelcomeDialog, NewRoomDialog +from chat.services.message_event_router import MessageEventRouter +from chat.use_cases.dialogs import NewRoomDialog, WelcomeDialog from chat.utils.file_handler import FileHandler -class ChatInterface: - programador_assistant = Assistants(nome="Programador") +class ChatInterface: def __init__(self, page: ft.Page, chat_app: ChatApp): self.page = page self.chat_app = chat_app @@ -17,164 +20,53 @@ def __init__(self, page: ft.Page, chat_app: ChatApp): self.user_name: str self.current_room = self.page.session.get("current_room") or self.chat_app.current_room - # Instancia os componentes de diálogo e file handler self.welcome_dialog = WelcomeDialog(self.join_chat_click) self.new_room_dialog = NewRoomDialog(self.save_new_room) self.file_handler = FileHandler(self.page, self.chat_app, self.on_message) - # Adiciona os diálogos à sobreposição da página self.page.overlay.append(self.welcome_dialog.dialog) self.page.overlay.append(self.new_room_dialog.dialog) - # Inscreve-se para receber mensagens via pubsub self.page.pubsub.subscribe(self.on_message) - # Cria os componentes da interface divididos em blocos - self.__create_menu_drawer() # Menu lateral com salas e usuários online - self.__create_chatbox() # Área principal com chat e inputs - - # Foco para o nome de usuário e atualiza a página - self.welcome_dialog.join_user_name.focus() - self.page.update() - - - # ======================== - # Criação do Menu Drawer - # ======================== - def __create_menu_drawer(self): - # Cria a parte de salas de chat - self.__create_rooms_drawer() - # Cria a parte de usuários online, utilizando active_users - self.__create_users_drawer() - - # Junta as duas divisões em um NavigationDrawer - self.menu_drawer = ft.NavigationDrawer( - controls=[ - # Bloco de salas - ft.Column( - controls=[ - ft.Container(height=12), - ft.Text("Salas de Chat", size=18, weight="bold", text_align="center"), - ft.Divider(), - *self.rooms_drawer_controls # Lista de ListTile das salas - ], - expand=True, - ), - # Bloco de usuários - ft.Column( - controls=[ - ft.Divider(), - ft.Text("Usuários Online", size=16, weight="bold", text_align="center"), - *self.users_drawer_controls # Lista de ListTile dos usuários - ], - alignment=ft.MainAxisAlignment.END - ), - ft.Row([self.new_room_btn], alignment=ft.MainAxisAlignment.CENTER), - ] + self.drawer = ChatDrawer( + page=self.page, + on_change_room=self.change_room_by_id, + on_send_private_message=self.send_private_message, + on_new_room_click=self.create_new_room_click, ) - # Atualiza a página para que o drawer seja o novo menu_drawer - self.page.drawer = self.menu_drawer - - self.menu_button = ft.IconButton( - icon=ft.Icons.MENU, - tooltip="Abrir menu", - on_click=lambda _: self.open_drawer(), - ) - - def __create_rooms_drawer(self): - # Botão para criar nova sala - self.new_room_btn = ft.ElevatedButton( - text="Nova sala", - on_click=self.create_new_room_click, - icon=ft.Icons.MEETING_ROOM, - width=130, + self.drawer.build(self.chat_app.rooms, self.chat_app.active_users) + + initial_room_name = self.chat_app.rooms[self.current_room].room.room_name + self.timeline = ChatTimeline(room_name=initial_room_name) + + self.message_router = MessageEventRouter( + renderer=MessageTimelineRenderer( + page=self.page, + on_edit=self.on_edit, + on_delete=self.on_delete, + ), + assistant_adapter=AssistantIntegrationAdapter(), ) - # Cria controles para as salas de chat - self.rooms_drawer_controls = [ - ft.ListTile( - leading=ft.Icon(ft.Icons.CHAT_BUBBLE_OUTLINE), - title=ft.Text(value.room.room_name), - on_click=lambda e, room_id=key: self.change_room_by_id(room_id), - ) for key, value in self.chat_app.rooms.items() - ] - - def __create_users_drawer(self): - # Obtém os nomes dos usuários ativos a partir do dicionário active_users - current_users = [user.user_name for user in self.chat_app.active_users.values()] - self.users_drawer_controls = [ - ft.ListTile( - leading=ft.Icon(ft.Icons.ACCOUNT_CIRCLE), - title=ft.Text(user), - on_click=lambda e, user=user: self.send_private_message(user), - ) for user in current_users - ] - def update_users_drawer(self): - # Atualiza dinamicamente a lista de usuários online utilizando active_users - current_users = [user.user_name for user in self.chat_app.active_users.values()] - self.users_drawer_controls = [ - ft.ListTile( - leading=ft.Icon(ft.Icons.ACCOUNT_CIRCLE), - title=ft.Text(user), - on_click=lambda e, user=user: self.send_private_message(user), - ) for user in current_users - ] - # Atualiza o bloco de usuários dentro do menu_drawer - self.menu_drawer.controls[-2] = ft.Column( - controls=[ - ft.Divider(), - ft.Text("Usuários Online", size=16, weight="bold", text_align="center"), - *self.users_drawer_controls - ], - alignment=ft.MainAxisAlignment.END - ) - self.page.update() + self.__create_user_formulary() + self.__create_chatbox() - def open_drawer(self): - self.page.drawer.open = True + self.welcome_dialog.join_user_name.focus() self.page.update() - # ======================== - # Criação do ChatBox - # ======================== def __create_chatbox(self): - # Cria o ChatRoom (área de exibição das mensagens) - self.__create_chat_room() - # Cria o UserFormulary (inputs para envio de mensagem e arquivos) - self.__create_user_formulary() - - # Junta os dois blocos em um container principal self.chatbox = ft.Column( controls=[ - # Cabeçalho com menu e identificação da sala - ft.Row([self.menu_button, self.room_name], alignment=ft.MainAxisAlignment.START), - # ChatRoom - self.chat_room_container, - # UserFormulary + ft.Row([self.drawer.menu_button, self.timeline.room_name], alignment=ft.MainAxisAlignment.START), + self.timeline.chat_room_container, self.user_formulary_container, ], expand=True, ) self.page.add(self.chatbox) - def __create_chat_room(self): - # Área de exibição das mensagens do chat - self.chat = ft.ListView(expand=True, spacing=10, auto_scroll=True) - self.chat_room_container = ft.Container( - content=self.chat, - border=ft.border.all(1, ft.Colors.OUTLINE), - border_radius=5, - padding=10, - expand=True, - ) - # Exibe o nome da sala atual - self.room_name = ft.Text( - f"Sala: {self.chat_app.rooms[self.current_room].room.room_name}", - size=20, weight="bold" - ) - def __create_user_formulary(self): - # Campo para nova mensagem self.new_message = ft.TextField( hint_text="Escreva uma mensagem...", autofocus=True, @@ -186,7 +78,6 @@ def __create_user_formulary(self): on_submit=self.send_message_click, border_radius=5, ) - # Barra de envio com botões para upload e envio self.input_bar = ft.Row( controls=[ self.new_message, @@ -195,7 +86,7 @@ def __create_user_formulary(self): tooltip="Compartilhar arquivo", on_click=lambda _: self.file_handler.file_picker.pick_files( allow_multiple=True, - allowed_extensions=['png', 'jpg', 'jpeg', 'gif', 'pdf', 'doc', 'docx', 'txt'] + allowed_extensions=["png", "jpg", "jpeg", "gif", "pdf", "doc", "docx", "txt"], ), ), ft.IconButton( @@ -215,37 +106,36 @@ def __create_user_formulary(self): alignment=ft.alignment.bottom_center, ) - # ======================== - # Outros Métodos de Ação - # ======================== + def update_users_drawer(self): + self.drawer.update_users(self.chat_app.active_users) + self.page.update() + def send_private_message(self, user: str): print("Enviando mensagem privada para: ", user) reciver = user.strip().lower() owner = self.user_id - private_room_id = str(owner+reciver).lower() - - # Debito técnico para possibilitar criações de sala com mais user + private_room_id = str(owner + reciver).lower() + if private_room_id in self.chat_app.rooms.keys(): self.change_room_by_id(private_room_id) return - - private_room_id2 = str(reciver+owner).lower() + + private_room_id2 = str(reciver + owner).lower() if private_room_id2 in self.chat_app.rooms.keys(): self.change_room_by_id(private_room_id2) return - + private_room_id = self.chat_app.new_private_room(owner=self.user_name, reciver=user, room_id=private_room_id) self.change_room_by_id(private_room_id) - + def change_room_by_id(self, room_id): print(f"Changing room to: {room_id}") self.page.session.set("current_room", room_id) - # self.current_room = room_id self.current_room = room_id - self.room_name.value = f"Sala: {self.chat_app.rooms[room_id].room.room_name}" - self.chat.controls.clear() + self.timeline.set_room_name(self.chat_app.rooms[room_id].room.room_name) + self.timeline.clear() for msg in self.chat_app.rooms[room_id].room.messages: - self.on_message(msg) + self.on_message(msg) self.page.drawer.open = False self.page.update() @@ -256,14 +146,14 @@ def save_new_room(self, e): self.new_room_dialog.room_name_field.error_text = "O nome não pode estar em branco!" self.page.update() return - elif not room_id: + if not room_id: self.new_room_dialog.room_id_field.error_text = "O ID não pode estar em branco!" self.page.update() return - + self.chat_app.new_room(room_id, room_name) self.new_room_dialog.dialog.open = False - self.__create_menu_drawer() + self.drawer.build(self.chat_app.rooms, self.chat_app.active_users) self.page.update() def create_new_room_click(self, e): @@ -298,7 +188,7 @@ def join_chat_click(self, e): else: self.page.session.set("user_name", user_name) self.page.session.set("user_id", user_id) - self.page.session.set("current_room", 'geral') + self.page.session.set("current_room", "geral") self.user_name = self.page.session.get("user_name") self.user_id = self.page.session.get("user_id") self.current_room = self.page.session.get("current_room") @@ -306,26 +196,25 @@ def join_chat_click(self, e): self.chat_app.add_user(user_name=self.user_name, user_id=self.user_id) self.welcome_dialog.dialog.open = False - # Carrega as mensagens existentes da sala atual for msg in self.chat_app.rooms[self.current_room].room.messages: - self.on_message(msg) + self.on_message(msg) self.page.update() - - # Atualiza o drawer para que os usuários conectados vejam a lista atualizada - msg = Message(user_name=user_name, - text=f"{user_name} entrou no chat.", - message_type="login_message", - room_id=self.current_room) - + + msg = Message( + user_name=user_name, + text=f"{user_name} entrou no chat.", + message_type="login_message", + room_id=self.current_room, + ) + self.chat_app.add_message_to_room(msg) self.page.pubsub.send_all(msg) def on_edit(self, chat_message: ChatMessage): def save_edit(e): chat_message.message.text = edit_field.value - # Atualiza a interface da mensagem editada - chat_message.controls[1].controls[1].value = chat_message.message.text + chat_message.controls[1].controls[1].value = chat_message.message.text chat_message.controls[1].controls[1].update() chat_message.update() edit_dlg.open = False @@ -345,53 +234,23 @@ def save_edit(e): self.page.update() def on_delete(self, chat_message: ChatMessage): - self.chat.controls.remove(chat_message) + self.timeline.remove(chat_message) self.page.update() def on_message(self, message: Message): - # Processa apenas mensagens da sala atual - if message.room_id != self.page.session.get("current_room"): + result = self.message_router.route( + message=message, + current_room_id=self.page.session.get("current_room"), + ) + + if not result.controls: return - assistant_response_processed = False - assistant_response = None + for control in result.controls: + self.timeline.append(control) - if message.message_type == "chat_message": - m = ChatMessage(message, self.on_edit, self.on_delete) - print("Messagem enviada: \n", message) - - if message.message_type == "login_message": - m = ft.Text(message.text, italic=True, color=ft.Colors.WHITE, size=12) - self.chat.controls.append(m) + if result.should_update_users: self.update_users_drawer() - self.page.update() return - elif message.message_type == "file_message": - file_ext = os.path.splitext(message.file.file_path)[1].lower() - if file_ext in ['.png', '.jpg', '.jpeg', '.gif']: - m = ft.Column([ - ft.Text(f"{message.user_name} compartilhou uma imagem:"), - ft.Image(src=message.file.file_path, width=200, height=200, visible=True, fit=ft.ImageFit.CONTAIN) - ]) - else: - m = ft.Column([ - ft.Text(f"{message.user_name} compartilhou um arquivo:"), - ft.ElevatedButton( - text=os.path.basename(message.file.file_path), - on_click=lambda _: self.page.launch_url(message.file.file_url) - ) - ]) - - self.chat.controls.append(m) - - # Guarda explícita para garantir apenas uma resposta do assistente por evento pubsub. - if message.room_id == "programador" and not assistant_response_processed: - assistant_response_processed = True - assistant_response = self.programador_assistant.process_message(message) - - if assistant_response: - ass_m = ChatMessage(assistant_response, self.on_edit, self.on_delete) - self.chat.controls.append(ass_m) - self.page.update() diff --git a/src/chat/components/chat_drawer.py b/src/chat/components/chat_drawer.py new file mode 100644 index 0000000..e03fe97 --- /dev/null +++ b/src/chat/components/chat_drawer.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import flet as ft + + +class ChatDrawer: + def __init__(self, page: ft.Page, on_change_room, on_send_private_message, on_new_room_click): + self.page = page + self._on_change_room = on_change_room + self._on_send_private_message = on_send_private_message + self._on_new_room_click = on_new_room_click + self.new_room_btn = ft.ElevatedButton( + text="Nova sala", + on_click=self._on_new_room_click, + icon=ft.Icons.MEETING_ROOM, + width=130, + ) + self.drawer = ft.NavigationDrawer(controls=[]) + self.menu_button = ft.IconButton( + icon=ft.Icons.MENU, + tooltip="Abrir menu", + on_click=lambda _: self.open(), + ) + + def build(self, rooms: dict, active_users: dict): + rooms_controls = [ + ft.ListTile( + leading=ft.Icon(ft.Icons.CHAT_BUBBLE_OUTLINE), + title=ft.Text(value.room.room_name), + on_click=lambda e, room_id=key: self._on_change_room(room_id), + ) + for key, value in rooms.items() + ] + + users_controls = [ + ft.ListTile( + leading=ft.Icon(ft.Icons.ACCOUNT_CIRCLE), + title=ft.Text(user.user_name), + on_click=lambda e, name=user.user_name: self._on_send_private_message(name), + ) + for user in active_users.values() + ] + + self.drawer.controls = [ + ft.Column( + controls=[ + ft.Container(height=12), + ft.Text("Salas de Chat", size=18, weight="bold", text_align="center"), + ft.Divider(), + *rooms_controls, + ], + expand=True, + ), + ft.Column( + controls=[ + ft.Divider(), + ft.Text("Usuários Online", size=16, weight="bold", text_align="center"), + *users_controls, + ], + alignment=ft.MainAxisAlignment.END, + ), + ft.Row([self.new_room_btn], alignment=ft.MainAxisAlignment.CENTER), + ] + + self.page.drawer = self.drawer + + def update_users(self, active_users: dict): + users_controls = [ + ft.ListTile( + leading=ft.Icon(ft.Icons.ACCOUNT_CIRCLE), + title=ft.Text(user.user_name), + on_click=lambda e, name=user.user_name: self._on_send_private_message(name), + ) + for user in active_users.values() + ] + self.drawer.controls[-2] = ft.Column( + controls=[ + ft.Divider(), + ft.Text("Usuários Online", size=16, weight="bold", text_align="center"), + *users_controls, + ], + alignment=ft.MainAxisAlignment.END, + ) + + def open(self): + self.page.drawer.open = True + self.page.update() diff --git a/src/chat/components/chat_timeline.py b/src/chat/components/chat_timeline.py new file mode 100644 index 0000000..753bcd8 --- /dev/null +++ b/src/chat/components/chat_timeline.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import flet as ft + + +class ChatTimeline: + def __init__(self, room_name: str): + self.chat = ft.ListView(expand=True, spacing=10, auto_scroll=True) + self.chat_room_container = ft.Container( + content=self.chat, + border=ft.border.all(1, ft.Colors.OUTLINE), + border_radius=5, + padding=10, + expand=True, + ) + self.room_name = ft.Text(f"Sala: {room_name}", size=20, weight="bold") + + def set_room_name(self, room_name: str): + self.room_name.value = f"Sala: {room_name}" + + def append(self, control): + self.chat.controls.append(control) + + def clear(self): + self.chat.controls.clear() + + def remove(self, control): + self.chat.controls.remove(control) diff --git a/src/chat/components/message_timeline_renderer.py b/src/chat/components/message_timeline_renderer.py new file mode 100644 index 0000000..2a54127 --- /dev/null +++ b/src/chat/components/message_timeline_renderer.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import os + +import flet as ft + +from chat.chat_message import ChatMessage +from chat.entities.message import Message +from chat.interfaces import MessageRendererProtocol + + +class MessageTimelineRenderer(MessageRendererProtocol): + def __init__(self, page: ft.Page, on_edit, on_delete): + self.page = page + self.on_edit = on_edit + self.on_delete = on_delete + + def render_chat_message(self, message: Message): + return ChatMessage(message, self.on_edit, self.on_delete) + + def render_login_message(self, message: Message): + return ft.Text(message.text, italic=True, color=ft.Colors.WHITE, size=12) + + def render_file_message(self, message: Message): + file_ext = os.path.splitext(message.file.file_path)[1].lower() + if file_ext in [".png", ".jpg", ".jpeg", ".gif"]: + return ft.Column([ + ft.Text(f"{message.user_name} compartilhou uma imagem:"), + ft.Image( + src=message.file.file_path, + width=200, + height=200, + visible=True, + fit=ft.ImageFit.CONTAIN, + ), + ]) + + return ft.Column([ + ft.Text(f"{message.user_name} compartilhou um arquivo:"), + ft.ElevatedButton( + text=os.path.basename(message.file.file_path), + on_click=lambda _: self.page.launch_url(message.file.file_url), + ), + ]) diff --git a/src/chat/interfaces.py b/src/chat/interfaces.py new file mode 100644 index 0000000..ebe499a --- /dev/null +++ b/src/chat/interfaces.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Optional, Protocol + +from chat.entities.message import Message + + +class AssistantAdapterProtocol(Protocol): + def should_handle(self, message: Message) -> bool: + ... + + def process_message(self, message: Message) -> Optional[Message]: + ... + + +class MessageRendererProtocol(Protocol): + def render_chat_message(self, message: Message) -> Any: + ... + + def render_login_message(self, message: Message) -> Any: + ... + + def render_file_message(self, message: Message) -> Any: + ... + + +@dataclass +class MessageRouteResult: + controls: list[Any] = field(default_factory=list) + should_update_users: bool = False diff --git a/src/chat/services/message_event_router.py b/src/chat/services/message_event_router.py new file mode 100644 index 0000000..6b30c63 --- /dev/null +++ b/src/chat/services/message_event_router.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from typing import Optional + +from chat.entities.message import Message +from chat.interfaces import AssistantAdapterProtocol, MessageRendererProtocol, MessageRouteResult + + +class MessageEventRouter: + def __init__( + self, + renderer: MessageRendererProtocol, + assistant_adapter: Optional[AssistantAdapterProtocol] = None, + ): + self.renderer = renderer + self.assistant_adapter = assistant_adapter + + def route(self, message: Message, current_room_id: str) -> MessageRouteResult: + if message.room_id != current_room_id: + return MessageRouteResult() + + if message.message_type == "login_message": + return MessageRouteResult( + controls=[self.renderer.render_login_message(message)], + should_update_users=True, + ) + + controls = [] + if message.message_type == "chat_message": + controls.append(self.renderer.render_chat_message(message)) + elif message.message_type == "file_message": + controls.append(self.renderer.render_file_message(message)) + else: + return MessageRouteResult() + + if self.assistant_adapter and self.assistant_adapter.should_handle(message): + assistant_response = self.assistant_adapter.process_message(message) + if assistant_response: + controls.append(self.renderer.render_chat_message(assistant_response)) + + return MessageRouteResult(controls=controls)