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