Skip to content

Commit

Permalink
TUI Improvements and upgrade textual to latest
Browse files Browse the repository at this point in the history
  • Loading branch information
yorevs committed Nov 20, 2024
1 parent 92f54f0 commit 4fffcc9
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 23 deletions.
2 changes: 1 addition & 1 deletion dependencies.hspd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 24 additions & 5 deletions src/main/askai/resources/askai.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ HeaderNotifications {
padding: 0 1 0 0;
}

MenuIcon {
Header > MenuIcon {
padding: 0 1 0 0;
width: 4;
content-align: center middle;
Expand Down Expand Up @@ -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;
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/askai/tui/app_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion src/main/askai/tui/app_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
class AppIcons(Enumeration):
"""Enumerated icons of the new AskAI UI application."""

# icons:                      鬒         
# icons:                  鬒         

DEFAULT = ""
STARTED = ""
Expand All @@ -43,3 +43,9 @@ class AppIcons(Enumeration):
BUILT_IN_SPEAKER = ""
HEADPHONES = ""
CLOCK = ""
SEND = ""
STOP = ""
REGENERATE = ""
LIKE = ""
DISLIKE = ""
COPY_REPLY = "穀"
37 changes: 36 additions & 1 deletion src/main/askai/tui/app_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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)
33 changes: 20 additions & 13 deletions src/main/askai/tui/askai_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/main/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 4fffcc9

Please sign in to comment.