Skip to content

Commit

Permalink
TUI improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
yorevs committed Nov 21, 2024
1 parent 4fffcc9 commit e435c8b
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 61 deletions.
2 changes: 1 addition & 1 deletion src/main/askai/core/askai.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def app_settings(self) -> list[tuple[str, ...]]:
all_settings.append(r)
return all_settings

def abort(self, signals: Any, frame: Any) -> None:
def abort(self, signals: Any | None = None, frame: Any | None = None) -> None:
"""Hook the SIGINT signal for cleanup or execution interruption. If two signals arrive within 1 second,
abort the application execution.
:param signals: Signal number from the operating system.
Expand Down
13 changes: 10 additions & 3 deletions src/main/askai/core/commander/commands/history_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
Copyright (c) 2024, HomeSetup
"""
from abc import ABC
from typing import Optional

from askai.core.support.shared_instances import shared
from askai.core.support.text_formatter import text_formatter
from askai.core.support.utilities import display_text
Expand Down Expand Up @@ -70,21 +72,26 @@ def context_forget(context: str | None = None) -> None:
)

@staticmethod
def context_copy(name: str | None = None) -> None:
def context_copy(name: str | None = None) -> Optional[str]:
"""Copy a context entry to the clipboard.
:param name: The name of the context entry to copy. If None, the default context will be copied.
"""
copied_text: str | None = None
if (name := name.upper()) in shared.context.keys:
if (ctx := str(shared.context.flat(name.upper()))) and (
stripped_role := re.sub(r"^((system|human|assistant):\s*)", "", ctx, flags=re.MULTILINE | re.IGNORECASE)
copied_text := re.sub(
r"^((system|human|AI|assistant):\s*)", "", ctx, flags=re.MULTILINE | re.DOTALL | re.IGNORECASE
)
):
pyperclip.copy(stripped_role)
pyperclip.copy(copied_text)
text_formatter.commander_print(f"`{name}` copied to the clipboard!")
else:
text_formatter.commander_print(f"There is nothing to copy from `{name}`!")
else:
text_formatter.commander_print(f"Context name not found: `{name}`!")

return copied_text

@staticmethod
def history_list() -> None:
"""List the input history entries."""
Expand Down
12 changes: 7 additions & 5 deletions src/main/askai/resources/askai.tcss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Styles reference: https://textual.textualize.io/styles

