feat: Implement AgentChatWidget for conversational AI interaction and add ASCII logo support

This commit is contained in:
scawful
2025-10-03 23:43:19 -04:00
parent 9fa9e2647c
commit a6cdc651c3
8 changed files with 754 additions and 4 deletions

View File

@@ -0,0 +1,382 @@
#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 message is JSON (tool result)
if (!is_user && msg.text.find('[') != std::string::npos &&
msg.text.find('{') != std::string::npos) {
// Try to render as table
RenderTableFromJson(msg.text);
} else {
// Regular text message
ImGui::TextWrapped("%s", msg.text.c_str());
}
ImGui::Unindent(20.0f);
}
void AgentChatWidget::RenderTableFromJson(const std::string& json_str) {
#ifdef YAZE_WITH_JSON
try {
auto json_data = nlohmann::json::parse(json_str);
if (!json_data.is_array() || json_data.empty()) {
ImGui::TextWrapped("%s", json_str.c_str());
return;
}
// Extract column headers from first object
std::vector<std::string> headers;
if (json_data[0].is_object()) {
for (auto& [key, value] : json_data[0].items()) {
headers.push_back(key);
}
}
if (headers.empty()) {
ImGui::TextWrapped("%s", json_str.c_str());
return;
}
// Render table
if (ImGui::BeginTable("ToolResultTable", headers.size(),
ImGuiTableFlags_Borders |
ImGuiTableFlags_RowBg |
ImGuiTableFlags_ScrollY)) {
// Headers
for (const auto& header : headers) {
ImGui::TableSetupColumn(header.c_str());
}
ImGui::TableHeadersRow();
// Rows
for (const auto& row : json_data) {
if (!row.is_object()) continue;
ImGui::TableNextRow();
for (size_t col = 0; col < headers.size(); ++col) {
ImGui::TableSetColumnIndex(col);
const auto& header = headers[col];
if (row.contains(header)) {
std::string cell_value;
if (row[header].is_string()) {
cell_value = row[header].get<std::string>();
} else {
cell_value = row[header].dump();
}
ImGui::TextWrapped("%s", cell_value.c_str());
}
}
}
ImGui::EndTable();
}
} catch (const nlohmann::json::exception& e) {
// Fallback to plain text if JSON parsing fails
ImGui::TextWrapped("%s", json_str.c_str());
}
#else
ImGui::TextWrapped("%s", json_str.c_str());
#endif
}
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;
// Process message through agent service
auto result = agent_service_->ProcessMessage(message);
if (!result.ok()) {
// Add error message to history
cli::agent::ChatMessage error_msg;
error_msg.sender = cli::agent::ChatMessage::Sender::kAgent;
error_msg.text = absl::StrFormat("Error: %s", result.status().message());
error_msg.timestamp = absl::Now();
// Note: We'd need to expose AddMessage in the service to do this properly
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["text"] = msg.text;
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

View File

@@ -0,0 +1,83 @@
#ifndef YAZE_APP_GUI_WIDGETS_AGENT_CHAT_WIDGET_H_
#define YAZE_APP_GUI_WIDGETS_AGENT_CHAT_WIDGET_H_
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "cli/service/agent/conversational_agent_service.h"
#include "app/rom.h"
namespace yaze {
namespace app {
namespace gui {
/**
* @class AgentChatWidget
* @brief ImGui widget for conversational AI agent interaction
*
* Provides a chat-like interface in the YAZE GUI for interacting with the
* z3ed AI agent. Shares the same backend as the TUI chat interface.
*/
class AgentChatWidget {
public:
AgentChatWidget();
~AgentChatWidget();
// Initialize with ROM context
void Initialize(Rom* rom);
// Main render function - call this in your ImGui loop
void Render(bool* p_open = nullptr);
// Load/save chat history
absl::Status LoadHistory(const std::string& filepath);
absl::Status SaveHistory(const std::string& filepath);
// Clear conversation history
void ClearHistory();
// Get the underlying service for advanced usage
cli::agent::ConversationalAgentService* GetService() {
return agent_service_.get();
}
private:
void RenderChatHistory();
void RenderInputArea();
void RenderToolbar();
void RenderMessageBubble(const cli::agent::ChatMessage& msg, int index);
void RenderTableFromJson(const std::string& json_str);
void SendMessage(const std::string& message);
void ScrollToBottom();
// UI State
char input_buffer_[4096];
bool scroll_to_bottom_;
bool auto_scroll_;
bool show_timestamps_;
bool show_reasoning_;
float message_spacing_;
// Agent service
std::unique_ptr<cli::agent::ConversationalAgentService> agent_service_;
Rom* rom_;
// UI colors
struct Colors {
ImVec4 user_bubble;
ImVec4 agent_bubble;
ImVec4 system_text;
ImVec4 error_text;
ImVec4 tool_call_bg;
ImVec4 timestamp_text;
} colors_;
};
} // namespace gui
} // namespace app
} // namespace yaze
#endif // YAZE_APP_GUI_WIDGETS_AGENT_CHAT_WIDGET_H_