feat: Add Collaboration Panel for enhanced team features

- Introduced `CollaborationPanel` class to manage ROM synchronization history, shared snapshots, and AI-generated proposals.
- Implemented UI components for displaying and interacting with collaboration data, including filtering and status indicators.
- Updated `README.md` to document the new collaboration features and their functionalities.
- Added necessary header and implementation files for the collaboration panel functionality.
This commit is contained in:
scawful
2025-10-04 22:25:13 -04:00
parent 13bbe8078a
commit 253f36542f
3 changed files with 616 additions and 1 deletions

View File

@@ -894,7 +894,8 @@ The AI response appears in your chat history and can reference specific details
- **Native Gemini Function Calling**: Upgraded from manual curl to native function calling API with automatic tool schema generation
- **Multimodal Vision Testing**: Comprehensive test suite for Gemini vision capabilities with screenshot integration
- **AI-Controlled GUI Automation**: Natural language parsing (`AIActionParser`) and test script generation (`GuiActionGenerator`) for automated tile placement
- **gRPC Windows Build Optimization**: vcpkg integration for 10-20x faster Windows builds, removed abseil-cpp submodule
- **gRPC Windows Build Optimization**: vcpkg integration for 10-20x faster Windows builds, removed abseil-cpp submodule
- **Collaboration UI Panel**: New `CollaborationPanel` widget with ROM sync history, snapshot gallery, and proposal management
- **Improved Documentation**: Consolidated architecture, enhancement plans, and build instructions with JSON-first approach
## 12. Troubleshooting

View File