.-hidden {
display: none;
visibility: hidden;
Expand Down Expand Up @@ -183,25 +185,25 @@ MarkdownBullet {

InputArea {
width: 100%;
height: 4;
height: 6;
layout: vertical;
border: round #183236;
}

InputArea > Input {
background: #051416;
border: round #183236;
padding: 0 1;
height: 3;
}

InputArea > InputIcons {
InputArea > InputActions {
background: #051416;
padding-left: 1;
color: #7FD5AD 70%;
height: 1;
layout: horizontal;
}

InputIcons > MenuIcon {
InputActions > MenuIcon {
background: #051416;
height: 1;
width: 4;
Expand Down
7 changes: 3 additions & 4 deletions src/main/askai/tui/app_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class AppIcons(Enumeration):
HELP = ""
SETTINGS = ""
INFO = ""
CONSOLE = ""
CONSOLE = ""
DEBUG_ON = ""
DEBUG_OFF = ""
SPEAKING_ON = "墳"
Expand All @@ -43,9 +43,8 @@ class AppIcons(Enumeration):
BUILT_IN_SPEAKER = ""
HEADPHONES = ""
CLOCK = ""
SEND = ""
STOP = ""
REGENERATE = ""
REGENERATE = ""
LIKE = ""
DISLIKE = ""
COPY_REPLY = "穀"
READ_ALOUD = ""
67 changes: 54 additions & 13 deletions src/main/askai/tui/app_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,25 @@
Copyright (c) 2024, HomeSetup
"""
from askai.tui.app_icons import AppIcons
import os
import re
import tempfile
from textwrap import dedent
from typing import Callable, Optional

from rich.text import Text
from textual import work
from textual.app import ComposeResult, RenderResult
from textual.containers import Container
from textual.events import Click
from textual.reactive import Reactive
from textual.widget import Widget
from textual.widgets import Collapsible, DataTable, Markdown, Static, Input
from textwrap import dedent
from typing import Callable, Optional

from askai.core.commander.commands.history_cmd import HistoryCmd
from askai.core.commander.commands.tts_stt_cmd import TtsSttCmd
from askai.core.support.shared_instances import shared
from askai.tui.app_icons import AppIcons
from askai.tui.app_suggester import InputSuggester


Expand Down Expand Up @@ -96,7 +104,7 @@ class AppInfo(Static):
"""
)

def __init__(self, app_info: str):
def __init__(self, app_info: str = ""):
super().__init__()
self.info_text = app_info

Expand Down Expand Up @@ -157,20 +165,53 @@ def engine_name(self) -> str:
def compose(self) -> ComposeResult:
"""Called to add widgets to the app."""
suggester = InputSuggester(case_sensitive=False)
yield InputIcons()
yield InputActions()
yield Input(placeholder=f"Message {self.engine_name}", suggester=suggester)


class InputIcons(Static):
class InputActions(Static):
"""Application Input Icons Area."""

def __init__(self):
super().__init__()
@staticmethod
def copy() -> None:
"""TODO"""
HistoryCmd.context_copy("LAST_REPLY")

def __init__(self, **kwargs):
super().__init__(**kwargs)

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)
yield MenuIcon(AppIcons.READ_ALOUD.value, "Read aloud", self.read_aloud)
yield MenuIcon(AppIcons.COPY_REPLY.value, "Copy", self.copy)
# FIXME Uncomment after the rate functionality is ready
# yield MenuIcon(AppIcons.LIKE.value, "Good response", self.like)
# yield MenuIcon(AppIcons.DISLIKE.value, "Bad response", self.dislike)
# yield MenuIcon(AppIcons.REGENERATE.value, "Regenerate response", self.regenerate)

@work(thread=True)
def read_aloud(self) -> None:
"""TODO"""
if (ctx := str(shared.context.flat("LAST_REPLY"))) and (
last_reply := re.sub(
r"^((system|human|AI|assistant):\s*)", "", ctx, flags=re.MULTILINE | re.DOTALL | re.IGNORECASE
)
):
TtsSttCmd.tts(last_reply.strip(), os.environ.get("TEMP", tempfile.gettempdir()), True)

def submit(self) -> None:
"""TODO"""
line_input: Input = self.app.line_input
self.post_message(Input.Submitted(line_input, line_input.value))

def like(self) -> None:
"""TODO"""
pass

def dislike(self) -> None:
"""TODO"""
pass

def regenerate(self) -> None:
"""TODO"""
pass
74 changes: 39 additions & 35 deletions src/main/askai/tui/askai_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,28 @@
Copyright (c) 2024, HomeSetup
"""
from pathlib import Path
from typing import Optional
import logging as log
import os

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
from hspylib.core.zoned_datetime import DATE_FORMAT, now, TIME_FORMAT
from hspylib.modules.application.version import Version
from hspylib.modules.cli.vt100.vt_color import VtColor
from hspylib.modules.eventbus.event import Event
from textual import on, work
from textual.app import App, ComposeResult
from textual.containers import ScrollableContainer
from textual.widgets import Footer, Input, MarkdownViewer
import nltk

from askai.__classpath__ import classpath
from askai.core.askai import AskAi
from askai.core.askai_configs import configs
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_events import *
from askai.core.askai_messages import msg
from askai.core.askai_prompt import prompt
from askai.core.commander.commander import commander_help
Expand All @@ -39,24 +49,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, 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
from hspylib.core.zoned_datetime import DATE_FORMAT, now, TIME_FORMAT
from hspylib.modules.application.version import Version
from hspylib.modules.cli.vt100.vt_color import VtColor
from hspylib.modules.eventbus.event import Event
from pathlib import Path
from textual import on, work
from textual.app import App, ComposeResult
from textual.containers import ScrollableContainer
from textual.widgets import Footer, Input, MarkdownViewer
from typing import Optional

import logging as log
import nltk
import os
from askai.tui.app_widgets import AppHelp, AppInfo, AppSettings, Splash, InputArea, InputActions

SOURCE_DIR: Path = classpath.source_path

Expand All @@ -77,6 +70,7 @@ class AskAiApp(App[None]):
("d", "debugging", " Debugging"),
("s", "speaking", " Speaking"),
("ctrl+l", "ptt", " Push-to-Talk"),
("ctrl+s", "stop", " Stop-GenAi"),
]
# fmt: on

Expand Down Expand Up @@ -140,6 +134,10 @@ def line_input(self) -> Input:
"""Get the Input widget."""
return self.query_one(Input)

@property
def input_actions(self):
return self.query_one(InputActions)

@property
def suggester(self) -> Optional[InputSuggester]:
"""Get the Input Suggester."""
Expand All @@ -163,7 +161,7 @@ def compose(self) -> ComposeResult:
yield Header()
with ScrollableContainer():
yield AppSettings()
yield AppInfo("")
yield AppInfo()
yield Splash(self.askai.SPLASH)
yield AppHelp(commander_help())
yield MarkdownViewer()
Expand Down Expand Up @@ -215,6 +213,7 @@ def enable_controls(self, enable: bool = True) -> None:
"""Enable or disable all UI controls, including the header, input, and footer.
:param enable: Whether to enable (True) or disable (False) the UI controls (default is True).
"""
self.input_actions.set_class(not enable, "-hidden")
self.header.disabled = not enable
self.line_input.loading = not enable
self.footer.disabled = not enable
Expand Down Expand Up @@ -262,18 +261,23 @@ async def action_ptt(self) -> None:
cache.save_input_history(suggestions)
self.enable_controls()

async def action_stop(self) -> None:
"""Stop generating response."""
self.askai.abort()
self.enable_controls()

@on(Input.Submitted)
async def on_submit(self, submitted: Input.Submitted) -> None:
"""A coroutine to handle input submission events.
:param submitted: The event that contains the submitted data.
"""
question: str = submitted.value
self.line_input.clear()
self.display_text(f"{shared.username_md}: {question}")
if self.ask_and_reply(question):
await self.suggester.add_suggestion(question)
suggestions = await self.suggester.suggestions()
cache.save_input_history(suggestions)
if question := submitted.value:
self.line_input.clear()
self.display_text(f"{shared.username_md}: {question}")
if self.ask_and_reply(question):
await self.suggester.add_suggestion(question)
suggestions = await self.suggester.suggestions()
cache.save_input_history(suggestions)

async def _write_markdown(self) -> None:
"""Write buffered text to the markdown file.
Expand Down

0 comments on commit e435c8b

Please sign in to comment.