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