Files
afs/apps/studio/src/ui/components/panels.cc
2025-12-30 11:24:15 -05:00

1602 lines
68 KiB
C++

#include "panels.h"
#include "../core.h"
#include "../../icons.h"
#include "core/filesystem.h"
#include <implot.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iomanip>
#include <sstream>
#include <unordered_map>
#include <vector>
#include <GLFW/glfw3.h>
namespace afs {
namespace viz {
std::filesystem::path ResolveHafsScawfulRoot() {
const char* env_root = std::getenv("AFS_SCAWFUL_ROOT");
if (env_root && env_root[0] != '\0') {
auto path = studio::core::FileSystem::ResolvePath(env_root);
if (studio::core::FileSystem::Exists(path)) {
return path;
}
}
auto plugin_path = studio::core::FileSystem::ResolvePath("~/.config/afs/plugins/afs_scawful");
if (studio::core::FileSystem::Exists(plugin_path)) {
return plugin_path;
}
const char* trunk_env = std::getenv("TRUNK_ROOT");
if (trunk_env && trunk_env[0] != '\0') {
auto trunk_root = studio::core::FileSystem::ResolvePath(trunk_env);
auto candidate = trunk_root / "scawful" / "research" / "afs_scawful";
if (studio::core::FileSystem::Exists(candidate)) {
return candidate;
}
}
auto fallback_path = studio::core::FileSystem::ResolvePath("~/src/trunk/scawful/research/afs_scawful");
if (studio::core::FileSystem::Exists(fallback_path)) {
return fallback_path;
}
return {};
}
namespace ui {
static bool ContainsInsensitive(const std::string& str, const std::string& pattern) {
if (pattern.empty()) return true;
auto it = std::search(str.begin(), str.end(), pattern.begin(), pattern.end(),
[](char ch1, char ch2) {
return std::tolower(static_cast<unsigned char>(ch1)) ==
std::tolower(static_cast<unsigned char>(ch2));
});
return it != str.end();
}
static std::string TrimCopy(const std::string& input) {
auto start = input.find_first_not_of(" \t\r\n");
if (start == std::string::npos) return "";
auto end = input.find_last_not_of(" \t\r\n");
return input.substr(start, end - start + 1);
}
static std::string UnquoteCopy(const std::string& input) {
if (input.size() >= 2 && input.front() == '"' && input.back() == '"') {
return input.substr(1, input.size() - 2);
}
return input;
}
struct HackOverrideEdit {
bool initialized = false;
bool enabled = false;
bool dirty = false;
std::string review_status;
float weight = 1.0f;
bool weight_set = false;
bool notes_set = false;
bool include_set = false;
bool exclude_set = false;
std::string notes;
std::vector<std::string> include_globs;
std::vector<std::string> exclude_globs;
};
struct NoteBufferState {
std::array<char, 512> buffer{};
bool initialized = false;
};
static bool LoadOverrideFile(const std::filesystem::path& path,
std::unordered_map<std::string, HackOverrideEdit>* edits,
std::string* error) {
edits->clear();
if (error) error->clear();
if (!studio::core::FileSystem::Exists(path)) {
if (error) *error = "Overrides file not found";
return false;
}
auto content_opt = studio::core::FileSystem::ReadFile(path);
if (!content_opt) {
if (error) *error = "Failed to read overrides file";
return false;
}
std::istringstream stream(*content_opt);
std::string line;
HackOverrideEdit current;
std::string current_name;
auto flush = [&]() {
if (current_name.empty()) return;
current.initialized = true;
current.enabled = true;
(*edits)[current_name] = current;
};
while (std::getline(stream, line)) {
std::string trimmed = TrimCopy(line);
if (trimmed.empty() || trimmed[0] == '#') continue;
if (trimmed == "[[hack]]") {
flush();
current = HackOverrideEdit{};
current_name.clear();
continue;
}
auto sep = trimmed.find('=');
if (sep == std::string::npos) continue;
std::string key = TrimCopy(trimmed.substr(0, sep));
std::string value = TrimCopy(trimmed.substr(sep + 1));
if (key == "name") {
current_name = UnquoteCopy(value);
} else if (key == "review_status") {
current.review_status = UnquoteCopy(value);
} else if (key == "notes") {
current.notes = UnquoteCopy(value);
current.notes_set = true;
} else if (key == "weight") {
try {
current.weight = std::stof(value);
current.weight_set = true;
} catch (...) {
// Ignore parse errors
}
} else if (key == "include_globs" || key == "exclude_globs") {
std::vector<std::string> items;
if (!value.empty() && value.front() == '[' && value.back() == ']') {
std::string inner = value.substr(1, value.size() - 2);
std::stringstream list_stream(inner);
std::string token;
while (std::getline(list_stream, token, ',')) {
std::string cleaned = UnquoteCopy(TrimCopy(token));
if (!cleaned.empty()) items.push_back(cleaned);
}
}
if (key == "include_globs") {
current.include_globs = std::move(items);
current.include_set = true;
} else {
current.exclude_globs = std::move(items);
current.exclude_set = true;
}
}
}
flush();
return true;
}
template<size_t N>
static void FillBuffer(std::array<char, N>& buffer, const std::string& text) {
buffer.fill('\0');
size_t copy_len = std::min(buffer.size() - 1, text.size());
std::memcpy(buffer.data(), text.data(), copy_len);
buffer[copy_len] = '\0';
}
static std::string JoinLines(const std::vector<std::string>& lines) {
std::ostringstream out;
for (size_t i = 0; i < lines.size(); ++i) {
out << lines[i];
if (i + 1 < lines.size()) out << "\n";
}
return out.str();
}
static std::vector<std::string> SplitLines(const std::string& input) {
std::vector<std::string> lines;
std::istringstream stream(input);
std::string line;
while (std::getline(stream, line)) {
std::string trimmed = TrimCopy(line);
if (!trimmed.empty()) lines.push_back(trimmed);
}
return lines;
}
static std::string EscapeTomlString(const std::string& value) {
std::string out;
out.reserve(value.size());
for (char ch : value) {
if (ch == '"') out += "\\\"";
else out += ch;
}
return out;
}
static void WriteStringArray(std::ostringstream& output, const std::string& key,
const std::vector<std::string>& values) {
output << key << " = [";
for (size_t i = 0; i < values.size(); ++i) {
output << "\"" << EscapeTomlString(values[i]) << "\"";
if (i + 1 < values.size()) output << ", ";
}
output << "]\n";
}
static bool RunPythonScript(const std::filesystem::path& script_path,
const std::filesystem::path& module_root,
std::string* output) {
if (output) output->clear();
if (!studio::core::FileSystem::Exists(script_path)) {
if (output) *output = "Script not found";
return false;
}
std::string cmd;
if (!module_root.empty()) {
cmd = "AFS_SCAWFUL_ROOT=\"" + module_root.string() + "\" ";
cmd += "PYTHONPATH=\"" + module_root.string() + "\" ";
}
cmd += "python3 \"" + script_path.string() + "\" 2>&1";
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) {
if (output) *output = "Failed to launch script";
return false;
}
char buffer[256];
std::ostringstream result;
while (fgets(buffer, sizeof(buffer), pipe)) {
result << buffer;
}
int status = pclose(pipe);
if (output) *output = result.str();
return status == 0;
}
void RenderInspectorPanel(AppState& state, const DataLoader& loader, ImFont* font_header, const std::string& data_path) {
const auto& trends = loader.GetQualityTrends();
const auto& coverage = loader.GetCoverage();
const auto& runs = loader.GetTrainingRuns();
const auto& generators = loader.GetGeneratorStats();
if (font_header) ImGui::PushFont(font_header);
ImGui::Text(ICON_MD_INSIGHTS " INSPECTOR");
if (font_header) ImGui::PopFont();
ImGui::Separator();
if (state.inspector_context != PlotKind::None) {
ImGui::PushStyleColor(ImGuiCol_Text, GetThemeColor(ImGuiCol_PlotLines, state.current_theme));
ImGui::Text(ICON_MD_SETTINGS " CHART PROPERTIES");
ImGui::PopStyleColor();
const auto& options = PlotOptions();
const char* label = "Unknown Plot";
for (const auto& opt : options) {
if (opt.kind == state.inspector_context) { label = opt.label; break; }
}
ImGui::Text("Subject: %s", label);
ImGui::Separator();
ImGui::Checkbox("Show Markers", &state.show_plot_markers);
ImGui::Checkbox("Show Legend", &state.show_plot_legends);
ImGui::SliderFloat("Line Weight", &state.line_weight, 1.0f, 5.0f);
if (state.inspector_context == PlotKind::QualityTrends) {
ImGui::Spacing();
ImGui::TextDisabled("Domain Filter");
for (auto& [domain, visible] : state.domain_visibility) {
ImGui::Checkbox(domain.c_str(), &visible);
}
}
if (ImGui::Button("Reset Context")) state.inspector_context = PlotKind::None;
ImGui::Separator();
}
ImGui::TextDisabled("Data Snapshot");
ImGui::Text("Runs: %zu", runs.size());
ImGui::Text("Generators: %zu", generators.size());
ImGui::Text("Regions: %zu", loader.GetEmbeddingRegions().size());
ImGui::Text("Sparse Regions: %d", coverage.sparse_regions);
ImGui::Text("Data Path:");
ImGui::TextWrapped("%s", data_path.c_str());
const auto& status = loader.GetLastStatus();
ImGui::Text("Sources: %d/%d ok", status.OkCount(), status.FoundCount());
if (status.error_count > 0) {
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f),
"Load Errors: %d", status.error_count);
if (!status.last_error.empty()) {
ImGui::TextDisabled("Last Error:");
ImGui::TextWrapped("%s (%s)",
status.last_error.c_str(),
status.last_error_source.empty()
? "unknown"
: status.last_error_source.c_str());
}
}
float avg_quality = 0.0f;
if (!trends.empty()) {
for (const auto& t : trends) avg_quality += t.mean;
avg_quality /= static_cast<float>(trends.size());
}
ImGui::Spacing();
ImGui::TextDisabled("Health Signals");
ImGui::ProgressBar(avg_quality, ImVec2(-1, 0), "Avg Quality");
ImGui::ProgressBar(coverage.coverage_score, ImVec2(-1, 0), "Coverage Score");
ImGui::Separator();
ImGui::TextDisabled("Selected Run");
if (state.selected_run_index >= 0 && state.selected_run_index < static_cast<int>(runs.size())) {
const auto& run = runs[state.selected_run_index];
ImGui::Text("%s", run.run_id.c_str());
if (!run.model_name.empty()) ImGui::Text("Model: %s", run.model_name.c_str());
if (!run.base_model.empty()) ImGui::Text("Base: %s", run.base_model.c_str());
ImGui::Text("Samples: %d", run.samples_count);
ImGui::Text("Final Loss: %.5f", run.final_loss);
if (!run.start_time.empty() || !run.end_time.empty()) {
ImGui::Text("Window: %s -> %s", run.start_time.empty() ? "?" : run.start_time.c_str(), run.end_time.empty() ? "?" : run.end_time.c_str());
}
if (!run.dataset_path.empty()) {
ImGui::Text("Dataset:");
ImGui::TextWrapped("%s", run.dataset_path.c_str());
}
if (!run.notes.empty()) {
ImGui::Text("Notes:");
ImGui::TextWrapped("%s", run.notes.c_str());
}
if (!run.domain_distribution.empty()) {
std::vector<std::pair<std::string, int>> domains(run.domain_distribution.begin(), run.domain_distribution.end());
std::sort(domains.begin(), domains.end(), [](const auto& a, const auto& b) { return a.second > b.second; });
if (domains.size() > 6) domains.resize(6);
std::vector<const char*> labels;
std::vector<float> values;
std::vector<std::string> label_storage;
for (const auto& [domain, count] : domains) {
label_storage.push_back(domain);
values.push_back(static_cast<float>(count));
}
for (const auto& label : label_storage) labels.push_back(label.c_str());
ImPlotFlags plot_flags = ImPlotFlags_NoLegend | ImPlotFlags_NoMenus;
ApplyPremiumPlotStyles("##RunDomains", state);
if (ImPlot::BeginPlot("##RunDomains", ImVec2(-1, 140), plot_flags)) {
ImPlotAxisFlags axis_flags = static_cast<ImPlotAxisFlags>(GetPlotAxisFlags(state));
ImPlot::SetupAxes("Domain", "Samples", axis_flags, axis_flags);
ImPlot::SetupAxisTicks(ImAxis_X1, 0, static_cast<double>(labels.size() - 1), static_cast<int>(labels.size()), labels.data());
ImPlot::SetNextFillStyle(GetSeriesColor(2), 0.75f);
ImPlot::PlotBars("Samples", values.data(), static_cast<int>(values.size()), 0.6);
ImPlot::EndPlot();
}
ImPlot::PopStyleColor(2);
ImPlot::PopStyleVar(6);
}
if (!run.eval_metrics.empty()) {
std::vector<const char*> labels;
std::vector<float> values;
std::vector<std::string> label_storage;
for (const auto& [metric, value] : run.eval_metrics) {
label_storage.push_back(metric);
values.push_back(value);
}
for (const auto& label : label_storage) labels.push_back(label.c_str());
ImPlotFlags plot_flags = ImPlotFlags_NoLegend | ImPlotFlags_NoMenus;
ApplyPremiumPlotStyles("##RunMetrics", state);
if (ImPlot::BeginPlot("##RunMetrics", ImVec2(-1, 120), plot_flags)) {
ImPlotAxisFlags axis_flags = static_cast<ImPlotAxisFlags>(GetPlotAxisFlags(state));
ImPlot::SetupAxes("Metric", "Score", axis_flags, axis_flags);
ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, 1.0, ImPlotCond_Once);
ImPlot::SetupAxisTicks(ImAxis_X1, 0, static_cast<double>(labels.size() - 1), static_cast<int>(labels.size()), labels.data());
ImPlot::SetNextFillStyle(GetSeriesColor(4), 0.75f);
ImPlot::PlotBars("Score", values.data(), static_cast<int>(values.size()), 0.6);
ImPlot::EndPlot();
}
ImPlot::PopStyleColor(2);
ImPlot::PopStyleVar(6);
}
} else {
ImGui::TextDisabled("Select a training run in the Dataset panel.");
}
ImGui::Separator();
ImGui::TextDisabled("Selected Generator");
if (state.selected_generator_index >= 0 && state.selected_generator_index < static_cast<int>(generators.size())) {
const auto& gen = generators[state.selected_generator_index];
ImGui::Text("%s", gen.name.c_str());
ImGui::Text("Accepted: %d", gen.samples_accepted);
ImGui::Text("Rejected: %d", gen.samples_rejected);
ImGui::Text("Avg Quality: %.3f", gen.avg_quality);
ImGui::ProgressBar(gen.acceptance_rate, ImVec2(-1, 0), "Acceptance Rate");
if (!gen.rejection_reasons.empty()) {
std::vector<std::pair<std::string, int>> reasons(gen.rejection_reasons.begin(), gen.rejection_reasons.end());
std::sort(reasons.begin(), reasons.end(), [](const auto& a, const auto& b) { return a.second > b.second; });
if (reasons.size() > 6) reasons.resize(6);
std::vector<const char*> labels;
std::vector<float> values;
std::vector<std::string> label_storage;
for (const auto& [reason, count] : reasons) {
std::string formatted = reason;
std::replace(formatted.begin(), formatted.end(), '_', ' ');
label_storage.push_back(formatted);
values.push_back(static_cast<float>(count));
}
for (const auto& label : label_storage) labels.push_back(label.c_str());
ImPlotFlags plot_flags = ImPlotFlags_NoLegend | ImPlotFlags_NoMenus;
ApplyPremiumPlotStyles("##GenRejections", state);
if (ImPlot::BeginPlot("##GenRejections", ImVec2(-1, 120), plot_flags)) {
ImPlotAxisFlags axis_flags = static_cast<ImPlotAxisFlags>(GetPlotAxisFlags(state));
ImPlot::SetupAxes("Reason", "Count", axis_flags, axis_flags);
ImPlot::SetupAxisTicks(ImAxis_X1, 0, static_cast<double>(labels.size() - 1), static_cast<int>(labels.size()), labels.data());
ImPlot::SetNextFillStyle(GetSeriesColor(7), 0.75f);
ImPlot::PlotBars("Count", values.data(), static_cast<int>(values.size()), 0.6);
ImPlot::EndPlot();
}
ImPlot::PopStyleColor(2);
ImPlot::PopStyleVar(6);
}
} else {
ImGui::TextDisabled("Select a generator in the Dataset panel.");
}
ImGui::Separator();
ImGui::Spacing();
if (font_header) ImGui::PushFont(font_header);
ImGui::Text(ICON_MD_FILE_DOWNLOAD_DONE " DATA INTEGRITY AUDIT");
if (font_header) ImGui::PopFont();
ImGui::Separator();
auto render_audit_item = [](const char* name, bool found, bool ok, const char* error) {
ImGui::BeginGroup();
if (!found) {
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), ICON_MD_FILE_OPEN " %s", name);
ImGui::SameLine(); ImGui::TextDisabled("(Not Found)");
} else if (!ok) {
ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), ICON_MD_REPORT_GMAILERRORRED " %s", name);
ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "(Error)");
if (error && error[0] != '\0') {
ImGui::Indent();
ImGui::TextDisabled("%s", error);
ImGui::Unindent();
}
} else {
ImGui::TextColored(ImVec4(0.4f, 0.8f, 0.4f, 1.0f), ICON_MD_CHECK_CIRCLE " %s", name);
ImGui::SameLine(); ImGui::TextDisabled("(Synced)");
}
ImGui::EndGroup();
};
render_audit_item("quality_feedback.json", status.quality_found, status.quality_ok, status.quality_ok ? "" : status.last_error.c_str());
render_audit_item("active_learning.json", status.active_found, status.active_ok, status.active_ok ? "" : status.last_error.c_str());
render_audit_item("training_feedback.json", status.training_found, status.training_ok, status.training_ok ? "" : status.last_error.c_str());
ImGui::Spacing();
ImGui::TextDisabled("Integrity Score: %.1f%%", (status.OkCount() / (float)std::max(1, status.FoundCount())) * 100.0f);
ImGui::ProgressBar(status.OkCount() / (float)std::max(1, status.FoundCount()), ImVec2(-1, 0));
}
void RenderDatasetPanel(AppState& state, const DataLoader& loader) {
const auto& runs = loader.GetTrainingRuns();
const auto& generators = loader.GetGeneratorStats();
const auto& coverage = loader.GetCoverage();
const auto& dataset_registry = loader.GetDatasetRegistry();
const auto& dataset_error = loader.GetDatasetRegistryError();
if (ImGui::BeginTabBar("DatasetTabs")) {
if (ImGui::BeginTabItem("Training Runs")) {
ImGui::InputTextWithHint("##RunFilter", "Filter by run ID or model", state.run_filter.data(), state.run_filter.size());
ImGui::SameLine();
if (ImGui::Button("Clear")) state.run_filter[0] = '\0';
if (ImGui::BeginTable("RunTable", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("Run ID", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Model", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Samples", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("Loss", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("Domains", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableHeadersRow();
for (size_t i = 0; i < runs.size(); ++i) {
const auto& run = runs[i];
if (!ContainsInsensitive(run.run_id, state.run_filter.data()) && !ContainsInsensitive(run.model_name, state.run_filter.data())) continue;
bool selected = static_cast<int>(i) == state.selected_run_index;
ImGui::TableNextRow();
ImGui::TableNextColumn();
if (ImGui::Selectable(run.run_id.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) {
state.selected_run_index = static_cast<int>(i);
state.selected_run_id = run.run_id;
}
ImGui::TableNextColumn(); ImGui::Text("%s", run.model_name.empty() ? "-" : run.model_name.c_str());
ImGui::TableNextColumn(); ImGui::Text("%d", run.samples_count);
ImGui::TableNextColumn(); ImGui::Text("%.4f", run.final_loss);
ImGui::TableNextColumn(); ImGui::Text("%zu", run.domain_distribution.size());
}
ImGui::EndTable();
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Data Sources")) {
const auto& resource_index = loader.GetResourceIndex();
const auto& resource_error = loader.GetResourceIndexError();
static std::string resource_status;
if (!resource_error.empty()) {
ImGui::TextColored(ImVec4(0.9f, 0.5f, 0.2f, 1.0f), "%s", resource_error.c_str());
}
if (resource_index.total_files == 0) {
ImGui::TextDisabled("No resource index loaded.");
} else {
ImGui::Text("Total files: %d", resource_index.total_files);
ImGui::Text("Duplicates: %d", resource_index.duplicates_found);
if (!resource_index.indexed_at.empty()) {
ImGui::TextDisabled("Indexed at: %s", resource_index.indexed_at.c_str());
}
}
ImGui::Spacing();
if (ImGui::Button("Rebuild Resource Index")) {
auto scawful_root = ResolveHafsScawfulRoot();
std::filesystem::path script_path = scawful_root.empty()
? std::filesystem::current_path() / "rebuild_resource_index.py"
: scawful_root / "scripts" / "rebuild_resource_index.py";
std::string build_output;
bool ok = RunPythonScript(script_path, scawful_root, &build_output);
if (!build_output.empty()) {
resource_status = ok ? "Resource index rebuilt (see logs)" : "Resource index rebuild failed (see logs)";
} else {
resource_status = ok ? "Resource index rebuilt" : "Resource index rebuild failed";
}
state.should_refresh = true;
}
if (!resource_status.empty()) {
ImGui::SameLine();
ImGui::TextDisabled("%s", resource_status.c_str());
}
if (!resource_index.by_source.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextDisabled("Sources");
std::vector<std::pair<std::string, int>> sources;
sources.reserve(resource_index.by_source.size());
for (const auto& [name, count] : resource_index.by_source) {
sources.emplace_back(name, count);
}
std::sort(sources.begin(), sources.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
if (ImGui::BeginTable("ResourceSources", 3,
ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable |
ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch, 1.5f);
ImGui::TableSetupColumn("Files", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("Share", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableHeadersRow();
for (const auto& entry : sources) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%s", entry.first.c_str());
ImGui::TableNextColumn();
ImGui::Text("%d", entry.second);
ImGui::TableNextColumn();
float share = resource_index.total_files > 0
? static_cast<float>(entry.second) / static_cast<float>(resource_index.total_files)
: 0.0f;
ImGui::Text("%.1f%%", share * 100.0f);
}
ImGui::EndTable();
}
}
if (!resource_index.by_type.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextDisabled("File Types");
std::vector<std::pair<std::string, int>> types;
types.reserve(resource_index.by_type.size());
for (const auto& [name, count] : resource_index.by_type) {
types.emplace_back(name, count);
}
std::sort(types.begin(), types.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
if (ImGui::BeginTable("ResourceTypes", 3,
ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable |
ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthStretch, 1.5f);
ImGui::TableSetupColumn("Files", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("Share", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableHeadersRow();
for (const auto& entry : types) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%s", entry.first.c_str());
ImGui::TableNextColumn();
ImGui::Text("%d", entry.second);
ImGui::TableNextColumn();
float share = resource_index.total_files > 0
? static_cast<float>(entry.second) / static_cast<float>(resource_index.total_files)
: 0.0f;
ImGui::Text("%.1f%%", share * 100.0f);
}
ImGui::EndTable();
}
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Datasets")) {
static std::string dataset_status;
if (!dataset_error.empty()) {
ImGui::TextColored(ImVec4(0.9f, 0.5f, 0.2f, 1.0f), "%s", dataset_error.c_str());
}
ImGui::InputTextWithHint("##DatasetFilter", "Filter by dataset name", state.dataset_filter.data(), state.dataset_filter.size());
ImGui::SameLine();
if (ImGui::Button("Clear##DatasetFilter")) state.dataset_filter[0] = '\0';
ImGui::SameLine();
if (ImGui::Button("Rebuild Dataset Registry")) {
auto scawful_root = ResolveHafsScawfulRoot();
std::filesystem::path script_path = scawful_root.empty()
? std::filesystem::current_path() / "build_dataset_registry.py"
: scawful_root / "scripts" / "build_dataset_registry.py";
std::string build_output;
bool ok = RunPythonScript(script_path, scawful_root, &build_output);
if (!build_output.empty()) {
dataset_status = ok ? "Dataset registry rebuilt (see logs)" : "Dataset registry rebuild failed (see logs)";
} else {
dataset_status = ok ? "Dataset registry rebuilt" : "Dataset registry rebuild failed";
}
state.should_refresh = true;
}
if (!dataset_status.empty()) {
ImGui::SameLine();
ImGui::TextDisabled("%s", dataset_status.c_str());
}
if (dataset_registry.datasets.empty()) {
ImGui::TextDisabled("No dataset registry loaded.");
} else {
std::uint64_t total_size = 0;
for (const auto& dataset : dataset_registry.datasets) {
total_size += dataset.size_bytes;
}
double total_mb = static_cast<double>(total_size) / (1024.0 * 1024.0);
ImGui::Text("Datasets: %zu", dataset_registry.datasets.size());
ImGui::SameLine();
ImGui::TextDisabled("Total size: %.2f MB", total_mb);
if (!dataset_registry.generated_at.empty()) {
ImGui::TextDisabled("Generated at: %s", dataset_registry.generated_at.c_str());
}
if (ImGui::BeginTable("DatasetRegistryTable", 4,
ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable |
ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Dataset", ImGuiTableColumnFlags_WidthStretch, 1.4f);
ImGui::TableSetupColumn("Files", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("Size (MB)", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableSetupColumn("Updated", ImGuiTableColumnFlags_WidthStretch, 1.1f);
ImGui::TableHeadersRow();
for (size_t i = 0; i < dataset_registry.datasets.size(); ++i) {
const auto& dataset = dataset_registry.datasets[i];
if (!ContainsInsensitive(dataset.name, state.dataset_filter.data())) {
continue;
}
ImGui::TableNextRow();
ImGui::TableNextColumn();
bool selected = static_cast<int>(i) == state.selected_dataset_index;
if (ImGui::Selectable(dataset.name.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) {
state.selected_dataset_index = static_cast<int>(i);
}
if (ImGui::IsItemHovered() && !dataset.path.empty()) {
ImGui::BeginTooltip();
ImGui::Text("%s", dataset.path.c_str());
ImGui::EndTooltip();
}
ImGui::TableNextColumn();
ImGui::Text("%zu", dataset.files.size());
ImGui::TableNextColumn();
double size_mb = static_cast<double>(dataset.size_bytes) / (1024.0 * 1024.0);
ImGui::Text("%.2f", size_mb);
ImGui::TableNextColumn();
ImGui::Text("%s", dataset.updated_at.empty() ? "-" : dataset.updated_at.c_str());
}
ImGui::EndTable();
}
ImGui::Separator();
if (state.selected_dataset_index >= 0 &&
state.selected_dataset_index < static_cast<int>(dataset_registry.datasets.size())) {
const auto& selected = dataset_registry.datasets[state.selected_dataset_index];
ImGui::TextDisabled("Selected Dataset");
ImGui::Text("%s", selected.name.c_str());
ImGui::TextDisabled("%s", selected.path.empty() ? "-" : selected.path.c_str());
ImGui::Text("Files: %zu", selected.files.size());
ImGui::BeginChild("DatasetFiles", ImVec2(0, 120), true);
size_t max_files = 12;
for (size_t i = 0; i < selected.files.size() && i < max_files; ++i) {
ImGui::BulletText("%s", selected.files[i].c_str());
}
if (selected.files.size() > max_files) {
ImGui::TextDisabled("... and %zu more", selected.files.size() - max_files);
}
ImGui::EndChild();
} else {
ImGui::TextDisabled("Select a dataset to view details.");
}
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Generators")) {
ImGui::InputTextWithHint("##GenFilter", "Filter by generator name", state.generator_filter.data(), state.generator_filter.size());
ImGui::SameLine();
if (ImGui::Button("Clear##Gen")) state.generator_filter[0] = '\0';
if (ImGui::BeginTable("GeneratorTable", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Generator", ImGuiTableColumnFlags_WidthStretch, 1.5f);
ImGui::TableSetupColumn("Accepted", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("Rejected", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("Rate %", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("Quality", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableHeadersRow();
for (size_t i = 0; i < generators.size(); ++i) {
const auto& gen = generators[i];
if (!ContainsInsensitive(gen.name, state.generator_filter.data())) continue;
bool selected = static_cast<int>(i) == state.selected_generator_index;
ImGui::TableNextRow();
ImGui::TableNextColumn();
if (ImGui::Selectable(gen.name.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) {
state.selected_generator_index = static_cast<int>(i);
state.selected_generator_name = gen.name;
}
ImGui::TableNextColumn(); ImGui::Text("%d", gen.samples_accepted);
ImGui::TableNextColumn(); ImGui::Text("%d", gen.samples_rejected);
ImGui::TableNextColumn(); ImGui::Text("%.1f", gen.acceptance_rate * 100.0f);
ImGui::TableNextColumn(); ImGui::Text("%.3f", gen.avg_quality);
}
ImGui::EndTable();
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Domains")) {
if (coverage.domain_coverage.empty()) {
ImGui::TextDisabled("No domain coverage data available.");
} else {
if (ImGui::BeginTable("DomainCoverageTable", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) {
ImGui::TableSetupColumn("Domain", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Coverage %", ImGuiTableColumnFlags_WidthFixed, 110);
ImGui::TableSetupColumn("Bar", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
for (const auto& [domain, value] : coverage.domain_coverage) {
ImGui::TableNextRow();
ImGui::TableNextColumn(); ImGui::Text("%s", domain.c_str());
ImGui::TableNextColumn(); ImGui::Text("%.1f", value * 100.0f);
ImGui::TableNextColumn(); ImGui::ProgressBar(value, ImVec2(-1, 0));
}
ImGui::EndTable();
}
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Curated Hacks")) {
const auto& hacks = loader.GetCuratedHacks();
const auto& curated_error = loader.GetCuratedHacksError();
static std::unordered_map<std::string, HackOverrideEdit> override_edits;
static std::unordered_map<std::string, NoteBufferState> note_buffers;
static bool overrides_loaded = false;
static std::string overrides_error;
static std::string overrides_status;
static std::string selected_hack_name;
static std::array<char, 2048> notes_buffer{};
static std::array<char, 1024> include_buffer{};
static std::array<char, 1024> exclude_buffer{};
auto scawful_root = ResolveHafsScawfulRoot();
std::filesystem::path override_path = scawful_root.empty()
? studio::core::FileSystem::ResolvePath("~/.config/afs/curated_hacks_overrides.toml")
: scawful_root / "config" / "curated_hacks_overrides.toml";
if (!overrides_loaded) {
LoadOverrideFile(override_path, &override_edits, &overrides_error);
overrides_loaded = true;
}
if (!curated_error.empty()) {
ImGui::TextColored(ImVec4(0.9f, 0.5f, 0.2f, 1.0f), "%s", curated_error.c_str());
}
ImGui::Spacing();
if (ImGui::Button("Reload Overrides")) {
LoadOverrideFile(override_path, &override_edits, &overrides_error);
overrides_status = overrides_error.empty() ? "Overrides reloaded" : overrides_error;
note_buffers.clear();
}
ImGui::SameLine();
if (ImGui::Button("Save Overrides")) {
std::ostringstream output;
output << "# Curated hack overrides (generated by AFS Studio)\n";
int saved = 0;
for (const auto& hack : hacks) {
auto it = override_edits.find(hack.name);
if (it == override_edits.end() || !it->second.enabled) continue;
const auto& edit = it->second;
output << "\n[[hack]]\n";
output << "name = \"" << hack.name << "\"\n";
if (!edit.review_status.empty()) {
output << "review_status = \"" << edit.review_status << "\"\n";
}
output << "weight = " << std::fixed << std::setprecision(2) << edit.weight << "\n";
if (edit.notes_set) {
output << "notes = \"" << EscapeTomlString(edit.notes) << "\"\n";
}
if (edit.include_set) {
WriteStringArray(output, "include_globs", edit.include_globs);
}
if (edit.exclude_set) {
WriteStringArray(output, "exclude_globs", edit.exclude_globs);
}
saved++;
}
if (studio::core::FileSystem::EnsureDirectory(override_path.parent_path()) &&
studio::core::FileSystem::WriteFile(override_path, output.str())) {
overrides_status = "Saved " + std::to_string(saved) + " overrides";
for (auto& [name, edit] : override_edits) {
edit.dirty = false;
}
} else {
overrides_status = "Failed to save overrides";
}
}
ImGui::SameLine();
if (ImGui::Button("Save + Rebuild Summary")) {
std::ostringstream output;
output << "# Curated hack overrides (generated by AFS Studio)\n";
int saved = 0;
for (const auto& hack : hacks) {
auto it = override_edits.find(hack.name);
if (it == override_edits.end() || !it->second.enabled) continue;
const auto& edit = it->second;
output << "\n[[hack]]\n";
output << "name = \"" << hack.name << "\"\n";
if (!edit.review_status.empty()) {
output << "review_status = \"" << edit.review_status << "\"\n";
}
output << "weight = " << std::fixed << std::setprecision(2) << edit.weight << "\n";
if (edit.notes_set) {
output << "notes = \"" << EscapeTomlString(edit.notes) << "\"\n";
}
if (edit.include_set) {
WriteStringArray(output, "include_globs", edit.include_globs);
}
if (edit.exclude_set) {
WriteStringArray(output, "exclude_globs", edit.exclude_globs);
}
saved++;
}
if (studio::core::FileSystem::EnsureDirectory(override_path.parent_path()) &&
studio::core::FileSystem::WriteFile(override_path, output.str())) {
std::filesystem::path script_path = override_path.parent_path().parent_path();
script_path /= "scripts/build_curated_hacks_summary.py";
std::string build_output;
bool ok = RunPythonScript(script_path, scawful_root, &build_output);
overrides_status = ok ? "Saved overrides and rebuilt summary" : "Saved overrides, rebuild failed";
if (!build_output.empty()) {
overrides_status += " (see logs)";
}
state.should_refresh = true;
for (auto& [name, edit] : override_edits) {
edit.dirty = false;
}
} else {
overrides_status = "Failed to save overrides";
}
}
if (!overrides_status.empty()) {
ImGui::SameLine();
ImGui::TextDisabled("%s", overrides_status.c_str());
}
ImGui::TextDisabled("Tip: Save + Rebuild Summary refreshes file counts automatically.");
if (!overrides_error.empty()) {
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f), "%s", overrides_error.c_str());
}
if (hacks.empty()) {
ImGui::TextDisabled("No curated hack summary available.");
if (!scawful_root.empty()) {
ImGui::TextDisabled("Run: %s", (scawful_root / "scripts" / "build_curated_hacks_summary.py").string().c_str());
} else {
ImGui::TextDisabled("Curated hacks plugin not configured.");
}
} else if (ImGui::BeginTable("CuratedHacksTable", 10,
ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable |
ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY |
ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoSavedSettings)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 1.2f);
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 85);
ImGui::TableSetupColumn("Review", ImGuiTableColumnFlags_WidthFixed, 85);
ImGui::TableSetupColumn("Weight", ImGuiTableColumnFlags_WidthFixed, 65);
ImGui::TableSetupColumn("Eligible", ImGuiTableColumnFlags_WidthFixed, 65);
ImGui::TableSetupColumn("Select", ImGuiTableColumnFlags_WidthFixed, 65);
ImGui::TableSetupColumn("Org%", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableSetupColumn("Addr%", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableSetupColumn("Comm%", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableSetupColumn("Path", ImGuiTableColumnFlags_WidthStretch, 2.0f);
ImGui::TableHeadersRow();
auto load_editor_buffers = [&](const auto& hack, HackOverrideEdit& edit) {
const std::string notes_text = edit.notes_set ? edit.notes : hack.notes;
const auto& include_list = edit.include_set ? edit.include_globs : hack.include_globs;
const auto& exclude_list = edit.exclude_set ? edit.exclude_globs : hack.exclude_globs;
FillBuffer(notes_buffer, notes_text);
FillBuffer(include_buffer, JoinLines(include_list));
FillBuffer(exclude_buffer, JoinLines(exclude_list));
auto& note_state = note_buffers[hack.name];
FillBuffer(note_state.buffer, notes_text);
note_state.initialized = true;
};
for (const auto& hack : hacks) {
auto& edit = override_edits[hack.name];
if (!edit.initialized) {
edit.review_status = hack.review_status;
edit.weight = hack.weight;
edit.weight_set = true;
edit.notes = hack.notes;
edit.notes_set = !hack.notes.empty();
edit.include_globs = hack.include_globs;
edit.include_set = !hack.include_globs.empty();
edit.exclude_globs = hack.exclude_globs;
edit.exclude_set = !hack.exclude_globs.empty();
edit.initialized = true;
} else if (!edit.weight_set) {
edit.weight = hack.weight;
edit.weight_set = true;
}
auto& note_state = note_buffers[hack.name];
if (!note_state.initialized) {
const std::string notes_text = edit.notes_set ? edit.notes : hack.notes;
FillBuffer(note_state.buffer, notes_text);
note_state.initialized = true;
}
ImGui::TableNextRow();
ImGui::TableNextColumn();
bool is_selected = (selected_hack_name == hack.name);
if (ImGui::Selectable(hack.name.empty() ? "-" : hack.name.c_str(), is_selected)) {
selected_hack_name = hack.name;
load_editor_buffers(hack, edit);
}
if (!hack.authors.empty() && ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Authors:");
for (const auto& author : hack.authors) {
ImGui::BulletText("%s", author.c_str());
}
const auto& include_list = edit.include_set ? edit.include_globs : hack.include_globs;
const auto& exclude_list = edit.exclude_set ? edit.exclude_globs : hack.exclude_globs;
const auto& sample_list = hack.sample_files;
if (!sample_list.empty()) {
ImGui::Separator();
ImGui::Text("Sample files:");
for (const auto& sample : sample_list) {
ImGui::BulletText("%s", sample.c_str());
}
}
if (!include_list.empty()) {
ImGui::Separator();
ImGui::Text("Include globs:");
for (const auto& glob : include_list) {
ImGui::BulletText("%s", glob.c_str());
}
}
if (!exclude_list.empty()) {
ImGui::Separator();
ImGui::Text("Exclude globs:");
for (const auto& glob : exclude_list) {
ImGui::BulletText("%s", glob.c_str());
}
}
ImGui::EndTooltip();
}
ImGui::TableNextColumn();
if (ImGui::Button(("Edit##" + hack.name).c_str())) {
selected_hack_name = hack.name;
load_editor_buffers(hack, edit);
}
ImGui::TableNextColumn();
bool enabled = edit.enabled;
if (ImGui::Checkbox(("##override_" + hack.name).c_str(), &enabled)) {
edit.enabled = enabled;
edit.dirty = true;
}
ImGui::TableNextColumn();
if (edit.dirty) {
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f), "dirty");
} else {
ImGui::TextDisabled("-");
}
ImGui::TableNextColumn();
ImGui::BeginDisabled(!edit.enabled);
float weight = edit.weight;
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::SliderFloat(("##weight_" + hack.name).c_str(), &weight, 0.0f, 1.0f, "%.2f")) {
edit.weight = weight;
edit.weight_set = true;
edit.dirty = true;
}
ImGui::EndDisabled();
ImGui::TableNextColumn();
ImGui::BeginDisabled(!edit.enabled);
const char* status_items[] = {"", "approved", "hold", "rejected"};
int status_index = 0;
if (edit.review_status == "approved") status_index = 1;
else if (edit.review_status == "hold") status_index = 2;
else if (edit.review_status == "rejected") status_index = 3;
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::Combo(("##review_" + hack.name).c_str(), &status_index, status_items, 4)) {
edit.review_status = status_items[status_index];
edit.dirty = true;
}
ImGui::EndDisabled();
ImGui::TableNextColumn();
ImGui::Text("%d/%d", hack.selected_files, hack.eligible_files);
ImGui::TableNextColumn();
ImGui::Text("%.0f", hack.org_ratio * 100.0f);
ImGui::TableNextColumn();
ImGui::Text("%.0f", hack.address_ratio * 100.0f);
ImGui::TableNextColumn();
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::InputTextWithHint(("##note_" + hack.name).c_str(),
"Notes...", note_state.buffer.data(),
note_state.buffer.size())) {
edit.notes = TrimCopy(std::string(note_state.buffer.data()));
edit.notes_set = true;
edit.enabled = true;
edit.dirty = true;
if (selected_hack_name == hack.name) {
FillBuffer(notes_buffer, edit.notes);
}
}
std::string notes = edit.notes_set ? edit.notes : hack.notes;
if (notes.empty()) notes = "-";
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::TextWrapped("%s", notes.c_str());
if (!hack.path.empty()) {
ImGui::Separator();
ImGui::TextDisabled("%s", hack.path.c_str());
}
ImGui::EndTooltip();
}
}
ImGui::EndTable();
}
if (!hacks.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Text(ICON_MD_EDIT " Override Editor");
if (selected_hack_name.empty()) {
ImGui::TextDisabled("Select a hack row to edit include/exclude globs and notes.");
} else {
auto it = std::find_if(hacks.begin(), hacks.end(), [&](const auto& h) {
return h.name == selected_hack_name;
});
if (it == hacks.end()) {
ImGui::TextDisabled("Selected hack not found.");
} else {
auto& edit = override_edits[it->name];
ImGui::Text("Editing: %s", it->name.c_str());
ImGui::SameLine();
if (ImGui::Button("Revert Override")) {
edit.enabled = false;
edit.dirty = true;
edit.review_status = it->review_status;
edit.weight = it->weight;
edit.weight_set = false;
edit.notes = it->notes;
edit.notes_set = false;
edit.include_globs = it->include_globs;
edit.include_set = false;
edit.exclude_globs = it->exclude_globs;
edit.exclude_set = false;
FillBuffer(notes_buffer, it->notes);
FillBuffer(include_buffer, JoinLines(it->include_globs));
FillBuffer(exclude_buffer, JoinLines(it->exclude_globs));
overrides_status = "Cleared override for " + it->name + " (not saved)";
}
bool notes_changed = ImGui::InputTextMultiline(
"Notes", notes_buffer.data(), notes_buffer.size(), ImVec2(-1, 90));
bool include_changed = ImGui::InputTextMultiline(
"Include globs (one per line)", include_buffer.data(), include_buffer.size(),
ImVec2(-1, 70));
bool exclude_changed = ImGui::InputTextMultiline(
"Exclude globs (one per line)", exclude_buffer.data(), exclude_buffer.size(),
ImVec2(-1, 70));
if (notes_changed || include_changed || exclude_changed) {
edit.notes = TrimCopy(std::string(notes_buffer.data()));
edit.notes_set = true;
edit.include_globs = SplitLines(include_buffer.data());
edit.include_set = true;
edit.exclude_globs = SplitLines(exclude_buffer.data());
edit.exclude_set = true;
edit.enabled = true;
edit.dirty = true;
overrides_status = "Updated overrides for " + it->name + " (not saved)";
}
if (ImGui::Button("Reset Editor")) {
auto load_editor_buffers = [&](const auto& hack, HackOverrideEdit& edit_ref) {
const std::string notes_text = edit_ref.notes_set ? edit_ref.notes : hack.notes;
const auto& include_list = edit_ref.include_set ? edit_ref.include_globs : hack.include_globs;
const auto& exclude_list = edit_ref.exclude_set ? edit_ref.exclude_globs : hack.exclude_globs;
FillBuffer(notes_buffer, notes_text);
FillBuffer(include_buffer, JoinLines(include_list));
FillBuffer(exclude_buffer, JoinLines(exclude_list));
};
load_editor_buffers(*it, edit);
}
}
}
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
void RenderSystemsPanel(AppState& state, ImFont* font_header, std::function<void(const char*)> refresh_callback) {
if (font_header) ImGui::PushFont(font_header);
ImGui::Text(ICON_MD_ROUTER " SYSTEMS OVERVIEW");
if (font_header) ImGui::PopFont();
ImGui::Separator();
if (ImGui::Button(ICON_MD_REFRESH " Refresh Now", ImVec2(-1, 0))) {
if (refresh_callback) refresh_callback("ui");
}
ImGui::Spacing();
double seconds_since = std::max(0.0, glfwGetTime() - state.last_refresh_time);
ImGui::Text("Auto Refresh: %s", state.auto_refresh ? "On" : "Off");
ImGui::Text("Interval: %.1fs", state.refresh_interval_sec);
ImGui::Text("Last Refresh: %.0fs ago", seconds_since);
ImGui::Text("Simulation: %s", state.simulate_activity ? "On" : "Off");
ImGui::Text("Quality Threshold: %.2f", state.quality_threshold);
ImGui::Text("Mission Concurrency: %d", state.mission_concurrency);
ImGui::Spacing();
ImGui::Separator();
ImGui::TextDisabled("Swarm Snapshot");
int active_count = 0;
int total_queue = 0;
float total_success = 0.0f;
for (const auto& agent : state.agents) {
if (agent.enabled) active_count++;
total_queue += agent.queue_depth;
total_success += agent.success_rate;
}
if (!state.agents.empty()) total_success /= static_cast<float>(state.agents.size());
if (ImGui::BeginTable("SystemSnapshot", 2, ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableNextColumn();
ImGui::Text("Active Agents");
ImGui::Text("Queue Depth");
ImGui::Text("Avg Success");
ImGui::TableNextColumn();
ImGui::Text("%d / %d", active_count, static_cast<int>(state.agents.size()));
ImGui::Text("%d", total_queue);
ImGui::Text("%.1f%%", total_success * 100.0f);
ImGui::EndTable();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::TextDisabled("Core Services");
auto render_row = [](const char* name, const char* status) {
ImVec4 status_color = ImVec4(0.4f, 0.8f, 0.4f, 1.0f);
if (strcmp(status, "Warning") == 0) status_color = ImVec4(0.9f, 0.7f, 0.2f, 1.0f);
else if (strcmp(status, "Error") == 0) status_color = ImVec4(0.9f, 0.3f, 0.3f, 1.0f);
ImGui::Bullet(); ImGui::SameLine();
ImGui::Text("%s", name); ImGui::SameLine();
ImGui::TextColored(status_color, "%s", status);
};
render_row("Orchestrator", "Active");
render_row("Knowledge Base", "Active");
render_row("Trainer", "Active");
render_row("Embedding SVC", "Warning");
}
void RenderMenuBar(AppState& state,
std::function<void(const char*)> refresh_callback,
std::function<void()> quit_callback,
ShortcutManager& shortcuts,
bool* show_sample_review,
bool* show_shortcuts_window) {
if (ImGui::BeginMainMenuBar()) {
auto shortcut_label = [&](ActionId action_id) -> std::string {
return shortcuts.FormatShortcut(action_id, ImGui::GetIO());
};
if (ImGui::BeginMenu("File")) {
std::string refresh_shortcut = shortcut_label(ActionId::Refresh);
if (ImGui::MenuItem("Refresh", refresh_shortcut.empty() ? nullptr
: refresh_shortcut.c_str())) {
if (refresh_callback) refresh_callback("manual");
}
ImGui::Separator();
std::string quit_shortcut = shortcut_label(ActionId::Quit);
if (ImGui::MenuItem("Quit", quit_shortcut.empty() ? nullptr
: quit_shortcut.c_str())) {
if (quit_callback) quit_callback();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View")) {
if (ImGui::MenuItem("Reset Layout (F1)")) {
state.force_reset_layout = true;
}
ImGui::Separator();
if (ImGui::BeginMenu("Workspace")) {
std::string dash_shortcut = shortcut_label(ActionId::WorkspaceDashboard);
if (ImGui::MenuItem("Dashboard", dash_shortcut.empty() ? nullptr
: dash_shortcut.c_str(),
state.current_workspace == Workspace::Dashboard)) {
state.current_workspace = Workspace::Dashboard;
if (state.reset_layout_on_workspace_change) state.force_reset_layout = true;
}
std::string analysis_shortcut = shortcut_label(ActionId::WorkspaceAnalysis);
if (ImGui::MenuItem("Analysis", analysis_shortcut.empty() ? nullptr
: analysis_shortcut.c_str(),
state.current_workspace == Workspace::Analysis)) {
state.current_workspace = Workspace::Analysis;
if (state.reset_layout_on_workspace_change) state.force_reset_layout = true;
}
std::string optimization_shortcut = shortcut_label(ActionId::WorkspaceOptimization);
if (ImGui::MenuItem("Optimization",
optimization_shortcut.empty() ? nullptr
: optimization_shortcut.c_str(),
state.current_workspace == Workspace::Optimization)) {
state.current_workspace = Workspace::Optimization;
if (state.reset_layout_on_workspace_change) state.force_reset_layout = true;
}
std::string systems_shortcut = shortcut_label(ActionId::WorkspaceSystems);
if (ImGui::MenuItem("Systems", systems_shortcut.empty() ? nullptr
: systems_shortcut.c_str(),
state.current_workspace == Workspace::Systems)) {
state.current_workspace = Workspace::Systems;
if (state.reset_layout_on_workspace_change) state.force_reset_layout = true;
}
std::string custom_shortcut = shortcut_label(ActionId::WorkspaceCustom);
if (ImGui::MenuItem("Custom Grid", custom_shortcut.empty() ? nullptr
: custom_shortcut.c_str(),
state.current_workspace == Workspace::Custom)) {
state.current_workspace = Workspace::Custom;
if (state.reset_layout_on_workspace_change) state.force_reset_layout = true;
}
std::string training_shortcut = shortcut_label(ActionId::WorkspaceTraining);
if (ImGui::MenuItem("Training Hub", training_shortcut.empty() ? nullptr
: training_shortcut.c_str(),
state.current_workspace == Workspace::Training)) {
state.current_workspace = Workspace::Training;
if (state.reset_layout_on_workspace_change) state.force_reset_layout = true;
}
std::string context_shortcut = shortcut_label(ActionId::WorkspaceContext);
if (ImGui::MenuItem("Context Broker", context_shortcut.empty() ? nullptr
: context_shortcut.c_str(),
state.current_workspace == Workspace::Context)) {
state.current_workspace = Workspace::Context;
if (state.reset_layout_on_workspace_change) state.force_reset_layout = true;
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Theme Profile")) {
if (ImGui::MenuItem("Deep Sea (Default)", nullptr, state.current_theme == ThemeProfile::Default)) state.current_theme = ThemeProfile::Default;
if (ImGui::MenuItem("Cyberpunk", nullptr, state.current_theme == ThemeProfile::Cyberpunk)) state.current_theme = ThemeProfile::Cyberpunk;
if (ImGui::MenuItem("Monochrome", nullptr, state.current_theme == ThemeProfile::Monochrome)) state.current_theme = ThemeProfile::Monochrome;
if (ImGui::MenuItem("Solarized", nullptr, state.current_theme == ThemeProfile::Solarized)) state.current_theme = ThemeProfile::Solarized;
if (ImGui::MenuItem("Nord", nullptr, state.current_theme == ThemeProfile::Nord)) state.current_theme = ThemeProfile::Nord;
if (ImGui::MenuItem("Dracula", nullptr, state.current_theme == ThemeProfile::Dracula)) state.current_theme = ThemeProfile::Dracula;
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::BeginMenu("Panels")) {
std::string inspector_shortcut = shortcut_label(ActionId::ToggleInspector);
ImGui::MenuItem("Inspector", inspector_shortcut.empty() ? nullptr
: inspector_shortcut.c_str(),
&state.show_inspector);
std::string dataset_shortcut = shortcut_label(ActionId::ToggleDatasetPanel);
ImGui::MenuItem("Dataset Panel", dataset_shortcut.empty() ? nullptr
: dataset_shortcut.c_str(),
&state.show_dataset_panel);
std::string systems_panel_shortcut = shortcut_label(ActionId::ToggleSystemsPanel);
ImGui::MenuItem("Systems Panel", systems_panel_shortcut.empty() ? nullptr
: systems_panel_shortcut.c_str(),
&state.show_systems_panel);
std::string status_shortcut = shortcut_label(ActionId::ToggleStatusBar);
ImGui::MenuItem("Status Strip", status_shortcut.empty() ? nullptr
: status_shortcut.c_str(),
&state.show_status_strip);
std::string controls_shortcut = shortcut_label(ActionId::ToggleControls);
ImGui::MenuItem("Sidebar Controls", controls_shortcut.empty() ? nullptr
: controls_shortcut.c_str(),
&state.show_controls);
ImGui::MenuItem("Chat Panel", nullptr, &state.show_chat_panel);
ImGui::Separator();
ImGui::MenuItem("Quality Trends", nullptr, &state.show_quality_trends);
ImGui::MenuItem("Generator Efficiency", nullptr, &state.show_generator_efficiency);
ImGui::MenuItem("Coverage Density", nullptr, &state.show_coverage_density);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Windows")) {
if (show_sample_review) {
std::string review_shortcut = shortcut_label(ActionId::ToggleSampleReview);
ImGui::MenuItem("Sample Review", review_shortcut.empty() ? nullptr
: review_shortcut.c_str(),
show_sample_review);
}
if (show_shortcuts_window) {
std::string shortcuts_shortcut =
shortcut_label(ActionId::ToggleShortcutsWindow);
ImGui::MenuItem("Keyboard Shortcuts",
shortcuts_shortcut.empty() ? nullptr
: shortcuts_shortcut.c_str(),
show_shortcuts_window);
}
ImGui::EndMenu();
}
ImGui::Separator();
std::string compact_shortcut = shortcut_label(ActionId::ToggleCompactUI);
ImGui::MenuItem("Compact UI", compact_shortcut.empty() ? nullptr
: compact_shortcut.c_str(),
&state.compact_charts);
std::string lock_shortcut = shortcut_label(ActionId::ToggleLockLayout);
ImGui::MenuItem("Lock Layout", lock_shortcut.empty() ? nullptr
: lock_shortcut.c_str(),
&state.lock_layout);
std::string reset_shortcut = shortcut_label(ActionId::ResetLayout);
if (ImGui::MenuItem("Reset Layout",
reset_shortcut.empty() ? nullptr
: reset_shortcut.c_str())) {
state.force_reset_layout = true;
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Tools")) {
std::string auto_refresh_shortcut =
shortcut_label(ActionId::ToggleAutoRefresh);
ImGui::MenuItem("Auto Refresh",
auto_refresh_shortcut.empty()
? nullptr
: auto_refresh_shortcut.c_str(),
&state.auto_refresh);
std::string simulate_shortcut =
shortcut_label(ActionId::ToggleSimulation);
ImGui::MenuItem("Simulate Activity",
simulate_shortcut.empty()
? nullptr
: simulate_shortcut.c_str(),
&state.simulate_activity);
ImGui::Separator();
std::string demo_shortcut = shortcut_label(ActionId::ToggleDemoWindow);
ImGui::MenuItem("Show ImGui Demo", demo_shortcut.empty() ? nullptr
: demo_shortcut.c_str(),
&state.show_demo_window);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("Documentation")) {}
std::string help_shortcut =
shortcut_label(ActionId::ToggleShortcutsWindow);
if (ImGui::MenuItem("Shortcuts",
help_shortcut.empty() ? nullptr
: help_shortcut.c_str())) {
if (show_shortcuts_window) *show_shortcuts_window = true;
}
ImGui::Separator();
if (ImGui::MenuItem("About AFS Viz")) {}
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
}
void RenderSidebar(AppState& state, const DataLoader& loader, ImFont* font_ui, ImFont* font_header) {
// Make the entire sidebar content scrollable to avoid overlaps
ImGui::BeginChild("SidebarScroll", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_NoBackground);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 4));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1, 1, 1, 0.05f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1, 1, 1, 0.08f));
auto sidebar_button = [&](const char* label, Workspace ws, const char* icon) {
bool active = state.current_workspace == ws;
ImGui::PushID(label);
ImVec2 size = ImVec2(ImGui::GetContentRegionAvail().x, 38);
ImVec2 p_cursor = ImGui::GetCursorScreenPos();
if (ImGui::InvisibleButton("##btn", size)) {
if (state.current_workspace != ws) {
state.current_workspace = ws;
if (state.reset_layout_on_workspace_change) state.force_reset_layout = true;
}
}
bool hovered = ImGui::IsItemHovered();
bool pressed = ImGui::IsItemActive();
ImDrawList* draw = ImGui::GetWindowDrawList();
ImU32 bg_col = hovered ? ImGui::GetColorU32(ImGuiCol_ButtonHovered) : (pressed ? ImGui::GetColorU32(ImGuiCol_ButtonActive) : 0);
if (active) bg_col = ImGui::GetColorU32(ImVec4(1, 1, 1, 0.08f));
if (bg_col != 0) {
draw->AddRectFilled(p_cursor, ImVec2(p_cursor.x + size.x, p_cursor.y + size.y), bg_col, 4.0f);
}
if (active) {
draw->AddRectFilled(p_cursor, ImVec2(p_cursor.x + 3, p_cursor.y + size.y), ImGui::GetColorU32(ImVec4(0.40f, 0.85f, 1.0f, 1.0f)), 2.0f);
}
ImVec2 text_pos = ImVec2(p_cursor.x + 12, p_cursor.y + (size.y - ImGui::GetFontSize()) * 0.5f);
draw->AddText(font_ui, ImGui::GetFontSize(), text_pos, active ? ImGui::GetColorU32(ImVec4(0.40f, 0.85f, 1.0f, 1.0f)) : ImGui::GetColorU32(ImGuiCol_Text), std::string(std::string(icon) + " " + label).c_str());
if (hovered) ImGui::SetTooltip("%s workspace", label);
ImGui::PopID();
};
auto sidebar_header = [&](const char* title, const char* icon = nullptr) {
ImGui::Spacing(); ImGui::Spacing();
if (font_header) ImGui::PushFont(font_header);
ImGui::SetCursorPosX(12);
if (icon) {
ImGui::TextDisabled("%s %s", icon, title);
} else {
ImGui::TextDisabled("%s", title);
}
if (font_header) ImGui::PopFont();
ImGui::Spacing();
};
sidebar_header("WORKSPACES", ICON_MD_VIEW_QUILT);
sidebar_button("Dashboard", Workspace::Dashboard, ICON_MD_DASHBOARD);
sidebar_button("Analysis", Workspace::Analysis, ICON_MD_ANALYTICS);
sidebar_button("Optimization", Workspace::Optimization, ICON_MD_SETTINGS_INPUT_COMPONENT);
sidebar_header("OPERATIONS", ICON_MD_SETTINGS_SUGGEST);
sidebar_button("Systems", Workspace::Systems, ICON_MD_ROUTER);
sidebar_button("Training", Workspace::Training, ICON_MD_MODEL_TRAINING);
sidebar_button("Custom Grid", Workspace::Custom, ICON_MD_DASHBOARD_CUSTOMIZE);
sidebar_header("REGISTRIES", ICON_MD_STORAGE);
sidebar_button("Context", Workspace::Context, ICON_MD_FOLDER_OPEN);
sidebar_button("Models", Workspace::Models, ICON_MD_STICKY_NOTE_2);
// New: Useful Tools Section
sidebar_header("SYSTEM TOOLS", ICON_MD_HANDYMAN);
ImGui::Indent(12);
// Health Summary
{
float health = 0.92f; // Mock health
ImGui::TextDisabled("System Health");
ImGui::ProgressBar(health, ImVec2(-12, 4), "");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Overall reliability score: %.0f%%", health * 100.0f);
}
ImGui::Spacing();
// Quick Toggles
if (ImGui::Checkbox("Simulate", &state.simulate_activity)) {}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Toggle background task simulation");
if (ImGui::Checkbox("Auto Refresh", &state.auto_refresh)) {}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Refresh data every %.1fs", state.refresh_interval_sec);
ImGui::Unindent(12);
// New: Mounts Management Section
sidebar_header("LOCAL MOUNTS", ICON_MD_STORAGE);
ImGui::Indent(12);
const auto& mounts = loader.GetMounts();
if (mounts.empty()) {
ImGui::TextDisabled("No mounts discovered");
} else {
for (const auto& mount : mounts) {
ImGui::BeginGroup();
ImGui::TextColored(mount.active ? ImVec4(0.4f, 1.0f, 0.6f, 1.0f) : ImVec4(0.5f, 0.5f, 0.5f, 1.0f),
mount.active ? ICON_MD_DNS : ICON_MD_DASHBOARD_CUSTOMIZE);
ImGui::SameLine();
ImGui::Text("%s", mount.name.c_str());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Path: %s\nStatus: %s", mount.path.c_str(), mount.active ? "Connected" : "Disconnected");
ImGui::EndGroup();
}
}
if (ImGui::SmallButton(ICON_MD_STORAGE " Mount Drives")) {
const_cast<DataLoader&>(loader).MountDrive("all");
}
ImGui::SameLine();
if (ImGui::SmallButton(ICON_MD_ADD " Add Mount")) {
// TODO: Implement mount dialog
}
ImGui::Unindent(12);
ImGui::PopStyleColor(3);
ImGui::PopStyleVar();
ImGui::EndChild(); // End SidebarScroll
}
} // namespace ui
} // namespace viz
} // namespace afs