yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
chat_tui.cc
Go to the documentation of this file.
1#include "cli/tui/chat_tui.h"
2
3#include <algorithm>
4#include <cmath>
5#include <utility>
6#include <vector>
7
8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
10#include "absl/time/time.h"
11#include "rom/rom.h"
13#include "cli/tui/tui.h"
14#include "ftxui/component/component.hpp"
15#include "ftxui/component/component_base.hpp"
16#include "ftxui/component/event.hpp"
17#include "ftxui/component/screen_interactive.hpp"
18#include "ftxui/dom/elements.hpp"
19#include "ftxui/dom/table.hpp"
20
21namespace yaze {
22namespace cli {
23namespace tui {
24
25using namespace ftxui;
26
27namespace {
28const std::vector<std::string> kSpinnerFrames = {"⠋", "⠙", "⠹", "⠸", "⠼",
29 "⠴", "⠦", "⠧", "⠇", "⠏"};
30
31Element RenderPanelPanel(const std::string& title,
32 const std::vector<Element>& body, Color border_color,
33 bool highlight = false) {
34 auto panel = window(text(title) | bold, vbox(body));
35 if (highlight) {
36 panel = panel | color(border_color) | bgcolor(Color::GrayDark);
37 } else {
38 panel = panel | color(border_color);
39 }
40 return panel;
41}
42
43Element RenderLatencySparkline(const std::vector<double>& data) {
44 if (data.empty()) {
45 return text("No latency data yet") | dim;
46 }
47 Elements bars;
48 for (double d : data) {
49 bars.push_back(gauge(d) | flex);
50 }
51 return hbox(bars);
52}
53
54Element RenderMetricLabel(const std::string& icon, const std::string& label,
55 const std::string& value, Color color) {
56 return hbox({text(icon) | ftxui::color(color),
57 text(" " + label + ": ") | bold,
58 text(value) | ftxui::color(color)});
59}
60} // namespace
61
62ChatTUI::ChatTUI(Rom* rom_context) : rom_context_(rom_context) {
63 if (rom_context_ != nullptr) {
65 rom_header_ = absl::StrFormat("ROM: %s | Size: %d bytes",
67 } else {
68 rom_header_ = "No ROM loaded.";
69 }
70 auto status = todo_manager_.Initialize();
71 todo_manager_ready_ = status.ok();
73 quick_actions_ = {"List dungeon entrances",
74 "Show sprite palette summary",
75 "Summarize overworld map",
76 "Find unused rooms",
77 "Explain ROM header",
78 "Search dialogue for 'Master Sword'",
79 "Suggest QA checklist",
80 "Show TODO status",
81 "Show ROM info"};
82}
83
87
88void ChatTUI::SetRomContext(Rom* rom_context) {
89 rom_context_ = rom_context;
91 if (rom_context_ != nullptr) {
92 rom_header_ = absl::StrFormat("ROM: %s | Size: %d bytes",
94 } else {
95 rom_header_ = "No ROM loaded.";
96 }
97}
98
100 autocomplete_engine_.RegisterCommand("/help", "Show help message");
101 autocomplete_engine_.RegisterCommand("/exit", "Exit the chat");
102 autocomplete_engine_.RegisterCommand("/quit", "Exit the chat");
103 autocomplete_engine_.RegisterCommand("/clear", "Clear chat history");
104 autocomplete_engine_.RegisterCommand("/rom_info", "Display ROM information");
105 autocomplete_engine_.RegisterCommand("/status", "Show chat statistics");
106
107 // Add common prompts
108 autocomplete_engine_.RegisterCommand("What is", "Ask a question about ROM");
109 autocomplete_engine_.RegisterCommand("How do I", "Get help with a task");
110 autocomplete_engine_.RegisterCommand("Show me", "Request information");
111 autocomplete_engine_.RegisterCommand("List all", "List items from ROM");
112 autocomplete_engine_.RegisterCommand("Find", "Search for something");
113}
114
116 auto input_message = std::make_shared<std::string>();
117
118 // Create autocomplete input component
119 auto input_component =
120 CreateAutocompleteInput(input_message.get(), &autocomplete_engine_);
121
122 auto todo_popup_toggle = [this] {
124 };
125 auto shortcut_palette_toggle = [this] {
127 };
128
129 // Handle Enter key BEFORE adding to container
130 input_component = CatchEvent(input_component,
131 [this, input_message, todo_popup_toggle,
132 shortcut_palette_toggle](const Event& event) {
133 if (event == Event::Return) {
134 if (input_message->empty())
135 return true;
136 OnSubmit(*input_message);
137 input_message->clear();
138 return true;
139 }
140 if (event == Event::Special({20})) { // Ctrl+T
141 todo_popup_toggle();
142 return true;
143 }
144 if (event == Event::Special({11})) { // Ctrl+K
145 shortcut_palette_toggle();
146 return true;
147 }
148 return false;
149 });
150
151 auto send_button = Button("Send", [this, input_message] {
152 if (input_message->empty())
153 return;
154 OnSubmit(*input_message);
155 input_message->clear();
156 });
157
158 auto quick_pick_index = std::make_shared<int>(0);
159 auto quick_pick_menu = Menu(&quick_actions_, quick_pick_index.get());
160
163
164 // Add both input and button to container
165 auto input_container = Container::Horizontal({
166 input_component,
167 send_button,
168 });
169
170 input_component->TakeFocus();
171
172 auto main_renderer = Renderer(input_container, [this, input_component,
173 send_button, quick_pick_menu,
174 quick_pick_index] {
175 const auto& history = agent_service_.GetHistory();
176
177 // Build history view from current history state
178 std::vector<Element> history_elements;
179
180 for (const auto& msg : history) {
181 Element header =
182 text(msg.sender == agent::ChatMessage::Sender::kUser ? "You"
183 : "Agent") |
184 bold |
185 color(msg.sender == agent::ChatMessage::Sender::kUser ? Color::Yellow
186 : Color::Green);
187
188 Element body;
189 if (msg.table_data.has_value()) {
190 std::vector<std::vector<std::string>> table_rows;
191 if (!msg.table_data->headers.empty()) {
192 table_rows.push_back(msg.table_data->headers);
193 }
194 for (const auto& row : msg.table_data->rows) {
195 table_rows.push_back(row);
196 }
197 Table table(table_rows);
198 table.SelectAll().Border(LIGHT);
199 if (!msg.table_data->headers.empty()) {
200 table.SelectRow(0).Decorate(bold);
201 }
202 body = table.Render();
203 } else if (msg.json_pretty.has_value()) {
204 // Word wrap for JSON
205 body = paragraphAlignLeft(msg.json_pretty.value());
206 } else {
207 // Word wrap for regular messages
208 body = paragraphAlignLeft(msg.message);
209 }
210
211 auto message_block = vbox({
212 header,
213 separator(),
214 body,
215 });
216
217 if (msg.metrics.has_value()) {
218 const auto& metrics = msg.metrics.value();
219 message_block =
220 vbox({message_block, separator(),
221 text(absl::StrFormat("📊 Turn %d | Elapsed: %.2fs",
222 metrics.turn_index,
223 metrics.total_elapsed_seconds)) |
224 color(Color::Cyan) | dim});
225 }
226
227 history_elements.push_back(message_block | border);
228 }
229
230 Element history_view;
231 if (history.empty()) {
232 history_view =
233 vbox({text("yaze TUI") | bold | center,
234 text("A conversational agent for ROM hacking") | center,
235 separator(), text("No messages yet. Start chatting!") | dim}) |
236 flex | center;
237 } else {
238 history_view = vbox(history_elements) | vscroll_indicator | frame | flex;
239 }
240
241 // Build info panel with responsive layout
242 auto metrics = CurrentMetrics();
243
244 Element header_line =
245 hbox({text(rom_header_) | bold, filler(),
246 agent_busy_.load() ? text(kSpinnerFrames[spinner_index_.load() %
247 kSpinnerFrames.size()]) |
248 color(Color::Yellow)
249 : text("✓") | color(Color::GreenLight)});
250
251 std::vector<Element> info_cards;
252 info_cards.push_back(RenderPanelPanel(
253 "Session",
254 {
255 RenderMetricLabel("🕒", "Turns",
256 absl::StrFormat("%d", metrics.turn_index),
257 Color::Cyan),
258 RenderMetricLabel(
259 "🙋", "User",
260 absl::StrFormat("%d", metrics.total_user_messages),
261 Color::White),
262 RenderMetricLabel(
263 "🤖", "Agent",
264 absl::StrFormat("%d", metrics.total_agent_messages),
265 Color::GreenLight),
266 RenderMetricLabel("🔧", "Tools",
267 absl::StrFormat("%d", metrics.total_tool_calls),
268 Color::YellowLight),
269 },
270 Color::GrayLight));
271
272 info_cards.push_back(RenderPanelPanel(
273 "Latency",
274 {RenderMetricLabel("⚡", "Last",
275 absl::StrFormat("%.2fs", last_response_seconds_),
276 Color::Yellow),
277 RenderMetricLabel(
278 "📈", "Average",
279 absl::StrFormat("%.2fs", metrics.average_latency_seconds),
280 Color::MagentaLight),
281 RenderLatencySparkline(latency_history_)},
282 Color::Magenta, agent_busy_.load()));
283
284 info_cards.push_back(RenderPanelPanel(
285 "Shortcuts",
286 {
287 text("⌨ Enter ↵ Send") | dim,
288 text("⌨ Shift+Enter ↩ Multiline") | dim,
289 text("⌨ /help, /rom_info, /status") | dim,
290 text("⌨ Ctrl+T TODO overlay · Ctrl+K shortcuts · f fullscreen") |
291 dim,
292 },
293 Color::BlueLight));
294
295 Elements layout_elements;
296 layout_elements.push_back(header_line);
297 layout_elements.push_back(separatorLight());
298 layout_elements.push_back(
299 vbox({hbox({
300 info_cards[0] | flex,
301 separator(),
302 info_cards[1] | flex,
303 separator(),
304 info_cards[2] | flex,
305 }) | flex,
306 separator(), history_view | bgcolor(Color::Black) | flex}) |
307 flex);
308
309 // Add metrics bar
310 layout_elements.push_back(separatorLight());
311 layout_elements.push_back(
312 hbox({text("Turns: ") | bold,
313 text(absl::StrFormat("%d", metrics.turn_index)), separator(),
314 text("User: ") | bold,
315 text(absl::StrFormat("%d", metrics.total_user_messages)),
316 separator(), text("Agent: ") | bold,
317 text(absl::StrFormat("%d", metrics.total_agent_messages)),
318 separator(), text("Tools: ") | bold,
319 text(absl::StrFormat("%d", metrics.total_tool_calls)), filler(),
320 text("Last response " +
321 absl::StrFormat("%.2fs", last_response_seconds_)) |
322 color(Color::GrayLight)}) |
323 color(Color::GrayLight));
324
325 // Add error if present
326 if (last_error_.has_value()) {
327 layout_elements.push_back(separator());
328 layout_elements.push_back(text(absl::StrCat("⚠ ERROR: ", *last_error_)) |
329 color(Color::Red));
330 }
331
332 // Add input area
333 layout_elements.push_back(separator());
334 layout_elements.push_back(
335 vbox({text("Quick Actions") | bold,
336 quick_pick_menu->Render() | frame | size(HEIGHT, EQUAL, 5) | flex,
337 separatorLight(), input_component->Render() | flex}));
338 layout_elements.push_back(hbox({
339 text("Press Enter to send | ") | dim,
340 send_button->Render(),
341 text(" | Tab quick actions · Ctrl+T TODO "
342 "overlay · Ctrl+K shortcuts") |
343 dim,
344 }) |
345 center);
346
347 Element base =
348 vbox(layout_elements) | borderRounded | bgcolor(Color::Black);
349
352 std::vector<Element> overlays;
353 overlays.push_back(base);
355 overlays.push_back(todo_popup_component_->Render());
356 }
358 overlays.push_back(shortcut_palette_component_->Render());
359 }
360 base = dbox(overlays);
361 }
362
363 return base;
364 });
365
366 screen_.Loop(main_renderer);
367}
368
369void ChatTUI::OnSubmit(const std::string& message) {
370 if (message.empty()) {
371 return;
372 }
373
374 if (message == "/exit" || message == "/quit") {
375 screen_.Exit();
376 return;
377 }
378 if (message == "/clear") {
380 // The renderer will see history is empty and detach children
381 return;
382 }
383 if (message == "/rom_info") {
384 auto response =
385 agent_service_.SendMessage("Show me information about the loaded ROM");
386 if (!response.ok()) {
387 last_error_ = response.status().message();
388 } else {
389 last_error_.reset();
390 }
391 return;
392 }
393 if (message == "/help") {
394 auto response = agent_service_.SendMessage("What commands can I use?");
395 if (!response.ok()) {
396 last_error_ = response.status().message();
397 } else {
398 last_error_.reset();
399 }
400 return;
401 }
402 if (message == "/status") {
403 const auto metrics = agent_service_.GetMetrics();
404 std::string status_message = absl::StrFormat(
405 "Chat Statistics:\n"
406 "- Total Turns: %d\n"
407 "- User Messages: %d\n"
408 "- Agent Messages: %d\n"
409 "- Tool Calls: %d\n"
410 "- Commands: %d\n"
411 "- Proposals: %d\n"
412 "- Total Elapsed Time: %.2f seconds\n"
413 "- Average Latency: %.2f seconds",
414 metrics.turn_index, metrics.total_user_messages,
415 metrics.total_agent_messages, metrics.total_tool_calls,
416 metrics.total_commands, metrics.total_proposals,
417 metrics.total_elapsed_seconds, metrics.average_latency_seconds);
418
419 // Add a system message with status
420 auto response = agent_service_.SendMessage(status_message);
421 if (!response.ok()) {
422 last_error_ = response.status().message();
423 } else {
424 last_error_.reset();
425 }
426 return;
427 }
428
429 LaunchAgentPrompt(message);
430}
431
432void ChatTUI::LaunchAgentPrompt(const std::string& prompt) {
433 if (prompt.empty()) {
434 return;
435 }
436
437 agent_busy_.store(true);
438 spinner_running_.store(true);
439 if (!spinner_thread_.joinable()) {
440 spinner_thread_ = std::thread([this] {
441 while (spinner_running_.load()) {
442 std::this_thread::sleep_for(std::chrono::milliseconds(90));
443 spinner_index_.fetch_add(1);
444 screen_.PostEvent(Event::Custom);
445 }
446 });
447 }
448
449 last_send_time_ = std::chrono::steady_clock::now();
450
451 auto future = std::async(std::launch::async, [this, prompt] {
452 auto response = agent_service_.SendMessage(prompt);
453 if (!response.ok()) {
454 last_error_ = response.status().message();
455 } else {
456 last_error_.reset();
457 }
458
459 auto end_time = std::chrono::steady_clock::now();
461 std::chrono::duration<double>(end_time - last_send_time_).count();
462
464 if (latency_history_.size() > 30) {
465 latency_history_.erase(latency_history_.begin());
466 }
467
468 agent_busy_.store(false);
469 StopSpinner();
470 screen_.PostEvent(Event::Custom);
471 });
472
473 {
474 std::lock_guard<std::mutex> lock(worker_mutex_);
475 worker_futures_.push_back(std::move(future));
476 }
477}
478
480 std::lock_guard<std::mutex> lock(worker_mutex_);
481 for (auto& future : worker_futures_) {
482 if (future.valid()) {
483 future.wait();
484 }
485 }
486 worker_futures_.clear();
487
488 StopSpinner();
489}
490
494
496 spinner_running_.store(false);
497 if (spinner_thread_.joinable()) {
498 spinner_thread_.join();
499 }
500}
501
511
521
522ftxui::Component ChatTUI::CreateTodoPopup() {
523 auto refresh_button =
524 Button("Refresh", [this] { screen_.PostEvent(Event::Custom); });
525 auto close_button = Button("Close", [this] {
526 todo_popup_visible_ = false;
527 screen_.PostEvent(Event::Custom);
528 });
529
530 auto renderer = Renderer([this, refresh_button, close_button] {
531 Elements rows;
532 if (!todo_manager_ready_) {
533 rows.push_back(text("TODO manager unavailable") | color(Color::Red) |
534 center);
535 } else {
536 auto todos = todo_manager_.GetAllTodos();
537 if (todos.empty()) {
538 rows.push_back(text("No TODOs tracked") | dim | center);
539 } else {
540 for (const auto& item : todos) {
541 rows.push_back(
542 hbox({text(absl::StrFormat("[%s]", item.StatusToString())) |
543 color(Color::Yellow),
544 text(" " + item.description) | flex,
545 text(item.category.empty()
546 ? ""
547 : absl::StrCat(" (", item.category, ")")) |
548 dim}));
549 }
550 }
551 }
552
553 return dbox({window(text("📝 TODO Overlay") | bold,
554 vbox({separatorLight(),
555 vbox(rows) | frame | size(HEIGHT, LESS_THAN, 12) |
556 size(WIDTH, LESS_THAN, 70),
557 separatorLight(),
558 hbox({refresh_button->Render(), text(" "),
559 close_button->Render()}) |
560 center})) |
561 size(WIDTH, LESS_THAN, 72) | size(HEIGHT, LESS_THAN, 18) |
562 center});
563 });
564
565 return renderer;
566}
567
568ftxui::Component ChatTUI::BuildShortcutPalette() {
569 std::vector<std::pair<std::string, std::string>> shortcuts = {
570 {"Ctrl+T", "Toggle TODO overlay"}, {"Ctrl+K", "Shortcut palette"},
571 {"Ctrl+L", "Clear chat history"}, {"Ctrl+Shift+S", "Save transcript"},
572 {"Ctrl+G", "Focus quick actions"}, {"Ctrl+P", "Command palette"},
573 {"Ctrl+F", "Fullscreen chat"}, {"Esc", "Back to unified layout"},
574 };
575
576 auto close_button = Button("Close", [this] {
577 shortcut_palette_visible_ = false;
578 screen_.PostEvent(Event::Custom);
579 });
580
581 auto renderer = Renderer([shortcuts, close_button] {
582 Elements rows;
583 for (const auto& [combo, desc] : shortcuts) {
584 rows.push_back(
585 hbox({text(combo) | bold | color(Color::Cyan), text(" " + desc)}));
586 }
587
588 return dbox(
589 {window(text("⌨ Shortcuts") | bold,
590 vbox({separatorLight(),
591 vbox(rows) | frame | size(HEIGHT, LESS_THAN, 12) |
592 size(WIDTH, LESS_THAN, 60),
593 separatorLight(), close_button->Render() | center})) |
594 size(WIDTH, LESS_THAN, 64) | size(HEIGHT, LESS_THAN, 16) | center});
595 });
596
597 return renderer;
598}
599
600bool ChatTUI::IsPopupOpen() const {
601 return todo_popup_visible_ || shortcut_palette_visible_;
602}
603
604} // namespace tui
605} // namespace cli
606} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
auto size() const
Definition rom.h:134
auto title() const
Definition rom.h:133
void RegisterCommand(const std::string &cmd, const std::string &desc, const std::vector< std::string > &examples={})
absl::StatusOr< ChatMessage > SendMessage(const std::string &message)
const std::vector< ChatMessage > & GetHistory() const
absl::Status Initialize()
Initialize the TODO manager and load persisted data.
std::vector< TodoItem > GetAllTodos() const
Get all TODO items.
std::string rom_header_
Definition chat_tui.h:57
ChatTUI(Rom *rom_context=nullptr)
Definition chat_tui.cc:62
void LaunchAgentPrompt(const std::string &prompt)
Definition chat_tui.cc:432
ftxui::Component todo_popup_component_
Definition chat_tui.h:75
std::atomic< bool > agent_busy_
Definition chat_tui.h:59
ftxui::Component shortcut_palette_component_
Definition chat_tui.h:76
ftxui::ScreenInteractive screen_
Definition chat_tui.h:51
agent::ConversationalAgentService agent_service_
Definition chat_tui.h:52
std::chrono::steady_clock::time_point last_send_time_
Definition chat_tui.h:65
std::vector< double > latency_history_
Definition chat_tui.h:67
void OnSubmit(const std::string &message)
Definition chat_tui.cc:369
std::mutex worker_mutex_
Definition chat_tui.h:64
ftxui::Component CreateTodoPopup()
Definition chat_tui.cc:522
std::thread spinner_thread_
Definition chat_tui.h:71
std::vector< std::future< void > > worker_futures_
Definition chat_tui.h:63
AutocompleteEngine autocomplete_engine_
Definition chat_tui.h:56
std::vector< std::string > quick_actions_
Definition chat_tui.h:69
void SetRomContext(Rom *rom_context)
Definition chat_tui.cc:88
std::atomic< bool > spinner_running_
Definition chat_tui.h:60
agent::TodoManager todo_manager_
Definition chat_tui.h:53
std::optional< std::string > last_error_
Definition chat_tui.h:55
agent::ChatMessage::SessionMetrics CurrentMetrics() const
Definition chat_tui.cc:491
double last_response_seconds_
Definition chat_tui.h:66
std::atomic< int > spinner_index_
Definition chat_tui.h:61
ftxui::Component BuildShortcutPalette()
Definition chat_tui.cc:568
Definition cli.h:17
const std::vector< std::string > kSpinnerFrames
Definition chat_tui.cc:28
Element RenderMetricLabel(const std::string &icon, const std::string &label, const std::string &value, Color color)
Definition chat_tui.cc:54
Element RenderPanelPanel(const std::string &title, const std::vector< Element > &body, Color border_color, bool highlight=false)
Definition chat_tui.cc:31
Element RenderLatencySparkline(const std::vector< double > &data)
Definition chat_tui.cc:43
Component CreateAutocompleteInput(std::string *input_str, AutocompleteEngine *engine)
Create an input component with autocomplete suggestions.