@@ -0,0 +1,445 @@
#include "app/gui/widgets/collaboration_panel.h"
#include <algorithm>
#include <ctime>
#include <iomanip>
#include <sstream>
#include "imgui/imgui.h"
namespace yaze {
namespace app {
namespace gui {
CollaborationPanel::CollaborationPanel()
: selected_tab_(0),
selected_rom_sync_(-1),
selected_snapshot_(-1),
selected_proposal_(-1),
show_sync_details_(false),
show_snapshot_preview_(true),
auto_scroll_(true),
filter_pending_only_(false) {
// Initialize search filter
search_filter_[0] = '\0';
// Initialize colors
colors_.sync_applied = ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
colors_.sync_pending = ImVec4(0.8f, 0.8f, 0.2f, 1.0f);
colors_.sync_error = ImVec4(0.8f, 0.2f, 0.2f, 1.0f);
colors_.proposal_pending = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
colors_.proposal_approved = ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
colors_.proposal_rejected = ImVec4(0.8f, 0.3f, 0.3f, 1.0f);
colors_.proposal_applied = ImVec4(0.2f, 0.6f, 0.8f, 1.0f);
}
CollaborationPanel::~CollaborationPanel() {
// Cleanup any OpenGL textures
for (auto& snapshot : snapshots_) {
if (snapshot.texture_id) {
// Note: Actual texture cleanup would depend on your renderer
// This is a placeholder
snapshot.texture_id = nullptr;
}
}
}
void CollaborationPanel::Render(bool* p_open) {
if (!ImGui::Begin("Collaboration", p_open, ImGuiWindowFlags_None)) {
ImGui::End();
return;
}
// Tabs for different collaboration features
if (ImGui::BeginTabBar("CollaborationTabs")) {
if (ImGui::BeginTabItem("ROM Sync")) {
selected_tab_ = 0;
RenderRomSyncTab();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Snapshots")) {
selected_tab_ = 1;
RenderSnapshotsTab();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Proposals")) {
selected_tab_ = 2;
RenderProposalsTab();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
}
void CollaborationPanel::RenderRomSyncTab() {
ImGui::TextWrapped("ROM Synchronization History");
ImGui::Separator();
// Toolbar
if (ImGui::Button("Clear History")) {
rom_syncs_.clear();
}
ImGui::SameLine();
ImGui::Checkbox("Auto-scroll", &auto_scroll_);
ImGui::SameLine();
ImGui::Checkbox("Show Details", &show_sync_details_);
ImGui::Separator();
// Stats
int applied_count = 0;
int pending_count = 0;
int error_count = 0;
for (const auto& sync : rom_syncs_) {
if (sync.applied) applied_count++;
else if (!sync.error_message.empty()) error_count++;
else pending_count++;
}
ImGui::Text("Total: %zu | ", rom_syncs_.size());
ImGui::SameLine();
ImGui::TextColored(colors_.sync_applied, "Applied: %d", applied_count);
ImGui::SameLine();
ImGui::TextColored(colors_.sync_pending, "Pending: %d", pending_count);
ImGui::SameLine();
ImGui::TextColored(colors_.sync_error, "Errors: %d", error_count);
ImGui::Separator();
// Sync list
if (ImGui::BeginChild("SyncList", ImVec2(0, 0), true)) {
for (size_t i = 0; i < rom_syncs_.size(); ++i) {
RenderRomSyncEntry(rom_syncs_[i], i);
}
if (auto_scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
ImGui::SetScrollHereY(1.0f);
}
}
ImGui::EndChild();
}
void CollaborationPanel::RenderSnapshotsTab() {
ImGui::TextWrapped("Shared Snapshots Gallery");
ImGui::Separator();
// Toolbar
if (ImGui::Button("Clear Gallery")) {
snapshots_.clear();
}
ImGui::SameLine();
ImGui::Checkbox("Show Preview", &show_snapshot_preview_);
ImGui::SameLine();
ImGui::InputText("Search", search_filter_, sizeof(search_filter_));
ImGui::Separator();
// Snapshot grid
if (ImGui::BeginChild("SnapshotGrid", ImVec2(0, 0), true)) {
float thumbnail_size = 150.0f;
float padding = 10.0f;
float cell_size = thumbnail_size + padding;
int columns = std::max(1, (int)((ImGui::GetContentRegionAvail().x) / cell_size));
for (size_t i = 0; i < snapshots_.size(); ++i) {
// Filter by search
if (search_filter_[0] != '\0') {
std::string search_lower = search_filter_;
std::string sender_lower = snapshots_[i].sender;
std::transform(search_lower.begin(), search_lower.end(), search_lower.begin(), ::tolower);
std::transform(sender_lower.begin(), sender_lower.end(), sender_lower.begin(), ::tolower);
if (sender_lower.find(search_lower) == std::string::npos &&
snapshots_[i].snapshot_type.find(search_lower) == std::string::npos) {
continue;
}
}
RenderSnapshotEntry(snapshots_[i], i);
// Grid layout
if ((i + 1) % columns != 0 && i < snapshots_.size() - 1) {
ImGui::SameLine();
}
}
}
ImGui::EndChild();
}
void CollaborationPanel::RenderProposalsTab() {
ImGui::TextWrapped("AI Proposals & Suggestions");
ImGui::Separator();
// Toolbar
if (ImGui::Button("Clear All")) {
proposals_.clear();
}
ImGui::SameLine();
ImGui::Checkbox("Pending Only", &filter_pending_only_);
ImGui::SameLine();
ImGui::InputText("Search", search_filter_, sizeof(search_filter_));
ImGui::Separator();
// Stats
int pending = 0, approved = 0, rejected = 0, applied = 0;
for (const auto& proposal : proposals_) {
if (proposal.status == "pending") pending++;
else if (proposal.status == "approved") approved++;
else if (proposal.status == "rejected") rejected++;
else if (proposal.status == "applied") applied++;
}
ImGui::Text("Total: %zu", proposals_.size());
ImGui::SameLine();
ImGui::TextColored(colors_.proposal_pending, " | Pending: %d", pending);
ImGui::SameLine();
ImGui::TextColored(colors_.proposal_approved, " | Approved: %d", approved);
ImGui::SameLine();
ImGui::TextColored(colors_.proposal_rejected, " | Rejected: %d", rejected);
ImGui::SameLine();
ImGui::TextColored(colors_.proposal_applied, " | Applied: %d", applied);
ImGui::Separator();
// Proposal list
if (ImGui::BeginChild("ProposalList", ImVec2(0, 0), true)) {
for (size_t i = 0; i < proposals_.size(); ++i) {
// Filter
if (filter_pending_only_ && proposals_[i].status != "pending") {
continue;
}
if (search_filter_[0] != '\0') {
std::string search_lower = search_filter_;
std::string sender_lower = proposals_[i].sender;
std::string desc_lower = proposals_[i].description;
std::transform(search_lower.begin(), search_lower.end(), search_lower.begin(), ::tolower);
std::transform(sender_lower.begin(), sender_lower.end(), sender_lower.begin(), ::tolower);
std::transform(desc_lower.begin(), desc_lower.end(), desc_lower.begin(), ::tolower);
if (sender_lower.find(search_lower) == std::string::npos &&
desc_lower.find(search_lower) == std::string::npos) {
continue;
}
}
RenderProposalEntry(proposals_[i], i);
}
}
ImGui::EndChild();
}
void CollaborationPanel::RenderRomSyncEntry(const RomSyncEntry& entry, int index) {
ImGui::PushID(index);
// Status indicator
ImVec4 status_color;
const char* status_icon;
if (entry.applied) {
status_color = colors_.sync_applied;
status_icon = "[✓]";
} else if (!entry.error_message.empty()) {
status_color = colors_.sync_error;
status_icon = "[✗]";
} else {
status_color = colors_.sync_pending;
status_icon = "[◷]";
}
ImGui::TextColored(status_color, "%s", status_icon);
ImGui::SameLine();
// Entry info
ImGui::Text("%s - %s (%s)",
FormatTimestamp(entry.timestamp).c_str(),
entry.sender.c_str(),
FormatFileSize(entry.diff_size).c_str());
// Details on hover or if enabled
if (show_sync_details_ || ImGui::IsItemHovered()) {
ImGui::Indent();
ImGui::TextWrapped("ROM Hash: %s", entry.rom_hash.substr(0, 16).c_str());
if (!entry.error_message.empty()) {
ImGui::TextColored(colors_.sync_error, "Error: %s", entry.error_message.c_str());
}
ImGui::Unindent();
}
ImGui::Separator();
ImGui::PopID();
}
void CollaborationPanel::RenderSnapshotEntry(const SnapshotEntry& entry, int index) {
ImGui::PushID(index);
ImGui::BeginGroup();
// Thumbnail placeholder or actual image
if (show_snapshot_preview_ && entry.is_image && entry.texture_id) {
ImGui::Image(entry.texture_id, ImVec2(150, 150));
} else {
// Placeholder
ImGui::BeginChild("SnapshotPlaceholder", ImVec2(150, 150), true);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 50);
ImGui::TextWrapped("%s", entry.snapshot_type.c_str());
ImGui::EndChild();
}
// Info
ImGui::TextWrapped("%s", entry.sender.c_str());
ImGui::Text("%s", FormatTimestamp(entry.timestamp).c_str());
ImGui::Text("%s", FormatFileSize(entry.data_size).c_str());
// Actions
if (ImGui::SmallButton("View")) {
selected_snapshot_ = index;
// TODO: Open snapshot viewer
}
ImGui::SameLine();
if (ImGui::SmallButton("Export")) {
// TODO: Export snapshot to file
}
ImGui::EndGroup();
ImGui::PopID();
}
void CollaborationPanel::RenderProposalEntry(const ProposalEntry& entry, int index) {
ImGui::PushID(index);
// Status icon and color
const char* icon = GetProposalStatusIcon(entry.status);
ImVec4 color = GetProposalStatusColor(entry.status);
ImGui::TextColored(color, "%s", icon);
ImGui::SameLine();
// Collapsible header
bool is_open = ImGui::TreeNode(entry.description.c_str());
if (is_open) {
ImGui::Indent();
ImGui::Text("From: %s", entry.sender.c_str());
ImGui::Text("Time: %s", FormatTimestamp(entry.timestamp).c_str());
ImGui::Text("Status: %s", entry.status.c_str());
ImGui::Separator();
// Proposal data
ImGui::TextWrapped("%s", entry.proposal_data.c_str());
// Actions for pending proposals
if (entry.status == "pending") {
ImGui::Separator();
if (ImGui::Button("✓ Approve")) {
// TODO: Send approval to server
}
ImGui::SameLine();
if (ImGui::Button("✗ Reject")) {
// TODO: Send rejection to server
}
ImGui::SameLine();
if (ImGui::Button("▶ Apply Now")) {
// TODO: Execute proposal
}
}
ImGui::Unindent();
ImGui::TreePop();
}
ImGui::Separator();
ImGui::PopID();
}
void CollaborationPanel::AddRomSync(const RomSyncEntry& entry) {
rom_syncs_.push_back(entry);
}
void CollaborationPanel::AddSnapshot(const SnapshotEntry& entry) {
snapshots_.push_back(entry);
}
void CollaborationPanel::AddProposal(const ProposalEntry& entry) {
proposals_.push_back(entry);
}
void CollaborationPanel::UpdateProposalStatus(const std::string& proposal_id, const std::string& status) {
for (auto& proposal : proposals_) {
if (proposal.proposal_id == proposal_id) {
proposal.status = status;
break;
}
}
}
void CollaborationPanel::Clear() {
rom_syncs_.clear();
snapshots_.clear();
proposals_.clear();
}
ProposalEntry* CollaborationPanel::GetProposal(const std::string& proposal_id) {
for (auto& proposal : proposals_) {
if (proposal.proposal_id == proposal_id) {
return &proposal;
}
}
return nullptr;
}
std::string CollaborationPanel::FormatTimestamp(int64_t timestamp) {
std::time_t time = timestamp / 1000; // Convert ms to seconds
std::tm* tm = std::localtime(&time);
char buffer[32];
std::strftime(buffer, sizeof(buffer), "%H:%M:%S", tm);
return std::string(buffer);
}
std::string CollaborationPanel::FormatFileSize(size_t bytes) {
const char* units[] = {"B", "KB", "MB", "GB"};
int unit_index = 0;
double size = static_cast<double>(bytes);
while (size >= 1024.0 && unit_index < 3) {
size /= 1024.0;
unit_index++;
}
char buffer[32];
snprintf(buffer, sizeof(buffer), "%.1f %s", size, units[unit_index]);
return std::string(buffer);
}
const char* CollaborationPanel::GetProposalStatusIcon(const std::string& status) {
if (status == "pending") return "[◷]";
if (status == "approved") return "[✓]";
if (status == "rejected") return "[✗]";
if (status == "applied") return "[✦]";
return "[?]";
}
ImVec4 CollaborationPanel::GetProposalStatusColor(const std::string& status) {
if (status == "pending") return colors_.proposal_pending;
if (status == "approved") return colors_.proposal_approved;
if (status == "rejected") return colors_.proposal_rejected;
if (status == "applied") return colors_.proposal_applied;
return ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
}
} // namespace gui
} // namespace app
} // namespace yaze

View File

@@ -0,0 +1,169 @@
#ifndef YAZE_APP_GUI_WIDGETS_COLLABORATION_PANEL_H_
#define YAZE_APP_GUI_WIDGETS_COLLABORATION_PANEL_H_
#include <map>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "imgui/imgui.h"
#ifdef YAZE_WITH_JSON
#include "nlohmann/json.hpp"
#endif
namespace yaze {
namespace app {
namespace gui {
/**
* @struct RomSyncEntry
* @brief Represents a ROM synchronization event
*/
struct RomSyncEntry {
std::string sync_id;
std::string sender;
std::string rom_hash;
int64_t timestamp;
size_t diff_size;
bool applied;
std::string error_message;
};
/**
* @struct SnapshotEntry
* @brief Represents a shared snapshot (image, map state, etc.)
*/
struct SnapshotEntry {
std::string snapshot_id;
std::string sender;
std::string snapshot_type; // "screenshot", "map_state", "tile_data", etc.
int64_t timestamp;
size_t data_size;
std::vector<uint8_t> data; // Base64-decoded image or JSON data
bool is_image;
// For images: decoded texture data
void* texture_id = nullptr;
int width = 0;
int height = 0;
};
/**
* @struct ProposalEntry
* @brief Represents an AI-generated proposal
*/
struct ProposalEntry {
std::string proposal_id;
std::string sender;
std::string description;
std::string proposal_data; // JSON or formatted text
std::string status; // "pending", "approved", "rejected", "applied"
int64_t timestamp;
#ifdef YAZE_WITH_JSON
nlohmann::json metadata;
#endif
};
/**
* @class CollaborationPanel
* @brief ImGui panel for collaboration features
*
* Displays:
* - ROM sync history and status
* - Shared snapshots gallery
* - Proposal management and voting
*/
class CollaborationPanel {
public:
CollaborationPanel();
~CollaborationPanel();
/**
* Render the collaboration panel
*/
void Render(bool* p_open = nullptr);
/**
* Add a ROM sync event
*/
void AddRomSync(const RomSyncEntry& entry);
/**
* Add a snapshot
*/
void AddSnapshot(const SnapshotEntry& entry);
/**
* Add a proposal
*/
void AddProposal(const ProposalEntry& entry);
/**
* Update proposal status
*/
void UpdateProposalStatus(const std::string& proposal_id, const std::string& status);
/**
* Clear all collaboration data
*/
void Clear();
/**
* Get proposal by ID
*/
ProposalEntry* GetProposal(const std::string& proposal_id);
private:
void RenderRomSyncTab();
void RenderSnapshotsTab();
void RenderProposalsTab();
void RenderRomSyncEntry(const RomSyncEntry& entry, int index);
void RenderSnapshotEntry(const SnapshotEntry& entry, int index);
void RenderProposalEntry(const ProposalEntry& entry, int index);
// Tab selection
int selected_tab_;
// Data
std::vector<RomSyncEntry> rom_syncs_;
std::vector<SnapshotEntry> snapshots_;
std::vector<ProposalEntry> proposals_;
// UI state
int selected_rom_sync_;
int selected_snapshot_;
int selected_proposal_;
bool show_sync_details_;
bool show_snapshot_preview_;
bool auto_scroll_;
// Filters
char search_filter_[256];
bool filter_pending_only_;
// Colors
struct {
ImVec4 sync_applied;
ImVec4 sync_pending;
ImVec4 sync_error;
ImVec4 proposal_pending;
ImVec4 proposal_approved;
ImVec4 proposal_rejected;
ImVec4 proposal_applied;
} colors_;
// Helper functions
std::string FormatTimestamp(int64_t timestamp);
std::string FormatFileSize(size_t bytes);
const char* GetProposalStatusIcon(const std::string& status);
ImVec4 GetProposalStatusColor(const std::string& status);
};
} // namespace gui
} // namespace app
} // namespace yaze
#endif // YAZE_APP_GUI_WIDGETS_COLLABORATION_PANEL_H_