feat: Introduce Agent Chat History Popup for Enhanced User Interaction

- Added AgentChatHistoryPopup class to provide a sidebar for displaying recent chat messages, improving user accessibility to chat history.
- Integrated the new popup into the editor's source files, ensuring proper inclusion and functionality within the existing UI framework.
- Refactored related files to accommodate the new popup, enhancing code organization and maintainability.
- Updated CMake configuration to include the new source files, ensuring a seamless build process.
This commit is contained in:
scawful
2025-10-05 16:08:44 -04:00
parent c8d7eb3597
commit 409cf357af
8 changed files with 528 additions and 91 deletions

View File

@@ -1,394 +0,0 @@
#include "app/editor/system/agent_chat_history_popup.h"
#include <cstring>
#include "absl/strings/str_format.h"
#include "absl/time/time.h"
#include "app/editor/agent/agent_ui_theme.h"
#include "app/editor/system/toast_manager.h"
#include "app/gui/icons.h"
#include "app/gui/style.h"
#include "imgui/imgui.h"
#include "imgui/misc/cpp/imgui_stdlib.h"
namespace yaze {
namespace editor {
AgentChatHistoryPopup::AgentChatHistoryPopup() {
std::memset(input_buffer_, 0, sizeof(input_buffer_));
}
void AgentChatHistoryPopup::Draw() {
if (!visible_) return;
const auto& theme = AgentUI::GetTheme();
// Set drawer position on the LEFT side (full height)
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(drawer_width_, io.DisplaySize.y), ImGuiCond_Always);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoTitleBar;
// Use current theme colors
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10, 10));
if (ImGui::Begin("##AgentChatPopup", &visible_, flags)) {
DrawHeader();
ImGui::Separator();
ImGui::Spacing();
// Calculate proper list height
float list_height = ImGui::GetContentRegionAvail().y - 220.0f;
ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.code_bg_color);
ImGui::BeginChild("MessageList", ImVec2(0, list_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
DrawMessageList();
if (needs_scroll_) {
ImGui::SetScrollHereY(1.0f);
needs_scroll_ = false;
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::Spacing();
// Quick actions bar
if (show_quick_actions_) {
DrawQuickActions();
ImGui::Spacing();
}
// Input section at bottom
DrawInputSection();
}
ImGui::End();
ImGui::PopStyleVar(2);
}
void AgentChatHistoryPopup::DrawMessageList() {
if (messages_.empty()) {
ImGui::TextDisabled("No messages yet. Start a conversation in the chat window.");
return;
}
// Calculate starting index for display limit
int start_index = messages_.size() > display_limit_ ?
messages_.size() - display_limit_ : 0;
for (int i = start_index; i < messages_.size(); ++i) {
const auto& msg = messages_[i];
// Skip internal messages
if (msg.is_internal) continue;
// Apply filter
if (message_filter_ == MessageFilter::kUserOnly &&
msg.sender != cli::agent::ChatMessage::Sender::kUser) continue;
if (message_filter_ == MessageFilter::kAgentOnly &&
msg.sender != cli::agent::ChatMessage::Sender::kAgent) continue;
DrawMessage(msg, i);
}
}
void AgentChatHistoryPopup::DrawMessage(const cli::agent::ChatMessage& msg, int index) {
ImGui::PushID(index);
bool from_user = (msg.sender == cli::agent::ChatMessage::Sender::kUser);
// Use theme colors with slight tint
ImVec4 text_color = ImGui::GetStyleColorVec4(ImGuiCol_Text);
ImVec4 header_color = from_user
? ImVec4(text_color.x * 1.2f, text_color.y * 0.9f, text_color.z * 0.5f, 1.0f) // Gold tint
: ImVec4(text_color.x * 0.7f, text_color.y * 1.0f, text_color.z * 0.9f, 1.0f); // Teal tint
const char* sender_label = from_user ? ICON_MD_PERSON " You" : ICON_MD_SMART_TOY " Agent";
// Message header
ImGui::TextColored(header_color, "%s", sender_label);
ImGui::SameLine();
ImGui::TextDisabled("%s", absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone()).c_str());
// Message content
ImGui::Indent(10.0f);
if (msg.table_data.has_value()) {
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.9f, 1.0f), ICON_MD_TABLE_CHART " [Table Data]");
} else if (msg.json_pretty.has_value()) {
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.9f, 1.0f), ICON_MD_DATA_OBJECT " [Structured Response]");
} else {
// Truncate long messages
std::string content = msg.message;
if (content.length() > 200) {
content = content.substr(0, 197) + "...";
}
ImGui::TextWrapped("%s", content.c_str());
}
// Show proposal indicator if present
if (msg.proposal.has_value()) {
ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.4f, 1.0f),
ICON_MD_PREVIEW " Proposal: %s", msg.proposal->id.c_str());
}
ImGui::Unindent(10.0f);
ImGui::Spacing();
ImGui::Separator();
ImGui::PopID();
}
void AgentChatHistoryPopup::DrawHeader() {
// Theme-matched header with subtle gradient
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 header_start = ImGui::GetCursorScreenPos();
ImVec2 header_size(ImGui::GetContentRegionAvail().x, 55);
// Subtle gradient matching theme
ImU32 color_top = ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
ImU32 color_bottom = ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_ChildBg));
draw_list->AddRectFilledMultiColor(
header_start,
ImVec2(header_start.x + header_size.x, header_start.y + header_size.y),
color_top, color_top, color_bottom, color_bottom);
// Thin accent line (no pulse - matches theme better)
ImU32 accent_color = ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Separator));
draw_list->AddLine(
ImVec2(header_start.x, header_start.y + header_size.y),
ImVec2(header_start.x + header_size.x, header_start.y + header_size.y),
accent_color, 1.5f);
ImGui::Dummy(ImVec2(0, 8));
// Title and provider dropdown (like connection header)
ImGui::Text(ICON_MD_CHAT);
ImGui::SameLine();
// Model dropdown (compact)
ImGui::SetNextItemWidth(120);
static int provider_idx = 0;
const char* providers[] = {"Mock", "Ollama", "Gemini"};
ImGui::Combo("##popup_provider", &provider_idx, providers, 3);
// Buttons properly spaced from right edge
ImGui::SameLine(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 75.0f);
// Compact mode toggle
if (ImGui::SmallButton(compact_mode_ ? ICON_MD_UNFOLD_MORE : ICON_MD_UNFOLD_LESS)) {
compact_mode_ = !compact_mode_;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(compact_mode_ ? "Expand view" : "Compact view");
}
ImGui::SameLine();
// Full chat button (closes popup when opened)
if (ImGui::SmallButton(ICON_MD_OPEN_IN_NEW)) {
if (open_chat_callback_) {
open_chat_callback_();
visible_ = false; // Close popup when opening main chat
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Open full chat");
}
ImGui::SameLine();
// Close button
if (ImGui::SmallButton(ICON_MD_CLOSE)) {
visible_ = false;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Close (Ctrl+H)");
}
// Message count with badge
int visible_count = 0;
for (const auto& msg : messages_) {
if (msg.is_internal) continue;
if (message_filter_ == MessageFilter::kUserOnly &&
msg.sender != cli::agent::ChatMessage::Sender::kUser) continue;
if (message_filter_ == MessageFilter::kAgentOnly &&
msg.sender != cli::agent::ChatMessage::Sender::kAgent) continue;
visible_count++;
}
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f),
"%d message%s", visible_count, visible_count == 1 ? "" : "s");
ImGui::Dummy(ImVec2(0, 5));
}
void AgentChatHistoryPopup::DrawQuickActions() {
// 4 buttons with narrower width
float button_width = (ImGui::GetContentRegionAvail().x - 15) / 4.0f;
// Multimodal snapshot button
if (ImGui::Button(ICON_MD_CAMERA, ImVec2(button_width, 30))) {
if (capture_snapshot_callback_) {
capture_snapshot_callback_();
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Capture screenshot");
}
ImGui::SameLine();
// Filter button with icon indicator
const char* filter_icons[] = {ICON_MD_FILTER_LIST, ICON_MD_PERSON, ICON_MD_SMART_TOY};
int filter_idx = static_cast<int>(message_filter_);
if (ImGui::Button(filter_icons[filter_idx], ImVec2(button_width, 30))) {
ImGui::OpenPopup("FilterPopup");
}
if (ImGui::IsItemHovered()) {
const char* filter_names[] = {"All", "User only", "Agent only"};
ImGui::SetTooltip("Filter: %s", filter_names[filter_idx]);
}
// Filter popup
if (ImGui::BeginPopup("FilterPopup")) {
if (ImGui::Selectable(ICON_MD_FILTER_LIST " All Messages", message_filter_ == MessageFilter::kAll)) {
message_filter_ = MessageFilter::kAll;
}
if (ImGui::Selectable(ICON_MD_PERSON " User Only", message_filter_ == MessageFilter::kUserOnly)) {
message_filter_ = MessageFilter::kUserOnly;
}
if (ImGui::Selectable(ICON_MD_SMART_TOY " Agent Only", message_filter_ == MessageFilter::kAgentOnly)) {
message_filter_ = MessageFilter::kAgentOnly;
}
ImGui::EndPopup();
}
ImGui::SameLine();
// Save session button
if (ImGui::Button(ICON_MD_SAVE, ImVec2(button_width, 30))) {
if (toast_manager_) {
toast_manager_->Show(ICON_MD_SAVE " Session auto-saved", ToastType::kSuccess, 1.5f);
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Save chat session");
}
ImGui::SameLine();
// Clear button
if (ImGui::Button(ICON_MD_DELETE, ImVec2(button_width, 30))) {
ClearHistory();
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Clear popup view");
}
}
void AgentChatHistoryPopup::DrawInputSection() {
ImGui::Separator();
ImGui::Spacing();
// Input field using theme colors
bool send_message = false;
if (ImGui::InputTextMultiline("##popup_input", input_buffer_, sizeof(input_buffer_),
ImVec2(-1, 60),
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CtrlEnterForNewLine)) {
send_message = true;
}
// Focus input on first show
if (focus_input_) {
ImGui::SetKeyboardFocusHere(-1);
focus_input_ = false;
}
// Send button (proper width)
ImGui::Spacing();
float send_button_width = ImGui::GetContentRegionAvail().x;
if (ImGui::Button(absl::StrFormat("%s Send", ICON_MD_SEND).c_str(), ImVec2(send_button_width, 32)) || send_message) {
if (std::strlen(input_buffer_) > 0) {
SendMessage(input_buffer_);
std::memset(input_buffer_, 0, sizeof(input_buffer_));
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Send message (Enter) • Ctrl+Enter for newline");
}
// Info text
ImGui::Spacing();
ImGui::TextDisabled(ICON_MD_INFO " Enter: send • Ctrl+Enter: newline");
}
void AgentChatHistoryPopup::SendMessage(const std::string& message) {
if (send_message_callback_) {
send_message_callback_(message);
if (toast_manager_) {
toast_manager_->Show(ICON_MD_SEND " Message sent", ToastType::kSuccess, 1.5f);
}
// Auto-scroll to see response
needs_scroll_ = true;
}
}
void AgentChatHistoryPopup::UpdateHistory(const std::vector<cli::agent::ChatMessage>& history) {
bool had_messages = !messages_.empty();
int old_size = messages_.size();
messages_ = history;
// Auto-scroll if new messages arrived
if (auto_scroll_ && messages_.size() > old_size) {
needs_scroll_ = true;
}
}
void AgentChatHistoryPopup::NotifyNewMessage() {
if (auto_scroll_) {
needs_scroll_ = true;
}
// Flash the window to draw attention
if (toast_manager_ && !visible_) {
toast_manager_->Show(ICON_MD_CHAT " New message received", ToastType::kInfo, 2.0f);
}
}
void AgentChatHistoryPopup::ClearHistory() {
messages_.clear();
if (toast_manager_) {
toast_manager_->Show("Chat history popup cleared", ToastType::kInfo, 2.0f);
}
}
void AgentChatHistoryPopup::ExportHistory() {
// TODO: Implement export functionality
if (toast_manager_) {
toast_manager_->Show("Export feature coming soon", ToastType::kInfo, 2.0f);
}
}
void AgentChatHistoryPopup::ScrollToBottom() {
needs_scroll_ = true;
}
} // namespace editor
} // namespace yaze

View File

@@ -1,125 +0,0 @@
#ifndef YAZE_APP_EDITOR_SYSTEM_AGENT_CHAT_HISTORY_POPUP_H
#define YAZE_APP_EDITOR_SYSTEM_AGENT_CHAT_HISTORY_POPUP_H
#include <string>
#include <vector>
#include "cli/service/agent/conversational_agent_service.h"
namespace yaze {
namespace editor {
class ToastManager;
/**
* @class AgentChatHistoryPopup
* @brief ImGui popup drawer for displaying chat history on the left side
*
* Provides a quick-access sidebar for viewing recent chat messages,
* complementing the ProposalDrawer on the right. Features:
* - Recent message list with timestamps
* - User/Agent message differentiation
* - Scroll to view older messages
* - Quick actions (clear, export, open full chat)
* - Syncs with AgentChatWidget and AgentEditor
*
* Positioned on the LEFT side of the screen as a slide-out panel.
*/
class AgentChatHistoryPopup {
public:
AgentChatHistoryPopup();
~AgentChatHistoryPopup() = default;
// Set dependencies
void SetToastManager(ToastManager* toast_manager) {
toast_manager_ = toast_manager;
}
// Render the popup UI
void Draw();
// Show/hide the popup
void Show() { visible_ = true; }
void Hide() { visible_ = false; }
void Toggle() { visible_ = !visible_; }
bool IsVisible() const { return visible_; }
// Update history from service
void UpdateHistory(const std::vector<cli::agent::ChatMessage>& history);
// Notify of new message (triggers auto-scroll)
void NotifyNewMessage();
// Set callback for opening full chat window
using OpenChatCallback = std::function<void()>;
void SetOpenChatCallback(OpenChatCallback callback) {
open_chat_callback_ = std::move(callback);
}
// Set callback for sending messages
using SendMessageCallback = std::function<void(const std::string&)>;
void SetSendMessageCallback(SendMessageCallback callback) {
send_message_callback_ = std::move(callback);
}
// Set callback for capturing snapshots
using CaptureSnapshotCallback = std::function<void()>;
void SetCaptureSnapshotCallback(CaptureSnapshotCallback callback) {
capture_snapshot_callback_ = std::move(callback);
}
private:
void DrawHeader();
void DrawMessageList();
void DrawMessage(const cli::agent::ChatMessage& msg, int index);
void DrawInputSection();
void DrawQuickActions();
void SendMessage(const std::string& message);
void ClearHistory();
void ExportHistory();
void ScrollToBottom();
bool visible_ = false;
bool needs_scroll_ = false;
bool auto_scroll_ = true;
bool compact_mode_ = true;
bool show_quick_actions_ = true;
// History state
std::vector<cli::agent::ChatMessage> messages_;
int display_limit_ = 50; // Show last 50 messages
// Input state
char input_buffer_[512] = {};
bool focus_input_ = false;
// UI state
float drawer_width_ = 420.0f;
float min_drawer_width_ = 300.0f;
float max_drawer_width_ = 700.0f;
bool is_resizing_ = false;
// Filter state
enum class MessageFilter {
kAll,
kUserOnly,
kAgentOnly
};
MessageFilter message_filter_ = MessageFilter::kAll;
// Visual state
float header_pulse_ = 0.0f;
int unread_count_ = 0;
// Dependencies
ToastManager* toast_manager_ = nullptr;
OpenChatCallback open_chat_callback_;
SendMessageCallback send_message_callback_;
CaptureSnapshotCallback capture_snapshot_callback_;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_SYSTEM_AGENT_CHAT_HISTORY_POPUP_H