263 lines
9.0 KiB
Python
263 lines
9.0 KiB
Python
"""
|
||
Module: ui.py
|
||
-------------
|
||
Obsahuje interaktivní rozhraní Robovojtíka založené na knihovně curses.
|
||
Implementuje barevné oddělení, trvalou hlavičku, historii chatu,
|
||
spinner pro indikaci čekání a vstupní oblast. Také prefix 'readfile:'.
|
||
"""
|
||
|
||
import curses
|
||
import time
|
||
import threading
|
||
import logging
|
||
import re
|
||
|
||
import api_interface
|
||
import shell_functions
|
||
import markdown_parser
|
||
|
||
logger = logging.getLogger("robovojtik.ui")
|
||
|
||
# Globální funkce – pro integraci s main.py
|
||
FUNCTIONS = []
|
||
|
||
|
||
def set_functions(funcs):
|
||
global FUNCTIONS
|
||
FUNCTIONS = funcs
|
||
|
||
|
||
automode = False
|
||
|
||
|
||
def main_curses(stdscr):
|
||
curses.start_color()
|
||
curses.use_default_colors()
|
||
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Hlavička
|
||
curses.init_pair(2, curses.COLOR_WHITE, -1) # Chat
|
||
curses.init_pair(3, curses.COLOR_GREEN, -1) # Výstup
|
||
curses.init_pair(4, curses.COLOR_YELLOW, -1) # Vstup
|
||
curses.init_pair(5, curses.COLOR_CYAN, -1) # Spinner
|
||
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_WHITE) # Oddělovač
|
||
|
||
curses.curs_set(1)
|
||
stdscr.nodelay(False)
|
||
stdscr.clear()
|
||
height, width = stdscr.getmaxyx()
|
||
|
||
header_height = 3
|
||
prompt_height = 2
|
||
left_width = width // 2
|
||
right_width = width - left_width - 1
|
||
chat_height = height - header_height - prompt_height
|
||
|
||
header_win = curses.newwin(header_height, left_width, 0, 0)
|
||
chat_win = curses.newwin(chat_height, left_width, header_height, 0)
|
||
prompt_win = curses.newwin(prompt_height, width, height - prompt_height, 0)
|
||
output_win = curses.newwin(height - prompt_height, right_width, 0, left_width + 1)
|
||
divider_win = curses.newwin(height - prompt_height, 1, 0, left_width)
|
||
|
||
header_win.bkgd(' ', curses.color_pair(1))
|
||
chat_win.bkgd(' ', curses.color_pair(2))
|
||
prompt_win.bkgd(' ', curses.color_pair(4))
|
||
output_win.bkgd(' ', curses.color_pair(3))
|
||
divider_win.bkgd(' ', curses.color_pair(6))
|
||
|
||
d_height, _ = divider_win.getmaxyx()
|
||
for y in range(d_height):
|
||
try:
|
||
divider_win.addch(y, 0, curses.ACS_VLINE)
|
||
except curses.error:
|
||
pass
|
||
divider_win.refresh()
|
||
|
||
header_text = [
|
||
"Vítejte v Robovojtikovi!",
|
||
"Zadejte dotaz, příkaz (prefix 'cmd:'), 'automat', 'readfile:' apod.",
|
||
"Pro ukončení zadejte 'vypni' nebo 'exit'."
|
||
]
|
||
max_chars = max(0, left_width - 2)
|
||
for idx, line in enumerate(header_text):
|
||
try:
|
||
header_win.addnstr(idx, 1, line, max_chars)
|
||
except curses.error:
|
||
pass
|
||
header_win.refresh()
|
||
|
||
chat_win.scrollok(True)
|
||
output_win.scrollok(True)
|
||
prompt_win.scrollok(True)
|
||
output_win.box()
|
||
output_win.refresh()
|
||
|
||
def spinner_func(ch):
|
||
mid_x = left_width // 2 - 5
|
||
try:
|
||
header_win.addnstr(1, mid_x, f"Čekám... {ch}", left_width - mid_x - 1,
|
||
curses.color_pair(5) | curses.A_BOLD)
|
||
header_win.refresh()
|
||
except curses.error:
|
||
pass
|
||
|
||
def add_to_chat(text):
|
||
segments = markdown_parser.parse_markdown(text)
|
||
for seg_text, seg_attr in segments:
|
||
try:
|
||
chat_win.addstr(seg_text, seg_attr)
|
||
except curses.error:
|
||
pass
|
||
chat_win.refresh()
|
||
|
||
add_to_chat("Historie chatu:")
|
||
|
||
global automode
|
||
|
||
while True:
|
||
prompt_win.erase()
|
||
try:
|
||
prompt_win.addstr(">> ", curses.A_BOLD)
|
||
except curses.error:
|
||
pass
|
||
prompt_win.refresh()
|
||
curses.echo()
|
||
try:
|
||
user_input = prompt_win.getstr().decode("utf-8").strip()
|
||
except Exception:
|
||
continue
|
||
curses.noecho()
|
||
|
||
if not user_input:
|
||
continue
|
||
|
||
add_to_chat("Ty: " + user_input)
|
||
|
||
# Ukončení
|
||
if user_input.lower() in ["vypni", "exit"]:
|
||
add_to_chat("Ukončuji Robovojtíka...")
|
||
time.sleep(1)
|
||
break
|
||
|
||
# Přepnutí automódu
|
||
if user_input.lower() == "automat":
|
||
automode = not automode
|
||
stav = "zapnut" if automode else "vypnut"
|
||
add_to_chat(f"Automód byl nyní {stav}.")
|
||
continue
|
||
|
||
# readfile:
|
||
if user_input.lower().startswith("readfile:"):
|
||
path = user_input[9:].strip()
|
||
add_to_chat(f"Načítám soubor: {path}")
|
||
from shell_functions import read_file_content
|
||
content = read_file_content(path)
|
||
add_to_chat(f"Obsah souboru:\n{content}")
|
||
assistant_response = api_interface.volani_asistenta(
|
||
f"Analyzuj obsah tohoto souboru:\n{content}",
|
||
spinner_func=spinner_func
|
||
)
|
||
add_to_chat("Robovojtík odpovídá:\n" + assistant_response)
|
||
continue
|
||
|
||
# Přímý příkaz cmd:
|
||
if user_input.startswith("cmd:"):
|
||
command = user_input[4:].strip()
|
||
add_to_chat(f"Rozpoznán příkaz: {command}")
|
||
if not automode:
|
||
prompt_win.erase()
|
||
try:
|
||
prompt_win.addstr("Spustit tento příkaz? (y/n): ", curses.A_BOLD)
|
||
except curses.error:
|
||
pass
|
||
prompt_win.refresh()
|
||
try:
|
||
potvrzeni = prompt_win.getstr().decode("utf-8").strip().lower()
|
||
except Exception:
|
||
potvrzeni = ""
|
||
curses.flushinp() # vyprázdní vstupní buffer
|
||
add_to_chat("Ty: " + potvrzeni)
|
||
if potvrzeni not in ("y", "yes", "ano"):
|
||
add_to_chat("Příkaz nebyl spuštěn.")
|
||
continue
|
||
output = shell_functions.run_command_locally(command)
|
||
output_win.erase()
|
||
try:
|
||
lines = output.splitlines()
|
||
y = 1
|
||
for line in lines:
|
||
output_win.addstr(y, 1, line)
|
||
y += 1
|
||
output_win.box()
|
||
output_win.refresh()
|
||
except curses.error:
|
||
pass
|
||
summary_prompt = (f"Analyzuj následující výstup příkazu. Neposkytuj žádné návrhy příkazů, "
|
||
f"jen shrň výsledky a uveď komentář:\n{output}")
|
||
assistant_summary = api_interface.volani_asistenta(summary_prompt, spinner_func=spinner_func)
|
||
add_to_chat("Robovojtík shrnuje výstup:\n" + assistant_summary)
|
||
continue
|
||
|
||
# Ostatní dotazy -> Asistent
|
||
assistant_response = api_interface.volani_asistenta(user_input, spinner_func=spinner_func)
|
||
add_to_chat("Robovojtík odpovídá:\n" + assistant_response)
|
||
|
||
# Pokud asistent navrhne příkaz, extrahujeme jej
|
||
match = re.search(r"`([^`]+)`", assistant_response)
|
||
if match:
|
||
navrhovany_prikaz = match.group(1)
|
||
elif assistant_response.strip().lower().startswith("navrhovaný příkaz:"):
|
||
lines = assistant_response.splitlines()
|
||
proposal_line = lines[0]
|
||
navrhovany_prikaz = proposal_line[len("navrhovaný příkaz:"):].strip()
|
||
else:
|
||
navrhovany_prikaz = None
|
||
|
||
if navrhovany_prikaz:
|
||
add_to_chat(f"Navrhovaný příkaz: {navrhovany_prikaz}")
|
||
if not automode:
|
||
prompt_win.erase()
|
||
try:
|
||
prompt_win.addstr("Spustit tento příkaz? (y/n): ", curses.A_BOLD)
|
||
except curses.error:
|
||
pass
|
||
prompt_win.refresh()
|
||
try:
|
||
potvrzeni = prompt_win.getstr().decode("utf-8").strip().lower()
|
||
except Exception:
|
||
potvrzeni = ""
|
||
curses.flushinp() # vyprázdní vstupní buffer
|
||
add_to_chat("Ty: " + potvrzeni)
|
||
if potvrzeni not in ("y", "yes", "ano"):
|
||
add_to_chat("Příkaz nebyl spuštěn.")
|
||
continue
|
||
output = shell_functions.run_command_locally(navrhovany_prikaz)
|
||
output_win.erase()
|
||
try:
|
||
lines = output.splitlines()
|
||
y = 1
|
||
for line in lines:
|
||
output_win.addstr(y, 1, line)
|
||
y += 1
|
||
output_win.box()
|
||
output_win.refresh()
|
||
except curses.error:
|
||
pass
|
||
summary_prompt = (
|
||
f"Výsledek příkazu:\n{output}\n\n"
|
||
"Prosím, stručně shrň výše uvedený výstup příkazu a "
|
||
"uveď pouze komentář bez návrhu dalších příkazů."
|
||
)
|
||
assistant_summary = api_interface.volani_asistenta(summary_prompt, spinner_func=spinner_func)
|
||
add_to_chat("Robovojtík shrnuje výstup:\n" + assistant_summary)
|
||
continue
|
||
|
||
prompt_win.erase()
|
||
prompt_win.refresh()
|
||
|
||
|
||
def main_ui():
|
||
try:
|
||
curses.wrapper(main_curses)
|
||
except Exception as e:
|
||
logger.exception("Neočekávaná chyba v curses wrapperu.")
|
||
print(f"Chyba: {str(e)}")
|