Skip to content

Commit

Permalink
Merge pull request #196 from hoomano/cli-client
Browse files Browse the repository at this point in the history
Cli client
  • Loading branch information
KellyRousselHoomano authored Jul 25, 2024
2 parents 370fa9e + bea4a4d commit cadf1b6
Show file tree
Hide file tree
Showing 22 changed files with 1,135 additions and 261 deletions.
12 changes: 12 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Mojodex CLI

CLI is still in very early phase and work in progress

## (FUTURE – NOT READY) Usage

```shell
(venv) user@system cli % mojodex run --task-name meeting_minutes
(venv) user@system cli % mojodex run --task-name ideas_to_note
(venv) user@system cli % mojodex run --task-name general_assistance
(venv) user@system cli % md general_assistance
```
199 changes: 199 additions & 0 deletions cli/components/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
from datetime import datetime
from textual.widget import Widget, events
from textual.widgets import Static
from textual.widgets import Button, Markdown, LoadingIndicator
from textual.containers import Vertical
from textual.app import ComposeResult
from textual import work
import threading
from entities.session import Session
from services.messaging import Messaging
from services.audio import AudioRecorder
from services.user_services import start_user_task_execution
from entities.message import PartialMessage, Message
import time




class MicButton(Button):
def __init__(self, id: str):
self.is_recording = False
super().__init__("🎤", id=id)
self.styles.width='100%'


def switch(self):
self.is_recording = not self.is_recording
if self.is_recording:
self.variant='error'
self.label='🔴'
self.active_effect_duration = 0
else:
self.variant='success'
self.label = '🎤'
self.active_effect_duration = 0.2


def _on_mount(self, event: events.Mount):
super()._on_mount(event)
self.focus()




class MessageWidget(Static):
def __init__(self, message: PartialMessage) -> None:
self.message = message
super().__init__(f"{self.message.icon}: {self.message.message}")
self.styles.border = ("round", "lightgrey") if self.message.author == "mojo" else ("round", "dodgerblue")
self.styles.color = "lightgrey" if self.message.author == "mojo" else "dodgerblue"

class MessagesList(Static):
def __init__(self, messages: list[PartialMessage], session_id: str) -> None:
try:
self.messages = messages
self.partial_message_placeholder : PartialMessage = None
self.session_id = session_id
self.message_placeholder_widget = None
if self.messages[-1].author == "user":
self.partial_message_placeholder = PartialMessage("", "mojo")
self.message_placeholder_widget = MessageWidget(self.partial_message_placeholder)
Messaging().on_mojo_token_callback = self.on_mojo_token_callback
super().__init__(classes="chat-messages-panel")
except Exception as e:
self.notify(message=f"Error: {e}", title="Error")

def on_mojo_token_callback(self, token_from_mojo):
try:
if self.message_placeholder_widget:
self.partial_message_placeholder.message=token_from_mojo['text']
self.message_placeholder_widget.update(f"{self.partial_message_placeholder.icon}: {self.partial_message_placeholder.message}")
except Exception as e:
self.notify(message=f"Error: {e}", title="Error")


def compose(self) -> ComposeResult:
try:
for message in self.messages:
if not message.is_draft:
yield MessageWidget(message)
if self.message_placeholder_widget:
yield self.message_placeholder_widget
except Exception as e:
self.notify(message=f"Error: {e}", title="Error")

class Chat(Widget):

def __init__(self, session_id: str, init_message:str, on_new_result: callable) -> None:
self.session = Session(session_id)
self.mic_button_id = 'mic_button'
self.mic_button_height = 8
self.mic_button_margin = 1
self.recorder = AudioRecorder()
self.recording_thread = None
self.init_message = init_message
self.partial_message_placeholder : PartialMessage = None

self.on_new_result = on_new_result
Messaging().connect_to_session(self.session.session_id)
Messaging().on_mojo_message_callback = self.on_mojo_message_callback
Messaging().on_draft_message_callback = self.on_draft_message_callback

super().__init__()

self.styles.height = "100%"
self.styles.content_align = ("center", "bottom")

self.mic_button_widget = MicButton(id=self.mic_button_id)
self.mic_button_widget.styles.height=self.mic_button_height
self.mic_button_widget.styles.margin = self.mic_button_margin

self.messages_list_widget = MessagesList(self.session.messages, self.session.session_id) if self.session.messages else Markdown(self.init_message, id="task_execution_description")

self.loading_indicator = LoadingIndicator()
self.loading_indicator.styles.height=self.mic_button_height
self.loading_indicator.styles.margin = self.mic_button_margin


def on_draft_message_callback(self, message_from_mojo):
try:
self.on_new_result(message_from_mojo)
message = Message(message_from_mojo["message_pk"], message_from_mojo['text'], "mojo", is_draft=True)
self.session.messages.append(message)

self._update_chat_interface_on_message_receveived()
except Exception as e:
self.notify(message=f"Error: {e}", title="Error")

def on_mojo_message_callback(self, message_from_mojo):
try:
message = Message(message_from_mojo["message_pk"], message_from_mojo['text'], "mojo")
self.session.messages.append(message)

self._update_chat_interface_on_message_receveived()
except Exception as e:
self.notify(message=f"Error: {e}", title="Error")


