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"
12#include "cli/tui/tui.h"
13#include "ftxui/component/component.hpp"
14#include "ftxui/component/component_base.hpp"
15#include "ftxui/component/event.hpp"
16#include "ftxui/component/screen_interactive.hpp"
17#include "ftxui/dom/elements.hpp"
18#include "ftxui/dom/table.hpp"
19#include "rom/rom.h"
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", absl::StrFormat("%d", metrics.total_user_messages),
260 Color::White),
261 RenderMetricLabel(
262 "🤖", "Agent",
263 absl::StrFormat("%d", metrics.total_agent_messages),
264 Color::GreenLight),
265 RenderMetricLabel("🔧", "Tools",
266 absl::StrFormat("%d", metrics.total_tool_calls),
267 Color::YellowLight),
268 },
269 Color::GrayLight));
270
271 info_cards.push_back(RenderPanelPanel(
272 "Latency",
273 {RenderMetricLabel("⚡", "Last",
274 absl::StrFormat("%.2fs", last_response_seconds_),
275 Color::Yellow),
276 RenderMetricLabel(
277 "📈", "Average",
278 absl::StrFormat("%.2fs", metrics.average_latency_seconds),
279 Color::MagentaLight),
280 RenderLatencySparkline(latency_history_)},
281 Color::Magenta, agent_busy_.load()));
282
283 info_cards.push_back(RenderPanelPanel(
284 "Shortcuts",
285 {
286 text("⌨ Enter ↵ Send") | dim,
287 text("⌨ Shift+Enter ↩ Multiline") | dim,
288 text("⌨ /help, /rom_info, /status") | dim,
289 text("⌨ Ctrl+T TODO overlay · Ctrl+K shortcuts · f fullscreen") |
290 dim,
291 },
292 Color::BlueLight));
293
294 Elements layout_elements;
295 layout_elements.push_back(header_line);
296 layout_elements.push_back(separatorLight());
297 layout_elements.push_back(
298 vbox({hbox({
299 info_cards[0] | flex,
300 separator(),
301 info_cards[1] | flex,
302 separator(),
303 info_cards[2] | flex,
304 }) | flex,
305 separator(), history_view | bgcolor(Color::Black) | flex}) |
306 flex);
307
308 // Add metrics bar
309 layout_elements.push_back(separatorLight());
310 layout_elements.push_back(
311 hbox({text("Turns: ") | bold,
312 text(absl::StrFormat("%d", metrics.turn_index)), separator(),
313 text("User: ") | bold,
314 text(absl::StrFormat("%d", metrics.total_user_messages)),
315 separator(), text("Agent: ") | bold,
316 text(absl::StrFormat("%d", metrics.total_agent_messages)),
317 separator(), text("Tools: ") | bold,
318 text(absl::StrFormat("%d", metrics.total_tool_calls)), filler(),
319 text("Last response " +
320 absl::StrFormat("%.2fs", last_response_seconds_)) |
321 color(Color::GrayLight)}) |
322 color(Color::GrayLight));
323
324 // Add error if present
325 if (last_error_.has_value()) {
326 layout_elements.push_back(separator());
327 layout_elements.push_back(text(absl::StrCat("⚠ ERROR: ", *last_error_)) |
328 color(Color::Red));
329 }
330
331 // Add input area
332 layout_elements.push_back(separator());
333 layout_elements.push_back(
334 vbox({text("Quick Actions") | bold,
335 quick_pick_menu->Render() | frame | size(HEIGHT, EQUAL, 5) | flex,
336 separatorLight(), input_component->Render() | flex}));
337 layout_elements.push_back(hbox({
338 text("Press Enter to send | ") | dim,
339 send_button->Render(),
340 text(" | Tab quick actions · Ctrl+T TODO "
341 "overlay · Ctrl+K shortcuts") |
342 dim,
343 }) |
344 center);
345
346 Element base =
347 vbox(layout_elements) | borderRounded | bgcolor(Color::Black);
348
351 std::vector<Element> overlays;
352 overlays.push_back(base);
354 overlays.push_back(todo_popup_component_->Render());
355 }
357 overlays.push_back(shortcut_palette_component_->Render());
358 }
359 base = dbox(overlays);
360 }
361
362 return base;
363 });
364
365 screen_.Loop(main_renderer);
366}
367
368void ChatTUI::OnSubmit(const std::string& message) {
369 if (message.empty()) {
370 return;
371 }
372
373 if (message == "/exit" || message == "/quit") {
374 screen_.Exit();
375 return;
376 }
377 if (message == "/clear") {
379 // The renderer will see history is empty and detach children
380 return;
381 }
382 if (message == "/rom_info") {
383 auto response =
384 agent_service_.SendMessage("Show me information about the loaded ROM");
385 if (!response.ok()) {
386 last_error_ = std::string(response.status().message());
387 } else {
388 last_error_.reset();
389 }
390 return;
391 }
392 if (message == "/help") {
393 auto response = agent_service_.SendMessage("What commands can I use?");
394 if (!response.ok()) {
395 last_error_ = std::string(response.status().message());
396 } else {
397 last_error_.reset();
398 }
399 return;
400 }
401 if (message == "/status") {
402 const auto metrics = agent_service_.GetMetrics();
403 std::string status_message = absl::StrFormat(
404 "Chat Statistics:\n"
405 "- Total Turns: %d\n"
406 "- User Messages: %d\n"
407 "- Agent Messages: %d\n"
408 "- Tool Calls: %d\n"
409 "- Commands: %d\n"
410 "- Proposals: %d\n"
411 "- Total Elapsed Time: %.2f seconds\n"
412 "- Average Latency: %.2f seconds",
413 metrics.turn_index, metrics.total_user_messages,
414 metrics.total_agent_messages, metrics.total_tool_calls,
415 metrics.total_commands, metrics.total_proposals,
416 metrics.total_elapsed_seconds, metrics.average_latency_seconds);
417
418 // Add a system message with status
419 auto response = agent_service_.SendMessage(status_message);
420 if (!response.ok()) {
421 last_error_ = std::string(response.status().message());
422 } else {
423 last_error_.reset();
424 }
425 return;
426 }
427
428 LaunchAgentPrompt(message);
429}
430
431void ChatTUI::LaunchAgentPrompt(const std::string& prompt) {
432 if (prompt.empty()) {
433 return;
434 }
435
436 agent_busy_.store(true);
437 spinner_running_.store(true);
438 if (!spinner_thread_.joinable()) {
439 spinner_thread_ = std::thread([this] {
440 while (spinner_running_.load()) {
441 std::this_thread::sleep_for(std::chrono::milliseconds(90));
442 spinner_index_.fetch_add(1);
443 screen_.PostEvent(Event::Custom);
444 }
445 });
446 }
447
448 last_send_time_ = std::chrono::steady_clock::now();
449
450 auto future = std::async(std::launch::async, [this, prompt] {
451 auto response = agent_service_.SendMessage(prompt);
452 if (!response.ok()) {
453 last_error_ = std::string(response.status().message());
454 } else {
455 last_error_.reset();
456 }
457
458 auto end_time = std::chrono::steady_clock::now();
460 std::chrono::duration<double>(end_time - last_send_time_).count();
461
463 if (latency_history_.size() > 30) {
464 latency_history_.erase(latency_history_.begin());
465 }
466
467 agent_busy_.store(false);
468 StopSpinner();
469 screen_.PostEvent(Event::Custom);
470 });
471
472 {
473 std::lock_guard<std::mutex> lock(worker_mutex_);
474 worker_futures_.push_back(std::move(future));
475 }
476}
477
479 std::lock_guard<std::mutex> lock(worker_mutex_);
480 for (auto& future : worker_futures_) {
481 if (future.valid()) {
482 future.wait();
483 }
484 }
485 worker_futures_.clear();
486
487 StopSpinner();
488}
489
493
495 spinner_running_.store(false);
496 if (spinner_thread_.joinable()) {
497 spinner_thread_.join();
498 }
499}
500
510
520
521ftxui::Component ChatTUI::CreateTodoPopup() {
522 auto refresh_button =
523 Button("Refresh", [this] { screen_.PostEvent(Event::Custom); });
524 auto close_button = Button("Close", [this] {
525 todo_popup_visible_ = false;
526 screen_.PostEvent(Event::Custom);
527 });
528
529 auto renderer = Renderer([this, refresh_button, close_button] {
530 Elements rows;
531 if (!todo_manager_ready_) {
532 rows.push_back(text("TODO manager unavailable") | color(Color::Red) |
533 center);
534 } else {
535 auto todos = todo_manager_.GetAllTodos();
536 if (todos.empty()) {
537 rows.push_back(text("No TODOs tracked") | dim | center);
538 } else {
539 for (const auto& item : todos) {
540 rows.push_back(
541 hbox({text(absl::StrFormat("[%s]", item.StatusToString())) |
542 color(Color::Yellow),
543 text(" " + item.description) | flex,
544 text(item.category.empty()
545 ? ""
546 : absl::StrCat(" (", item.category, ")")) |
547 dim}));
548 }
549 }
550 }
551
552 return dbox({window(text("📝 TODO Overlay") | bold,
553 vbox({separatorLight(),
554 vbox(rows) | frame | size(HEIGHT, LESS_THAN, 12) |
555 size(WIDTH, LESS_THAN, 70),
556 separatorLight(),
557 hbox({refresh_button->Render(), text(" "),
558 close_button->Render()}) |
559 center})) |
560 size(WIDTH, LESS_THAN, 72) | size(HEIGHT, LESS_THAN, 18) |
561 center});
562 });
563
564 return renderer;
565}
566
567ftxui::Component ChatTUI::BuildShortcutPalette() {
568 std::vector<std::pair<std::string, std::string>> shortcuts = {
569 {"Ctrl+T", "Toggle TODO overlay"}, {"Ctrl+K", "Shortcut palette"},
570 {"Ctrl+L", "Clear chat history"}, {"Ctrl+Shift+S", "Save transcript"},
571 {"Ctrl+G", "Focus quick actions"}, {"Ctrl+P", "Command palette"},
572 {"Ctrl+F", "Fullscreen chat"}, {"Esc", "Back to unified layout"},
573 };
574
575 auto close_button = Button("Close", [this] {
576 shortcut_palette_visible_ = false;
577 screen_.PostEvent(Event::Custom);
578 });
579
580 auto renderer = Renderer([shortcuts, close_button] {
581 Elements rows;
582 for (const auto& [combo, desc] : shortcuts) {
583 rows.push_back(
584 hbox({text(combo) | bold | color(Color::Cyan), text(" " + desc)}));
585 }
586
587 return dbox(
588 {window(text("⌨ Shortcuts") | bold,
589 vbox({separatorLight(),
590 vbox(rows) | frame | size(HEIGHT, LESS_THAN, 12) |
591 size(WIDTH, LESS_THAN, 60),
592 separatorLight(), close_button->Render() | center})) |
593 size(WIDTH, LESS_THAN, 64) | size(HEIGHT, LESS_THAN, 16) | center});
594 });
595
596 return renderer;
597}
598
599bool ChatTUI::IsPopupOpen() const {
600 return todo_popup_visible_ || shortcut_palette_visible_;
601}
602
603} // namespace tui
604} // namespace cli
605} // 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:431
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:368
std::mutex worker_mutex_
Definition chat_tui.h:64
ftxui::Component CreateTodoPopup()
Definition chat_tui.cc:521
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:490
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:567
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.