yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
agent_chat_widget.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <fstream>
6#include <iostream>
7
8#include "absl/strings/str_format.h"
9#include "absl/time/time.h"
10#include "imgui/imgui.h"
11#include "imgui/misc/cpp/imgui_stdlib.h"
12#include "util/platform_paths.h"
13
14#ifdef YAZE_WITH_JSON
15#include "nlohmann/json.hpp"
16#endif
17
18namespace yaze {
19
20namespace gui {
21
22namespace {
23
24std::string ResolveAgentChatHistoryPath() {
25 auto agent_dir = util::PlatformPaths::GetAppDataSubdirectory("agent");
26 if (agent_dir.ok()) {
27 return (*agent_dir / "agent_chat_history.json").string();
28 }
30 if (docs_dir.ok()) {
31 return (*docs_dir / "agent_chat_history.json").string();
32 }
34 if (temp_dir.ok()) {
35 return (*temp_dir / "agent_chat_history.json").string();
36 }
37 return (std::filesystem::current_path() / "agent_chat_history.json").string();
38}
39
40} // namespace
41
43 : scroll_to_bottom_(false),
44 auto_scroll_(true),
45 show_timestamps_(true),
46 show_reasoning_(false),
47 message_spacing_(12.0f),
48 rom_(nullptr) {
49 memset(input_buffer_, 0, sizeof(input_buffer_));
50
51 // Initialize colors with a pleasant dark theme
52 colors_.user_bubble = ImVec4(0.2f, 0.4f, 0.8f, 1.0f); // Blue
53 colors_.agent_bubble = ImVec4(0.3f, 0.3f, 0.35f, 1.0f); // Dark gray
54 colors_.system_text = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); // Light gray
55 colors_.error_text = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red
56 colors_.tool_call_bg = ImVec4(0.2f, 0.5f, 0.3f, 0.3f); // Green tint
57 colors_.timestamp_text = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); // Medium gray
58
59#ifdef Z3ED_AI
60 agent_service_ = std::make_unique<cli::agent::ConversationalAgentService>();
61#endif
62}
63
65
67 rom_ = rom;
68#ifdef Z3ED_AI
69 if (agent_service_ && rom_) {
70 agent_service_->SetRomContext(rom_);
71 }
72#endif
73}
74
75void AgentChatWidget::Render(bool* p_open) {
76#ifndef Z3ED_AI
77 ImGui::Begin("Agent Chat", p_open);
78 ImGui::TextColored(colors_.error_text, "AI features not available");
79 ImGui::TextWrapped(
80 "Build with -DZ3ED_AI=ON to enable the conversational agent.");
81 ImGui::End();
82 return;
83#else
84
85 ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_FirstUseEver);
86 if (!ImGui::Begin("Z3ED Agent Chat", p_open)) {
87 ImGui::End();
88 return;
89 }
90
91 // Render toolbar at top
93 ImGui::Separator();
94
95 // Chat history area (scrollable)
96 ImGui::BeginChild("ChatHistory",
97 ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - 60), true,
98 ImGuiWindowFlags_AlwaysVerticalScrollbar);
100
101 // Auto-scroll to bottom when new messages arrive
102 if (scroll_to_bottom_ ||
103 (auto_scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) {
104 ImGui::SetScrollHereY(1.0f);
105 scroll_to_bottom_ = false;
106 }
107
108 ImGui::EndChild();
109
110 // Input area at bottom
112
113 ImGui::End();
114#endif
115}
116
118 if (ImGui::Button("Clear History")) {
119 ClearHistory();
120 }
121 ImGui::SameLine();
122
123 if (ImGui::Button("Save History")) {
124 std::string filepath = ResolveAgentChatHistoryPath();
125 if (auto status = SaveHistory(filepath); !status.ok()) {
126 std::cerr << "Failed to save history: " << status.message() << std::endl;
127 } else {
128 std::cout << "Saved chat history to: " << filepath << std::endl;
129 }
130 }
131 ImGui::SameLine();
132
133 if (ImGui::Button("Load History")) {
134 std::string filepath = ResolveAgentChatHistoryPath();
135 if (auto status = LoadHistory(filepath); !status.ok()) {
136 std::cerr << "Failed to load history: " << status.message() << std::endl;
137 }
138 }
139
140 ImGui::SameLine();
141 ImGui::Checkbox("Auto-scroll", &auto_scroll_);
142
143 ImGui::SameLine();
144 ImGui::Checkbox("Show Timestamps", &show_timestamps_);
145
146 ImGui::SameLine();
147 ImGui::Checkbox("Show Reasoning", &show_reasoning_);
148}
149
151#ifdef Z3ED_AI
152 if (!agent_service_)
153 return;
154
155 const auto& history = agent_service_->GetHistory();
156
157 if (history.empty()) {
158 ImGui::TextColored(
160 "No messages yet. Type a message below to start chatting!");
161 return;
162 }
163
164 for (size_t i = 0; i < history.size(); ++i) {
165 RenderMessageBubble(history[i], i);
166 ImGui::Spacing();
167 if (message_spacing_ > 0) {
168 ImGui::Dummy(ImVec2(0, message_spacing_));
169 }
170 }
171#endif
172}
173
175 int index) {
176 bool is_user = (msg.sender == cli::agent::ChatMessage::Sender::kUser);
177
178 // Timestamp (if enabled)
179 if (show_timestamps_) {
180 std::string timestamp =
181 absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone());
182 ImGui::TextColored(colors_.timestamp_text, "[%s]", timestamp.c_str());
183 ImGui::SameLine();
184 }
185
186 // Sender label
187 const char* sender_label = is_user ? "You" : "Agent";
188 ImVec4 sender_color = is_user ? colors_.user_bubble : colors_.agent_bubble;
189 ImGui::TextColored(sender_color, "%s:", sender_label);
190
191 // Message content
192 ImGui::Indent(20.0f);
193
194 // Check if we have table data to render
195 if (!is_user && msg.table_data.has_value()) {
196 RenderTableData(msg.table_data.value());
197 } else if (!is_user && msg.json_pretty.has_value()) {
198 ImGui::TextWrapped("%s", msg.json_pretty.value().c_str());
199 } else {
200 // Regular text message
201 ImGui::TextWrapped("%s", msg.message.c_str());
202 }
203
204 ImGui::Unindent(20.0f);
205}
206
209 if (table.headers.empty()) {
210 return;
211 }
212
213 // Render table
214 if (ImGui::BeginTable("ToolResultTable", table.headers.size(),
215 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
216 ImGuiTableFlags_ScrollY)) {
217 // Headers
218 for (const auto& header : table.headers) {
219 ImGui::TableSetupColumn(header.c_str());
220 }
221 ImGui::TableHeadersRow();
222
223 // Rows
224 for (const auto& row : table.rows) {
225 ImGui::TableNextRow();
226 for (size_t col = 0; col < std::min(row.size(), table.headers.size());
227 ++col) {
228 ImGui::TableSetColumnIndex(col);
229 ImGui::TextWrapped("%s", row[col].c_str());
230 }
231 }
232
233 ImGui::EndTable();
234 }
235}
236
238 ImGui::Separator();
239 ImGui::Text("Message:");
240
241 // Multi-line input
242 ImGui::PushItemWidth(-1);
243 bool enter_pressed = ImGui::InputTextMultiline(
244 "##input", input_buffer_, sizeof(input_buffer_), ImVec2(-1, 60),
245 ImGuiInputTextFlags_EnterReturnsTrue);
246 ImGui::PopItemWidth();
247
248 // Send button
249 if (ImGui::Button("Send", ImVec2(100, 0)) || enter_pressed) {
250 if (strlen(input_buffer_) > 0) {
252 memset(input_buffer_, 0, sizeof(input_buffer_));
253 ImGui::SetKeyboardFocusHere(-1); // Keep focus on input
254 }
255 }
256
257 ImGui::SameLine();
258 ImGui::TextColored(colors_.system_text,
259 "Tip: Press Enter to send (Shift+Enter for newline)");
260}
261
262void AgentChatWidget::SendMessage(const std::string& message) {
263#ifdef Z3ED_AI
264 if (!agent_service_)
265 return;
266
267 // Send message through agent service
268 auto result = agent_service_->SendMessage(message);
269
270 if (!result.ok()) {
271 std::cerr << "Error processing message: " << result.status() << std::endl;
272 }
273
274 scroll_to_bottom_ = true;
275#endif
276}
277
279#ifdef Z3ED_AI
280 if (agent_service_) {
281 agent_service_->ResetConversation();
282 }
283#endif
284}
285
286absl::Status AgentChatWidget::LoadHistory(const std::string& filepath) {
287#if defined(Z3ED_AI) && defined(YAZE_WITH_JSON)
288 if (!agent_service_) {
289 return absl::FailedPreconditionError("Agent service not initialized");
290 }
291
292 std::ifstream file(filepath);
293 if (!file.is_open()) {
294 return absl::NotFoundError(
295 absl::StrFormat("Could not open file: %s", filepath));
296 }
297
298 try {
299 nlohmann::json j;
300 file >> j;
301
302 // Parse and load messages
303 // Note: This would require exposing a LoadHistory method in
304 // ConversationalAgentService For now, we'll just return success
305
306 return absl::OkStatus();
307 } catch (const nlohmann::json::exception& e) {
308 return absl::InvalidArgumentError(
309 absl::StrFormat("Failed to parse JSON: %s", e.what()));
310 }
311#else
312 return absl::UnimplementedError("AI features not available");
313#endif
314}
315
316absl::Status AgentChatWidget::SaveHistory(const std::string& filepath) {
317#if defined(Z3ED_AI) && defined(YAZE_WITH_JSON)
318 if (!agent_service_) {
319 return absl::FailedPreconditionError("Agent service not initialized");
320 }
321
322 std::filesystem::path path(filepath);
323 if (path.has_parent_path()) {
324 std::error_code ec;
325 std::filesystem::create_directories(path.parent_path(), ec);
326 if (ec) {
327 return absl::InternalError(absl::StrFormat(
328 "Failed to create history directory: %s", ec.message()));
329 }
330 }
331
332 std::ofstream file(filepath);
333 if (!file.is_open()) {
334 return absl::InternalError(
335 absl::StrFormat("Could not create file: %s", filepath));
336 }
337
338 try {
339 nlohmann::json j;
340 const auto& history = agent_service_->GetHistory();
341
342 j["version"] = 1;
343 j["messages"] = nlohmann::json::array();
344
345 for (const auto& msg : history) {
346 nlohmann::json msg_json;
347 msg_json["sender"] =
348 (msg.sender == cli::agent::ChatMessage::Sender::kUser) ? "user"
349 : "agent";
350 msg_json["message"] = msg.message;
351 msg_json["timestamp"] = absl::FormatTime(msg.timestamp);
352 j["messages"].push_back(msg_json);
353 }
354
355 file << j.dump(2); // Pretty print with 2-space indent
356
357 return absl::OkStatus();
358 } catch (const nlohmann::json::exception& e) {
359 return absl::InternalError(
360 absl::StrFormat("Failed to serialize JSON: %s", e.what()));
361 }
362#else
363 return absl::UnimplementedError("AI features not available");
364#endif
365}
366
370
371} // namespace gui
372
373} // 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
void RenderTableData(const cli::agent::ChatMessage::TableData &table)
void RenderMessageBubble(const cli::agent::ChatMessage &msg, int index)
absl::Status LoadHistory(const std::string &filepath)
void Render(bool *p_open=nullptr)
struct yaze::gui::AgentChatWidget::Colors colors_
void SendMessage(const std::string &message)
absl::Status SaveHistory(const std::string &filepath)
std::unique_ptr< cli::agent::ConversationalAgentService > agent_service_
static absl::StatusOr< std::filesystem::path > GetTempDirectory()
Get a temporary directory for the application.
static absl::StatusOr< std::filesystem::path > GetAppDataSubdirectory(const std::string &subdir)
Get a subdirectory within the app data folder.
static absl::StatusOr< std::filesystem::path > GetUserDocumentsSubdirectory(const std::string &subdir)
Get a subdirectory within the user documents folder.
std::vector< std::vector< std::string > > rows
std::optional< std::string > json_pretty