""" 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)}")