def _update_chat_interface_on_message_receveived(self):
try:
self.messages_list_widget.remove()
mounting_on = self.query_one(f"#chat-interface", Widget)
self.messages_list_widget = MessagesList(self.session.messages, self.session.session_id)
self.app.call_from_thread(lambda: mounting_on.mount(self.messages_list_widget, before=0))

self.loading_indicator.remove()
mounting_on = self.query_one(f"#chat-interface", Widget)
self.app.call_from_thread(lambda: mounting_on.mount(self.mic_button_widget))
except Exception as e:
raise Exception(f"_update_chat_interface_on_message_receveived: {e}")

def compose(self):
try:
with Vertical(id="chat-interface"):
yield self.messages_list_widget
yield self.mic_button_widget
except Exception as e:
self.notify(message=f"Error: {e}", title="Error")




def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id.startswith(self.mic_button_id) and isinstance(event.button, MicButton):
button = event.button
if not button.is_recording:
if self.recording_thread is None or not self.recording_thread.is_alive():
self.recording_thread = threading.Thread(target=self.recorder.start_recording, args=(self.notify,))
self.recording_thread.start()

else:
if self.recording_thread and self.recording_thread.is_alive():
self.mic_button_widget.remove()
mounting_on = self.query_one(f"#chat-interface", Widget)
mounting_on.mount(self.loading_indicator)
thread = threading.Thread(target=self.process_recording)
thread.start()
button.switch()



def process_recording(self):

self.recorder.stop_recording(self.notify)
self.recording_thread.join()

message : Message = start_user_task_execution(self.session.session_id, datetime.now().isoformat())
self.session.messages.append(message)

self.messages_list_widget.remove()
mounting_on = self.query_one(f"#chat-interface", Widget)
self.messages_list_widget = MessagesList(self.session.messages, self.session.session_id)
self.app.call_from_thread(lambda: mounting_on.mount(self.messages_list_widget, before=0))






22 changes: 22 additions & 0 deletions cli/components/menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from textual.widgets import Button
from textual.widget import Widget
from textual.containers import ScrollableContainer


class MenuItem(Button):
def __init__(self, name: str, action: callable, id: str=None, classes=None) -> None:
self.action = action
super().__init__(name, id=id, classes=classes)
self.styles.width='100%'

class Menu(ScrollableContainer):

def __init__(self, menu_items: list[MenuItem], id: str= None) -> None:
self.menu_items = menu_items
super().__init__(id=id)


def compose(self):
for item in self.menu_items:
yield item

100 changes: 100 additions & 0 deletions cli/components/mojodex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from textual.app import App, ComposeResult
from textual.widget import Widget
from textual.widgets import Header, Footer
from textual.containers import Vertical
from textual.reactive import reactive
from textual.keys import Keys
from textual.binding import Binding

import threading

from components.menu import Menu, MenuItem


class Mojodex(App):
CSS_PATH = "style.tcss"
BINDINGS= [
Binding(key="q", action="quit", description="Quit"),
Binding(key="n", action="new_task", description="New Task")
]
def action_new_task(self):
try:
# go to new task screen
self.navigate(self.menu.menu_items[0])
self.current_menu = self.menu
self.focus_option(0)
except Exception as e:
self.notify(f"Error: {e}")


def action_quit(self):
self.app.exit()

focused_index = reactive(0)

def __init__(self, menu) -> None:
self.menu : Menu = menu
super().__init__()
self.initial_widget = self.menu.menu_items[1].action()
self.current_page_id = self.initial_widget.id
self.current_menu = self.menu

def compose(self) -> ComposeResult:
yield Header(id="header")
with Vertical(id="sidebar"):
yield self.menu
yield self.initial_widget
yield Footer()

def on_mount(self) -> None:
self.focus_option(1) # Initially focus the first button

def focus_option(self, index: int) -> None:
if 0 <= index < len(self.current_menu.menu_items):
self.current_menu.menu_items[index].focus()
self.focused_index = index

def on_key(self, event) -> None:
if event.key == Keys.Up:
self.focus_option((self.focused_index - 1) % len(self.current_menu.menu_items))
elif event.key == Keys.Down:
self.focus_option((self.focused_index + 1) % len(self.current_menu.menu_items))
elif event.key == Keys.Right:
try:
self.current_menu = self.query_one(f"#{self.current_page_id}", Widget).menu if not None else self.menu
except Exception as e:
self.notify(f"Error: {e}")
self.current_menu = self.menu
self.focus_option(0)
elif event.key == Keys.Left:
self.current_menu = self.menu
self.focus_option(0)

def on_button_pressed(self, event: MenuItem.Pressed) -> None:
if isinstance(event.button, MenuItem):
menu_item = event.button
if menu_item.id in [item.id for item in self.menu.menu_items]:
self.navigate(menu_item)


def navigate(self, menu_item):
try:
body = self.query_one(f"#{self.current_page_id}", Widget)
body.remove()

menu_item_action_thread = threading.Thread(target=self._mount_menu_item, args=(menu_item,))
menu_item_action_thread.start()
except Exception as e:
self.notify(f"Error: {e}", severity="error", title="Error")


def _mount_menu_item(self, menu_item):
try:
widget = menu_item.action()
self.current_page_id = widget.id
self.app.call_from_thread(lambda: self.mount(widget))
except Exception as e:
self.notify(f"Error: {e}", severity="error", title="Error")



Loading

0 comments on commit cadf1b6

Please sign in to comment.