From 4fffcc91e5576a9ea0d156623f47c0ec959fd188 Mon Sep 17 00:00:00 2001 From: Hugo Saporetti Junior Date: Tue, 19 Nov 2024 21:13:02 -0300 Subject: [PATCH] TUI Improvements and upgrade textual to latest --- dependencies.hspd | 2 +- src/main/askai/resources/askai.tcss | 29 ++++++++++++++++++---- src/main/askai/tui/app_header.py | 3 ++- src/main/askai/tui/app_icons.py | 8 ++++++- src/main/askai/tui/app_widgets.py | 37 ++++++++++++++++++++++++++++- src/main/askai/tui/askai_app.py | 33 +++++++++++++++---------- src/main/requirements.txt | 2 +- 7 files changed, 91 insertions(+), 23 deletions(-) diff --git a/dependencies.hspd b/dependencies.hspd index ea22eeae..a26b0da1 100644 --- a/dependencies.hspd +++ b/dependencies.hspd @@ -44,7 +44,7 @@ package: html2text, version: 2024.2.26, mode: eq /* CLI/TUI */ package: rich, version: 13.8.1, mode: eq -package: textual, version: 0.80.1, mode: eq +package: textual, version: 0.86.3, mode: eq /* Audio */ package: soundfile, version: 0.12.1, mode: eq diff --git a/src/main/askai/resources/askai.tcss b/src/main/askai/resources/askai.tcss index f5f3efc3..27cf3c35 100644 --- a/src/main/askai/resources/askai.tcss +++ b/src/main/askai/resources/askai.tcss @@ -96,7 +96,7 @@ HeaderNotifications { padding: 0 1 0 0; } -MenuIcon { +Header > MenuIcon { padding: 0 1 0 0; width: 4; content-align: center middle; @@ -181,19 +181,38 @@ MarkdownBullet { color: #7FD5AD; } -Input { +InputArea { + width: 100%; + height: 4; + layout: vertical; +} + +InputArea > Input { background: #051416; - padding: 0 1; border: round #183236; - width: 100%; + padding: 0 1; height: 3; } +InputArea > InputIcons { + background: #051416; + padding-left: 1; + height: 1; + layout: horizontal; +} + +InputIcons > MenuIcon { + background: #051416; + height: 1; + width: 4; + content-align: center middle; +} + Input:focus, MarkdownViewer:focus, MarkdownTableOfContents:focus { border: round #7FD5AD; } -LoadingIndicator { +Input > LoadingIndicator { color: #7FD5AD; background: #051416; } diff --git a/src/main/askai/tui/app_header.py b/src/main/askai/tui/app_header.py index ed7ce057..f1d4b70a 100644 --- a/src/main/askai/tui/app_header.py +++ b/src/main/askai/tui/app_header.py @@ -52,6 +52,7 @@ def screen_sub_title(self) -> str: def compose(self): """Compose the Header Widget.""" + yield MenuIcon(AppIcons.EXIT.value, "Exit the application", self.app.exit) yield MenuIcon(AppIcons.TOC.value, "Show/Hide Table of Contents", self._show_toc) yield MenuIcon(AppIcons.CONSOLE.value, "Show console", self._show_console) yield MenuIcon(AppIcons.SETTINGS.value, "Show settings", self._show_settings) @@ -169,7 +170,7 @@ def render(self) -> RenderResult: ) def refresh_icons(self) -> None: - """Update the application widgets. This callback is required because ask_and_reply is async.""" + """Update the application widgets.""" self.headphones = recorder.is_headphones() self.debugging = configs.is_debug self.caching = configs.is_cache diff --git a/src/main/askai/tui/app_icons.py b/src/main/askai/tui/app_icons.py index 1093b25d..58f858be 100644 --- a/src/main/askai/tui/app_icons.py +++ b/src/main/askai/tui/app_icons.py @@ -19,7 +19,7 @@ class AppIcons(Enumeration): """Enumerated icons of the new AskAI UI application.""" - # icons:                      鬒  穀          + # icons:                  鬒          DEFAULT = "" STARTED = "" @@ -43,3 +43,9 @@ class AppIcons(Enumeration): BUILT_IN_SPEAKER = "" HEADPHONES = "" CLOCK = "" + SEND = "" + STOP = "" + REGENERATE = "" + LIKE = "" + DISLIKE = "" + COPY_REPLY = "穀" diff --git a/src/main/askai/tui/app_widgets.py b/src/main/askai/tui/app_widgets.py index 6f9bdef4..d455d216 100644 --- a/src/main/askai/tui/app_widgets.py +++ b/src/main/askai/tui/app_widgets.py @@ -19,10 +19,12 @@ from textual.events import Click from textual.reactive import Reactive from textual.widget import Widget -from textual.widgets import Collapsible, DataTable, Markdown, Static +from textual.widgets import Collapsible, DataTable, Markdown, Static, Input from textwrap import dedent from typing import Callable, Optional +from askai.tui.app_suggester import InputSuggester + class MenuIcon(Widget): """Display an 'icon' on the left of the header.""" @@ -139,3 +141,36 @@ def watch_data(self) -> None: label = Text(str(i), style="#B0FC38 italic") self.table.add_row(*row[1:], key=row[0], label=label) self.refresh() + + +class InputArea(Widget): + """Application Input Area.""" + + def __init__(self, engine_nickname: str): + super().__init__() + self._engine_nickname = engine_nickname + + @property + def engine_name(self) -> str: + return self._engine_nickname + + def compose(self) -> ComposeResult: + """Called to add widgets to the app.""" + suggester = InputSuggester(case_sensitive=False) + yield InputIcons() + yield Input(placeholder=f"Message {self.engine_name}", suggester=suggester) + + +class InputIcons(Static): + """Application Input Icons Area.""" + + def __init__(self): + super().__init__() + + def compose(self) -> ComposeResult: + """Called to add widgets to the app.""" + yield MenuIcon(AppIcons.SEND.value, "Send", self.app.exit) + yield MenuIcon(AppIcons.COPY_REPLY.value, "Copy last response", self.app.exit) + yield MenuIcon(AppIcons.REGENERATE.value, "Regenerate response", self.app.exit) + yield MenuIcon(AppIcons.LIKE.value, "Good response", self.app.exit) + yield MenuIcon(AppIcons.DISLIKE.value, "Bad response", self.app.exit) diff --git a/src/main/askai/tui/askai_app.py b/src/main/askai/tui/askai_app.py index b62fb51a..4885089e 100644 --- a/src/main/askai/tui/askai_app.py +++ b/src/main/askai/tui/askai_app.py @@ -15,7 +15,15 @@ from askai.__classpath__ import classpath from askai.core.askai import AskAi from askai.core.askai_configs import configs -from askai.core.askai_events import * +from askai.core.askai_events import ( + AskAiEvents, + ASKAI_BUS_NAME, + REPLY_EVENT, + MIC_LISTENING_EVENT, + DEVICE_CHANGED_EVENT, + MODE_CHANGED_EVENT, + events, +) from askai.core.askai_messages import msg from askai.core.askai_prompt import prompt from askai.core.commander.commander import commander_help @@ -31,7 +39,7 @@ from askai.tui.app_header import Header from askai.tui.app_icons import AppIcons from askai.tui.app_suggester import InputSuggester -from askai.tui.app_widgets import AppHelp, AppInfo, AppSettings, Splash +from askai.tui.app_widgets import AppHelp, AppInfo, AppSettings, Splash, InputArea from hspylib.core.enums.charset import Charset from hspylib.core.tools.commons import file_is_not_empty from hspylib.core.tools.text_tools import ensure_endswith @@ -78,9 +86,9 @@ def __init__( self, speak: bool, debug: bool, cacheable: bool, tempo: int, engine_name: str, model_name: str, mode: RouterMode ): super().__init__() - self._askai = AskAi(speak, debug, cacheable, tempo, engine_name, model_name, mode) - self._re_render = True - self._display_buffer = list() + self._askai: AskAi = AskAi(speak, debug, cacheable, tempo, engine_name, model_name, mode) + self._re_render: bool = True + self._display_buffer: list[str] = list() self._startup() def __str__(self) -> str: @@ -149,7 +157,6 @@ def footer(self) -> Footer: def compose(self) -> ComposeResult: """Called to add widgets to the app.""" - suggester = InputSuggester(case_sensitive=False) footer = Footer() footer.upper_case_keys = True footer.ctrl_to_caret = True @@ -160,18 +167,18 @@ def compose(self) -> ComposeResult: yield Splash(self.askai.SPLASH) yield AppHelp(commander_help()) yield MarkdownViewer() - yield Input(placeholder=f"Message {self.engine.nickname()}", suggester=suggester) + yield InputArea(self.engine.nickname()) yield footer async def on_mount(self) -> None: """Called application is mounted.""" - self.enable_controls(False) self.screen.title = self.APP_TITLE self.screen.sub_title = self.engine.ai_model_name() self.md_console.set_class(True, "-hidden") self.md_console.show_table_of_contents = False - self._setup() self.md_console.set_interval(0.25, self._cb_refresh_console) + self.enable_controls(False) + self._setup() def on_markdown_viewer_navigator_updated(self) -> None: """Refresh bindings for forward / back when the document changes.""" @@ -241,7 +248,7 @@ async def action_debugging(self) -> None: """Toggle Debugging ON/OFF.""" self.ask_and_reply("/debug") - @work(thread=True, exclusive=True) + @work(thread=True) async def action_ptt(self) -> None: """Handle the Push-To-Talk (PTT) action for Speech-To-Text (STT) input. This method allows the user to use Push-To-Talk as an input method, converting spoken words into text. @@ -370,9 +377,9 @@ def ask_and_reply(self, question: str) -> tuple[bool, Optional[str]]: :param question: The question to ask the AI engine. :return: A tuple containing a boolean indicating success or failure, and the AI's reply as an optional string. """ - self.enable_controls(False) + self.call_from_thread(self.enable_controls, False) status, reply = self.askai.ask_and_reply(question) - self.enable_controls() + self.call_from_thread(self.enable_controls) return status, reply @@ -389,7 +396,7 @@ def _startup(self) -> None: askai_bus.subscribe(MODE_CHANGED_EVENT, self._cb_mode_changed_event) log.info("AskAI is ready to use!") - @work(thread=True, exclusive=True) + @work(thread=True) def _setup(self) -> None: """Setup the TUI controls.""" player.start_delay() diff --git a/src/main/requirements.txt b/src/main/requirements.txt index f1186dbf..e1f909c6 100644 --- a/src/main/requirements.txt +++ b/src/main/requirements.txt @@ -24,7 +24,7 @@ protobuf==4.25.4 aiohttp==3.10.5 html2text==2024.2.26 rich==13.8.1 -textual==0.80.1 +textual==0.86.3 soundfile==0.12.1 PyAudio==0.2.14 SpeechRecognition==3.10.4