studio: improve data-backed graphs
This commit is contained in:
@@ -113,6 +113,7 @@ void App::RefreshData(const char* reason) {
|
||||
bool ok = loader_.Refresh();
|
||||
state_.last_refresh_time = glfwGetTime();
|
||||
SyncDataBackedState();
|
||||
EnsureActiveGraph();
|
||||
|
||||
const auto& status = loader_.GetLastStatus();
|
||||
std::string msg;
|
||||
@@ -230,6 +231,35 @@ void App::SyncDataBackedState() {
|
||||
}
|
||||
}
|
||||
|
||||
void App::EnsureActiveGraph() {
|
||||
if (state_.active_graph != PlotKind::None &&
|
||||
graph_browser_.IsGraphAvailable(state_.active_graph, state_, loader_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PlotKind preferred[] = {
|
||||
PlotKind::KnowledgeGraph,
|
||||
PlotKind::DatasetInventory,
|
||||
PlotKind::MountsStatus,
|
||||
};
|
||||
|
||||
for (auto kind : preferred) {
|
||||
if (graph_browser_.IsGraphAvailable(kind, state_, loader_)) {
|
||||
graph_navigator_.NavigateToGraph(state_, kind);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& graph : graph_browser_.GetAllGraphs()) {
|
||||
if (graph_browser_.IsGraphAvailable(graph.kind, state_, loader_)) {
|
||||
graph_navigator_.NavigateToGraph(state_, graph.kind);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
state_.active_graph = PlotKind::None;
|
||||
}
|
||||
|
||||
void App::SeedDefaultState() {
|
||||
ui::AppendLog(state_, "system", "AFS Studio environment ready.", "system");
|
||||
state_.sparkline_data.resize(30, 0.0f);
|
||||
@@ -367,7 +397,7 @@ void App::RenderLayout() {
|
||||
if (state_.show_graph_browser) {
|
||||
ImGui::Begin("GraphBrowser", &state_.show_graph_browser,
|
||||
ImGuiWindowFlags_NoCollapse);
|
||||
graph_browser_.Render(state_);
|
||||
graph_browser_.Render(state_, loader_);
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class App {
|
||||
void SeedDefaultState();
|
||||
void SyncDataBackedState();
|
||||
void TickSimulatedMetrics(float dt);
|
||||
void EnsureActiveGraph();
|
||||
|
||||
void RenderFrame();
|
||||
void RenderLayout();
|
||||
|
||||
@@ -40,7 +40,17 @@ RegistryReader::RegistryReader(const std::filesystem::path& registry_path)
|
||||
: registry_path_(registry_path) {}
|
||||
|
||||
std::filesystem::path RegistryReader::ResolveDefaultPath() const {
|
||||
return ResolveContextRoot() / "models" / "registry.json";
|
||||
auto preferred = ResolveContextRoot() / "models" / "registry.json";
|
||||
if (core::FileSystem::Exists(preferred)) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
auto fallback = core::FileSystem::ResolvePath("~/.context/models/registry.json");
|
||||
if (core::FileSystem::Exists(fallback)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return preferred;
|
||||
}
|
||||
|
||||
bool RegistryReader::Exists() const {
|
||||
|
||||
@@ -22,6 +22,8 @@ using json = nlohmann::json;
|
||||
constexpr size_t kTrendWindow = 5;
|
||||
constexpr float kPi = 3.14159265f;
|
||||
|
||||
std::optional<std::filesystem::path> ResolveTrunkRoot();
|
||||
|
||||
std::optional<std::filesystem::path> ResolveHafsScawfulRoot() {
|
||||
const char* env_root = std::getenv("AFS_SCAWFUL_ROOT");
|
||||
if (env_root && env_root[0] != '\0') {
|
||||
@@ -318,6 +320,15 @@ bool DataLoader::Refresh() {
|
||||
}
|
||||
|
||||
LoadResult resource = LoadResourceIndex(&next_resource_index);
|
||||
last_status_.resource_index_found = resource.found;
|
||||
last_status_.resource_index_ok = resource.ok;
|
||||
if (resource.found && !resource.ok) {
|
||||
last_status_.error_count += 1;
|
||||
if (last_status_.last_error.empty()) {
|
||||
last_status_.last_error = resource.error;
|
||||
last_status_.last_error_source = "resource_index.json";
|
||||
}
|
||||
}
|
||||
if (!resource.found) {
|
||||
resource_index_ = ResourceIndexData{};
|
||||
resource_index_error_ = "resource_index.json not found";
|
||||
@@ -329,6 +340,15 @@ bool DataLoader::Refresh() {
|
||||
}
|
||||
|
||||
LoadResult registry = LoadDatasetRegistry(&next_dataset_registry);
|
||||
last_status_.dataset_registry_found = registry.found;
|
||||
last_status_.dataset_registry_ok = registry.ok;
|
||||
if (registry.found && !registry.ok) {
|
||||
last_status_.error_count += 1;
|
||||
if (last_status_.last_error.empty()) {
|
||||
last_status_.last_error = registry.error;
|
||||
last_status_.last_error_source = "dataset_registry.json";
|
||||
}
|
||||
}
|
||||
if (!registry.found) {
|
||||
dataset_registry_ = DatasetRegistryData{};
|
||||
dataset_registry_error_ = "dataset_registry.json not found";
|
||||
@@ -340,6 +360,15 @@ bool DataLoader::Refresh() {
|
||||
}
|
||||
|
||||
LoadResult context_graph = LoadContextGraph(&next_context_graph);
|
||||
last_status_.context_graph_found = context_graph.found;
|
||||
last_status_.context_graph_ok = context_graph.ok;
|
||||
if (context_graph.found && !context_graph.ok) {
|
||||
last_status_.error_count += 1;
|
||||
if (last_status_.last_error.empty()) {
|
||||
last_status_.last_error = context_graph.error;
|
||||
last_status_.last_error_source = "afs_graph.json";
|
||||
}
|
||||
}
|
||||
if (!context_graph.found) {
|
||||
context_graph_ = ContextGraphData{};
|
||||
context_graph_error_ = "afs_graph.json not found";
|
||||
@@ -386,7 +415,10 @@ bool DataLoader::Refresh() {
|
||||
has_data_ = !quality_trends_.empty() || !generator_stats_.empty() ||
|
||||
!embedding_regions_.empty() || !training_runs_.empty() ||
|
||||
!optimization_data_.domain_effectiveness.empty() ||
|
||||
!optimization_data_.threshold_sensitivity.empty();
|
||||
!optimization_data_.threshold_sensitivity.empty() ||
|
||||
!dataset_registry_.datasets.empty() ||
|
||||
resource_index_.total_files > 0 ||
|
||||
!context_graph_.labels.empty();
|
||||
last_error_ = last_status_.last_error;
|
||||
|
||||
return last_status_.AnyOk() || (!(last_status_.FoundCount() > 0) && has_data_);
|
||||
|
||||
@@ -150,19 +150,30 @@ struct LoadStatus {
|
||||
bool active_ok = false;
|
||||
bool training_found = false;
|
||||
bool training_ok = false;
|
||||
bool resource_index_found = false;
|
||||
bool resource_index_ok = false;
|
||||
bool dataset_registry_found = false;
|
||||
bool dataset_registry_ok = false;
|
||||
bool context_graph_found = false;
|
||||
bool context_graph_ok = false;
|
||||
int error_count = 0;
|
||||
std::string last_error;
|
||||
std::string last_error_source;
|
||||
|
||||
int FoundCount() const {
|
||||
return static_cast<int>(quality_found) + static_cast<int>(active_found) +
|
||||
static_cast<int>(training_found);
|
||||
static_cast<int>(training_found) + static_cast<int>(resource_index_found) +
|
||||
static_cast<int>(dataset_registry_found) + static_cast<int>(context_graph_found);
|
||||
}
|
||||
int OkCount() const {
|
||||
return static_cast<int>(quality_ok) + static_cast<int>(active_ok) +
|
||||
static_cast<int>(training_ok);
|
||||
static_cast<int>(training_ok) + static_cast<int>(resource_index_ok) +
|
||||
static_cast<int>(dataset_registry_ok) + static_cast<int>(context_graph_ok);
|
||||
}
|
||||
bool AnyOk() const {
|
||||
return quality_ok || active_ok || training_ok || resource_index_ok ||
|
||||
dataset_registry_ok || context_graph_ok;
|
||||
}
|
||||
bool AnyOk() const { return quality_ok || active_ok || training_ok; }
|
||||
};
|
||||
|
||||
/// Loads training data from JSON files.
|
||||
|
||||
@@ -37,6 +37,7 @@ enum class PlotKind {
|
||||
Effectiveness,
|
||||
Thresholds,
|
||||
MountsStatus,
|
||||
DatasetInventory,
|
||||
};
|
||||
|
||||
enum class GraphViewMode { Single, Compare, Overview };
|
||||
@@ -238,7 +239,7 @@ struct AppState {
|
||||
bool show_plot_legends = true;
|
||||
bool show_plot_markers = true;
|
||||
bool data_scientist_mode = false;
|
||||
bool show_all_charts = true;
|
||||
bool show_all_charts = false;
|
||||
float pulse_timer = 0.0f;
|
||||
int custom_grid_rows = 2;
|
||||
int custom_grid_columns = 2;
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace viz {
|
||||
namespace ui {
|
||||
void RenderMountsChart(AppState& state, const DataLoader& loader);
|
||||
void RenderKnowledgeGraphChart(AppState& state, const DataLoader& loader);
|
||||
void RenderDatasetInventoryChart(AppState& state, const DataLoader& loader);
|
||||
|
||||
|
||||
|
||||
@@ -265,6 +266,65 @@ void RenderTrainingLossChart(AppState& state, const DataLoader& loader) {
|
||||
ImPlot::PopStyleVar(6);
|
||||
}
|
||||
|
||||
void RenderDatasetInventoryChart(AppState& state, const DataLoader& loader) {
|
||||
RenderChartHeader(PlotKind::DatasetInventory,
|
||||
"DATASET INVENTORY",
|
||||
"Dataset sizes and file counts from the registry index.",
|
||||
state);
|
||||
|
||||
const auto& registry = loader.GetDatasetRegistry();
|
||||
if (registry.datasets.empty()) {
|
||||
ImGui::TextDisabled("No dataset registry data available");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<const char*> labels;
|
||||
std::vector<float> sizes_mb;
|
||||
std::vector<float> file_counts;
|
||||
std::vector<std::string> label_storage;
|
||||
labels.reserve(registry.datasets.size());
|
||||
sizes_mb.reserve(registry.datasets.size());
|
||||
file_counts.reserve(registry.datasets.size());
|
||||
label_storage.reserve(registry.datasets.size());
|
||||
|
||||
for (const auto& dataset : registry.datasets) {
|
||||
label_storage.push_back(dataset.name);
|
||||
sizes_mb.push_back(static_cast<float>(dataset.size_bytes) / (1024.0f * 1024.0f));
|
||||
file_counts.push_back(static_cast<float>(dataset.files.size()));
|
||||
}
|
||||
|
||||
for (const auto& label : label_storage) labels.push_back(label.c_str());
|
||||
|
||||
ImPlotFlags plot_flags = BasePlotFlags(state, true);
|
||||
ApplyPremiumPlotStyles("##DatasetInventory", state);
|
||||
if (ImPlot::BeginPlot("##DatasetInventory", ImGui::GetContentRegionAvail(), plot_flags)) {
|
||||
ImPlotAxisFlags axis_flags = static_cast<ImPlotAxisFlags>(GetPlotAxisFlags(state));
|
||||
ImPlot::SetupAxes("Dataset", "Size (MB)", axis_flags, axis_flags);
|
||||
ImPlot::SetupAxis(ImAxis_Y2, "Files", axis_flags);
|
||||
if (!labels.empty()) {
|
||||
ImPlot::SetupAxisTicks(ImAxis_X1, 0, static_cast<double>(labels.size() - 1),
|
||||
static_cast<int>(labels.size()), labels.data());
|
||||
}
|
||||
HandlePlotContextMenu(PlotKind::DatasetInventory, state);
|
||||
|
||||
ImPlot::SetNextFillStyle(GetSeriesColor(3), 0.75f);
|
||||
ImPlot::PlotBars("Size (MB)", sizes_mb.data(),
|
||||
static_cast<int>(sizes_mb.size()), 0.6f);
|
||||
|
||||
ImPlot::SetAxis(ImAxis_Y2);
|
||||
ImPlot::SetNextLineStyle(GetSeriesColor(6), 2.0f);
|
||||
if (state.show_plot_markers) {
|
||||
ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 4.0f, GetSeriesColor(6));
|
||||
}
|
||||
ImPlot::PlotLine("Files", file_counts.data(),
|
||||
static_cast<int>(file_counts.size()));
|
||||
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
ImPlot::PopStyleColor(2);
|
||||
ImPlot::PopStyleVar(6);
|
||||
}
|
||||
|
||||
void RenderRejectionChart(AppState& state, const DataLoader& loader) {
|
||||
RenderChartHeader(PlotKind::Rejections,
|
||||
"REJECTION REASONS",
|
||||
@@ -1075,6 +1135,9 @@ void RenderPlotByKind(PlotKind kind, AppState& state, const DataLoader& loader)
|
||||
case PlotKind::MountsStatus:
|
||||
RenderMountsChart(state, loader);
|
||||
break;
|
||||
case PlotKind::DatasetInventory:
|
||||
RenderDatasetInventoryChart(state, loader);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "graph_browser.h"
|
||||
#include "../core.h"
|
||||
#include "../../icons.h"
|
||||
#include "../../data_loader.h"
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
|
||||
@@ -15,6 +16,7 @@ void GraphBrowser::InitializeGraphRegistry() {
|
||||
// Training Category
|
||||
{PlotKind::TrainingLoss, "Training Loss", "Loss curves over training steps", GraphCategory::Training, true, true, true, true},
|
||||
{PlotKind::LossVsSamples, "Loss vs Samples", "Training loss progression by sample count", GraphCategory::Training, false, true, true, false},
|
||||
{PlotKind::DatasetInventory, "Dataset Inventory", "Dataset sizes and file counts", GraphCategory::Training, false, false, true, false},
|
||||
|
||||
// Quality Category
|
||||
{PlotKind::QualityTrends, "Quality Trends", "Data quality trends by domain", GraphCategory::Quality, true, true, true, true},
|
||||
@@ -93,7 +95,57 @@ const GraphInfo* GraphBrowser::GetGraphInfo(PlotKind kind) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void GraphBrowser::Render(AppState& state) {
|
||||
bool GraphBrowser::IsGraphAvailable(PlotKind kind,
|
||||
const AppState& state,
|
||||
const DataLoader& loader) const {
|
||||
switch (kind) {
|
||||
case PlotKind::QualityTrends:
|
||||
case PlotKind::QualityDirection:
|
||||
return !loader.GetQualityTrends().empty();
|
||||
case PlotKind::GeneratorEfficiency:
|
||||
case PlotKind::GeneratorMix:
|
||||
return !loader.GetGeneratorStats().empty();
|
||||
case PlotKind::CoverageDensity:
|
||||
case PlotKind::EmbeddingDensity:
|
||||
case PlotKind::EmbeddingQuality:
|
||||
case PlotKind::LatentSpace:
|
||||
return !loader.GetEmbeddingRegions().empty();
|
||||
case PlotKind::TrainingLoss:
|
||||
case PlotKind::LossVsSamples:
|
||||
return !loader.GetTrainingRuns().empty();
|
||||
case PlotKind::DomainCoverage:
|
||||
return !loader.GetCoverage().domain_coverage.empty();
|
||||
case PlotKind::Rejections:
|
||||
return !loader.GetRejectionSummary().reasons.empty();
|
||||
case PlotKind::EvalMetrics: {
|
||||
const auto& runs = loader.GetTrainingRuns();
|
||||
for (const auto& run : runs) {
|
||||
if (!run.eval_metrics.empty()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case PlotKind::Effectiveness:
|
||||
return !loader.GetOptimizationData().domain_effectiveness.empty() ||
|
||||
!loader.GetCoverage().domain_coverage.empty();
|
||||
case PlotKind::Thresholds:
|
||||
return !loader.GetOptimizationData().threshold_sensitivity.empty();
|
||||
case PlotKind::AgentThroughput:
|
||||
return !state.agents.empty();
|
||||
case PlotKind::MissionQueue:
|
||||
case PlotKind::MissionProgress:
|
||||
return !state.missions.empty();
|
||||
case PlotKind::MountsStatus:
|
||||
return !loader.GetMounts().empty();
|
||||
case PlotKind::KnowledgeGraph:
|
||||
return !loader.GetContextGraph().labels.empty();
|
||||
case PlotKind::DatasetInventory:
|
||||
return !loader.GetDatasetRegistry().datasets.empty();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void GraphBrowser::Render(AppState& state, const DataLoader& loader) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
|
||||
|
||||
// Search bar
|
||||
@@ -136,7 +188,17 @@ void GraphBrowser::Render(AppState& state) {
|
||||
for (auto kind : state.graph_bookmarks) {
|
||||
const GraphInfo* info = GetGraphInfo(kind);
|
||||
if (info) {
|
||||
bool available = IsGraphAvailable(info->kind, state, loader);
|
||||
if (!available && !state.show_all_charts) {
|
||||
continue;
|
||||
}
|
||||
if (!available && state.show_all_charts) {
|
||||
ImGui::BeginDisabled();
|
||||
RenderGraphItem(*info, state);
|
||||
ImGui::EndDisabled();
|
||||
} else {
|
||||
RenderGraphItem(*info, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +220,17 @@ void GraphBrowser::Render(AppState& state) {
|
||||
for (auto kind : recent_unique) {
|
||||
const GraphInfo* info = GetGraphInfo(kind);
|
||||
if (info) {
|
||||
bool available = IsGraphAvailable(info->kind, state, loader);
|
||||
if (!available && !state.show_all_charts) {
|
||||
continue;
|
||||
}
|
||||
if (!available && state.show_all_charts) {
|
||||
ImGui::BeginDisabled();
|
||||
RenderGraphItem(*info, state);
|
||||
ImGui::EndDisabled();
|
||||
} else {
|
||||
RenderGraphItem(*info, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,13 +242,31 @@ void GraphBrowser::Render(AppState& state) {
|
||||
ImGui::Spacing();
|
||||
|
||||
auto filtered = GetFilteredGraphs(state.browser_filter, state.graph_search_query);
|
||||
if (!state.show_all_charts) {
|
||||
filtered.erase(std::remove_if(filtered.begin(), filtered.end(),
|
||||
[&](const GraphInfo& graph) {
|
||||
return !IsGraphAvailable(graph.kind, state, loader);
|
||||
}),
|
||||
filtered.end());
|
||||
}
|
||||
|
||||
if (filtered.empty()) {
|
||||
if (state.show_all_charts) {
|
||||
ImGui::TextDisabled("No graphs found");
|
||||
} else {
|
||||
ImGui::TextDisabled("No graphs with data (toggle Show All Chart Windows to view empty graphs)");
|
||||
}
|
||||
} else {
|
||||
ImGui::BeginChild("GraphList", ImVec2(0, 0), false);
|
||||
for (const auto& graph : filtered) {
|
||||
bool available = IsGraphAvailable(graph.kind, state, loader);
|
||||
if (!available && state.show_all_charts) {
|
||||
ImGui::BeginDisabled();
|
||||
RenderGraphItem(graph, state);
|
||||
ImGui::EndDisabled();
|
||||
} else {
|
||||
RenderGraphItem(graph, state);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
#include <vector>
|
||||
#include "../../models/state.h"
|
||||
|
||||
namespace afs::viz {
|
||||
class DataLoader;
|
||||
}
|
||||
|
||||
namespace afs::viz::ui {
|
||||
|
||||
// Graph metadata for browser display
|
||||
@@ -23,17 +27,22 @@ public:
|
||||
GraphBrowser();
|
||||
|
||||
// Render the graph browser sidebar
|
||||
void Render(AppState& state);
|
||||
void Render(AppState& state, const DataLoader& loader);
|
||||
|
||||
// Get all available graphs
|
||||
const std::vector<GraphInfo>& GetAllGraphs() const { return all_graphs_; }
|
||||
|
||||
// Get filtered graphs based on category and search
|
||||
std::vector<GraphInfo> GetFilteredGraphs(GraphCategory category, const std::string& search) const;
|
||||
std::vector<GraphInfo> GetFilteredGraphs(GraphCategory category,
|
||||
const std::string& search) const;
|
||||
|
||||
// Get graph info by kind
|
||||
const GraphInfo* GetGraphInfo(PlotKind kind) const;
|
||||
|
||||
bool IsGraphAvailable(PlotKind kind,
|
||||
const AppState& state,
|
||||
const DataLoader& loader) const;
|
||||
|
||||
// Get category name
|
||||
static const char* GetCategoryName(GraphCategory category);
|
||||
|
||||
|
||||
@@ -245,6 +245,7 @@ const std::vector<PlotOption>& PlotOptions() {
|
||||
{PlotKind::Effectiveness, "Domain Effectiveness"},
|
||||
{PlotKind::Thresholds, "Threshold Sensitivity"},
|
||||
{PlotKind::MountsStatus, "Local Mounts Status"},
|
||||
{PlotKind::DatasetInventory, "Dataset Inventory"},
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user