339 lines
9.3 KiB
C++
339 lines
9.3 KiB
C++
#include "app/gui/widgets/agent_chat_widget.h"
|
|
|
|
#include <algorithm>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
|
|
#include "imgui/imgui.h"
|
|
#include "imgui/misc/cpp/imgui_stdlib.h"
|
|
#include "absl/strings/str_format.h"
|
|
#include "absl/time/time.h"
|
|
|
|
#ifdef YAZE_WITH_JSON
|
|
#include "nlohmann/json.hpp"
|
|
#endif
|
|
|
|
namespace yaze {
|
|
namespace app {
|
|
namespace gui {
|
|
|
|
AgentChatWidget::AgentChatWidget()
|
|
: scroll_to_bottom_(false),
|
|
auto_scroll_(true),
|
|
show_timestamps_(true),
|
|
show_reasoning_(false),
|
|
message_spacing_(12.0f),
|
|
rom_(nullptr) {
|
|
memset(input_buffer_, 0, sizeof(input_buffer_));
|
|
|
|
// Initialize colors with a pleasant dark theme
|
|
colors_.user_bubble = ImVec4(0.2f, 0.4f, 0.8f, 1.0f); // Blue
|
|
colors_.agent_bubble = ImVec4(0.3f, 0.3f, 0.35f, 1.0f); // Dark gray
|
|
colors_.system_text = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); // Light gray
|
|
colors_.error_text = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red
|
|
colors_.tool_call_bg = ImVec4(0.2f, 0.5f, 0.3f, 0.3f); // Green tint
|
|
colors_.timestamp_text = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); // Medium gray
|
|
|
|
#ifdef Z3ED_AI_AVAILABLE
|
|
agent_service_ = std::make_unique<cli::agent::ConversationalAgentService>();
|
|
#endif
|
|
}
|
|
|
|
AgentChatWidget::~AgentChatWidget() = default;
|
|
|
|
void AgentChatWidget::Initialize(Rom* rom) {
|
|
rom_ = rom;
|
|
#ifdef Z3ED_AI_AVAILABLE
|
|
if (agent_service_ && rom_) {
|
|
agent_service_->SetRomContext(rom_);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void AgentChatWidget::Render(bool* p_open) {
|
|
#ifndef Z3ED_AI_AVAILABLE
|
|
ImGui::Begin("Agent Chat", p_open);
|
|
ImGui::TextColored(colors_.error_text,
|
|
"AI features not available");
|
|
ImGui::TextWrapped(
|
|
"Build with -DZ3ED_AI=ON to enable the conversational agent.");
|
|
ImGui::End();
|
|
return;
|
|
#else
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_FirstUseEver);
|
|
if (!ImGui::Begin("Z3ED Agent Chat", p_open)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
// Render toolbar at top
|
|
RenderToolbar();
|
|
ImGui::Separator();
|
|
|
|
// Chat history area (scrollable)
|
|
ImGui::BeginChild("ChatHistory",
|
|
ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - 60),
|
|
true,
|
|
ImGuiWindowFlags_AlwaysVerticalScrollbar);
|
|
RenderChatHistory();
|
|
|
|
// Auto-scroll to bottom when new messages arrive
|
|
if (scroll_to_bottom_ || (auto_scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) {
|
|
ImGui::SetScrollHereY(1.0f);
|
|
scroll_to_bottom_ = false;
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
|
|
// Input area at bottom
|
|
RenderInputArea();
|
|
|
|
ImGui::End();
|
|
#endif
|
|
}
|
|
|
|
void AgentChatWidget::RenderToolbar() {
|
|
if (ImGui::Button("Clear History")) {
|
|
ClearHistory();
|
|
}
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::Button("Save History")) {
|
|
std::string filepath = ".yaze/agent_chat_history.json";
|
|
if (auto status = SaveHistory(filepath); !status.ok()) {
|
|
std::cerr << "Failed to save history: " << status.message() << std::endl;
|
|
} else {
|
|
std::cout << "Saved chat history to: " << filepath << std::endl;
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::Button("Load History")) {
|
|
std::string filepath = ".yaze/agent_chat_history.json";
|
|
if (auto status = LoadHistory(filepath); !status.ok()) {
|
|
std::cerr << "Failed to load history: " << status.message() << std::endl;
|
|
}
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("Auto-scroll", &auto_scroll_);
|
|
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("Show Timestamps", &show_timestamps_);
|
|
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("Show Reasoning", &show_reasoning_);
|
|
}
|
|
|
|
void AgentChatWidget::RenderChatHistory() {
|
|
#ifdef Z3ED_AI_AVAILABLE
|
|
if (!agent_service_) return;
|
|
|
|
const auto& history = agent_service_->GetHistory();
|
|
|
|
if (history.empty()) {
|
|
ImGui::TextColored(colors_.system_text,
|
|
"No messages yet. Type a message below to start chatting!");
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < history.size(); ++i) {
|
|
RenderMessageBubble(history[i], i);
|
|
ImGui::Spacing();
|
|
if (message_spacing_ > 0) {
|
|
ImGui::Dummy(ImVec2(0, message_spacing_));
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void AgentChatWidget::RenderMessageBubble(const cli::agent::ChatMessage& msg, int index) {
|
|
bool is_user = (msg.sender == cli::agent::ChatMessage::Sender::kUser);
|
|
|
|
// Timestamp (if enabled)
|
|
if (show_timestamps_) {
|
|
std::string timestamp = absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone());
|
|
ImGui::TextColored(colors_.timestamp_text, "[%s]", timestamp.c_str());
|
|
ImGui::SameLine();
|
|
}
|
|
|
|
// Sender label
|
|
const char* sender_label = is_user ? "You" : "Agent";
|
|
ImVec4 sender_color = is_user ? colors_.user_bubble : colors_.agent_bubble;
|
|
ImGui::TextColored(sender_color, "%s:", sender_label);
|
|
|
|
// Message content
|
|
ImGui::Indent(20.0f);
|
|
|
|
// Check if we have table data to render
|
|
if (!is_user && msg.table_data.has_value()) {
|
|
RenderTableData(msg.table_data.value());
|
|
} else if (!is_user && msg.json_pretty.has_value()) {
|
|
ImGui::TextWrapped("%s", msg.json_pretty.value().c_str());
|
|
} else {
|
|
// Regular text message
|
|
ImGui::TextWrapped("%s", msg.message.c_str());
|
|
}
|
|
|
|
ImGui::Unindent(20.0f);
|
|
}
|
|
|
|
void AgentChatWidget::RenderTableData(const cli::agent::ChatMessage::TableData& table) {
|
|
if (table.headers.empty()) {
|
|
return;
|
|
}
|
|
|
|
// Render table
|
|
if (ImGui::BeginTable("ToolResultTable", table.headers.size(),
|
|
ImGuiTableFlags_Borders |
|
|
ImGuiTableFlags_RowBg |
|
|
ImGuiTableFlags_ScrollY)) {
|
|
// Headers
|
|
for (const auto& header : table.headers) {
|
|
ImGui::TableSetupColumn(header.c_str());
|
|
}
|
|
ImGui::TableHeadersRow();
|
|
|
|
// Rows
|
|
for (const auto& row : table.rows) {
|
|
ImGui::TableNextRow();
|
|
for (size_t col = 0; col < std::min(row.size(), table.headers.size()); ++col) {
|
|
ImGui::TableSetColumnIndex(col);
|
|
ImGui::TextWrapped("%s", row[col].c_str());
|
|
}
|
|
}
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
|
|
void AgentChatWidget::RenderInputArea() {
|
|
ImGui::Separator();
|
|
ImGui::Text("Message:");
|
|
|
|
// Multi-line input
|
|
ImGui::PushItemWidth(-1);
|
|
bool enter_pressed = ImGui::InputTextMultiline(
|
|
"##input",
|
|
input_buffer_,
|
|
sizeof(input_buffer_),
|
|
ImVec2(-1, 60),
|
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
|
ImGui::PopItemWidth();
|
|
|
|
// Send button
|
|
if (ImGui::Button("Send", ImVec2(100, 0)) || enter_pressed) {
|
|
if (strlen(input_buffer_) > 0) {
|
|
SendMessage(input_buffer_);
|
|
memset(input_buffer_, 0, sizeof(input_buffer_));
|
|
ImGui::SetKeyboardFocusHere(-1); // Keep focus on input
|
|
}
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(colors_.system_text,
|
|
"Tip: Press Enter to send (Shift+Enter for newline)");
|
|
}
|
|
|
|
void AgentChatWidget::SendMessage(const std::string& message) {
|
|
#ifdef Z3ED_AI_AVAILABLE
|
|
if (!agent_service_) return;
|
|
|
|
// Send message through agent service
|
|
auto result = agent_service_->SendMessage(message);
|
|
|
|
if (!result.ok()) {
|
|
std::cerr << "Error processing message: " << result.status() << std::endl;
|
|
}
|
|
|
|
scroll_to_bottom_ = true;
|
|
#endif
|
|
}
|
|
|
|
void AgentChatWidget::ClearHistory() {
|
|
#ifdef Z3ED_AI_AVAILABLE
|
|
if (agent_service_) {
|
|
agent_service_->ClearHistory();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
absl::Status AgentChatWidget::LoadHistory(const std::string& filepath) {
|
|
#if defined(Z3ED_AI_AVAILABLE) && defined(YAZE_WITH_JSON)
|
|
if (!agent_service_) {
|
|
return absl::FailedPreconditionError("Agent service not initialized");
|
|
}
|
|
|
|
std::ifstream file(filepath);
|
|
if (!file.is_open()) {
|
|
return absl::NotFoundError(
|
|
absl::StrFormat("Could not open file: %s", filepath));
|
|
}
|
|
|
|
try {
|
|
nlohmann::json j;
|
|
file >> j;
|
|
|
|
// Parse and load messages
|
|
// Note: This would require exposing a LoadHistory method in ConversationalAgentService
|
|
// For now, we'll just return success
|
|
|
|
return absl::OkStatus();
|
|
} catch (const nlohmann::json::exception& e) {
|
|
return absl::InvalidArgumentError(
|
|
absl::StrFormat("Failed to parse JSON: %s", e.what()));
|
|
}
|
|
#else
|
|
return absl::UnimplementedError("AI features not available");
|
|
#endif
|
|
}
|
|
|
|
absl::Status AgentChatWidget::SaveHistory(const std::string& filepath) {
|
|
#if defined(Z3ED_AI_AVAILABLE) && defined(YAZE_WITH_JSON)
|
|
if (!agent_service_) {
|
|
return absl::FailedPreconditionError("Agent service not initialized");
|
|
}
|
|
|
|
std::ofstream file(filepath);
|
|
if (!file.is_open()) {
|
|
return absl::InternalError(
|
|
absl::StrFormat("Could not create file: %s", filepath));
|
|
}
|
|
|
|
try {
|
|
nlohmann::json j;
|
|
const auto& history = agent_service_->GetHistory();
|
|
|
|
j["version"] = 1;
|
|
j["messages"] = nlohmann::json::array();
|
|
|
|
for (const auto& msg : history) {
|
|
nlohmann::json msg_json;
|
|
msg_json["sender"] = (msg.sender == cli::agent::ChatMessage::Sender::kUser)
|
|
? "user" : "agent";
|
|
msg_json["message"] = msg.message;
|
|
msg_json["timestamp"] = absl::FormatTime(msg.timestamp);
|
|
j["messages"].push_back(msg_json);
|
|
}
|
|
|
|
file << j.dump(2); // Pretty print with 2-space indent
|
|
|
|
return absl::OkStatus();
|
|
} catch (const nlohmann::json::exception& e) {
|
|
return absl::InternalError(
|
|
absl::StrFormat("Failed to serialize JSON: %s", e.what()));
|
|
}
|
|
#else
|
|
return absl::UnimplementedError("AI features not available");
|
|
#endif
|
|
}
|
|
|
|
void AgentChatWidget::ScrollToBottom() {
|
|
scroll_to_bottom_ = true;
|
|
}
|
|
|
|
} // namespace gui
|
|
} // namespace app
|
|
} // namespace yaze
|