apps: add studio sources

This commit is contained in:
scawful
2025-12-30 10:25:25 -05:00
parent 39b74ffed3
commit 5db5f87a68
75 changed files with 19943 additions and 1 deletions

160
apps/studio/CMakeLists.txt Normal file
View File

@@ -0,0 +1,160 @@
cmake_minimum_required(VERSION 3.15)
project(afs_studio LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# M1/ARM64 optimizations
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64")
add_compile_options(-march=armv8-a+simd+dotprod -O3 -ffast-math)
add_compile_definitions(AFS_ARM64=1)
endif()
# =============================================================================
# Dependencies
# =============================================================================
option(AFS_FETCH_GLFW "Fetch GLFW automatically if missing" ON)
# Fetch Dear ImGui
FetchContent_Declare(
imgui
GIT_REPOSITORY https://github.com/ocornut/imgui.git
GIT_TAG docking
)
FetchContent_MakeAvailable(imgui)
# Fetch ImPlot
FetchContent_Declare(
implot
GIT_REPOSITORY https://github.com/epezent/implot.git
GIT_TAG master
)
FetchContent_MakeAvailable(implot)
# Fetch nlohmann/json (header-only)
FetchContent_Declare(
json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
)
FetchContent_MakeAvailable(json)
# Find GLFW
find_package(glfw3 3.3 QUIET)
if(NOT glfw3_FOUND AND AFS_FETCH_GLFW)
message(STATUS "glfw3 not found - fetching with FetchContent")
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
glfw
GIT_REPOSITORY https://github.com/glfw/glfw.git
GIT_TAG 3.4
)
FetchContent_MakeAvailable(glfw)
set(glfw3_FOUND TRUE)
endif()
if(NOT glfw3_FOUND)
message(FATAL_ERROR "glfw3 not found. Install it or set AFS_FETCH_GLFW=ON.")
endif()
find_package(OpenGL REQUIRED)
# =============================================================================
# Build afs_studio
# =============================================================================
# ImGui sources
set(IMGUI_SOURCES
${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_tables.cpp
${imgui_SOURCE_DIR}/imgui_widgets.cpp
${imgui_SOURCE_DIR}/imgui_demo.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp
)
# ImPlot sources
set(IMPLOT_SOURCES
${implot_SOURCE_DIR}/implot.cpp
${implot_SOURCE_DIR}/implot_items.cpp
${implot_SOURCE_DIR}/implot_demo.cpp
)
# afs_studio application
add_executable(afs_studio
src/main.cc
src/app.cc
src/data_loader.cc
src/core/registry_reader.cc
src/core/training_monitor.cc
src/core/deployment_actions.cc
src/core/filesystem.cc
src/core/logger.cc
src/core/context.cc
src/core/assets.cc
src/core/llama_client.cc
src/ui/core.cc
src/ui/components/metrics.cc
src/ui/components/charts.cc
src/ui/components/tabs.cc
src/ui/components/panels.cc
src/ui/components/model_registry.cc
src/ui/components/training_dashboard.cc
src/ui/components/deployment_panel.cc
src/ui/components/comparison_view.cc
src/ui/components/graph_browser.cc
src/ui/components/graph_navigator.cc
src/ui/components/companion_panels.cc
src/ui/charts/quality_trends.cc
src/ui/charts/generator_efficiency.cc
src/ui/charts/coverage_density.cc
src/ui/panels/chat_panel.cc
src/ui/shortcuts.cc
src/widgets/text_editor.cc
src/widgets/sample_review.cc
${IMGUI_SOURCES}
${IMPLOT_SOURCES}
)
target_include_directories(afs_studio PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${imgui_SOURCE_DIR}
${imgui_SOURCE_DIR}/backends
${implot_SOURCE_DIR}
)
target_compile_definitions(afs_studio PRIVATE
IMGUI_IMPL_OPENGL_LOADER_GLAD=0
)
if(TARGET OpenGL::OpenGL)
set(AFS_OPENGL_TARGET OpenGL::OpenGL)
else()
set(AFS_OPENGL_TARGET OpenGL::GL)
endif()
target_link_libraries(afs_studio PRIVATE
glfw
${AFS_OPENGL_TARGET}
nlohmann_json::nlohmann_json
)
# macOS specific
if(APPLE)
target_link_libraries(afs_studio PRIVATE
"-framework Cocoa"
"-framework IOKit"
"-framework CoreVideo"
)
endif()
# Install
install(TARGETS afs_studio RUNTIME DESTINATION bin)
message(STATUS "afs_studio will be built")

33
apps/studio/README.md Normal file
View File

@@ -0,0 +1,33 @@
# afs_studio
Native C++17 visualization and training management application for AFS.
## Build
```bash
# From project root
cmake -B build -S . -DAFS_BUILD_STUDIO=ON
cmake --build build --target afs_studio
```
## Run
```bash
./build/apps/studio/afs_studio
```
## Features
- **Dashboard**: Training metrics overview
- **Analysis**: Quality score trends, domain breakdown
- **Training Hub**: Real-time training status
- **Sample Review**: Data quality inspection
- **Text Editor**: Built-in code editor
- **Shortcut System**: Customizable keyboard shortcuts (Ctrl+/)
## Dependencies (auto-fetched)
- Dear ImGui (docking branch)
- ImPlot
- GLFW
- nlohmann/json

584
apps/studio/src/app.cc Normal file
View File

@@ -0,0 +1,584 @@
#include "app.h"
#include "core/logger.h"
#include "core/context.h"
#include "core/assets.h"
#include "ui/panels/chat_panel.h"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cfloat>
#include <cmath>
#include <cstdio>
#include <fstream>
#include <cstring>
#include <vector>
// GLFW + OpenGL
#define GL_SILENCE_DEPRECATION
#include <GLFW/glfw3.h>
// Dear ImGui
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "imgui_internal.h"
#include "implot.h"
#include "themes/afs_theme.h"
#include "icons.h"
// Modular Components
#include "ui/core.h"
#include "ui/components/metrics.h"
#include "ui/components/charts.h"
#include "ui/components/tabs.h"
#include "ui/components/panels.h"
namespace afs {
namespace viz {
App::App(const std::string& data_path)
: data_path_(data_path), loader_(data_path) {
LOG_INFO("AFS Studio initialize with data path: " + data_path);
std::snprintf(state_.new_agent_role.data(), state_.new_agent_role.size(), "Evaluator");
std::snprintf(state_.new_mission_owner.data(), state_.new_mission_owner.size(), "Ops");
std::snprintf(state_.system_prompt.data(), state_.system_prompt.size(),
"You are a AFS data science assistant. Analyze the training trends and suggest optimizations.");
const char* home = std::getenv("HOME");
state_.current_browser_path = home ? std::filesystem::path(home) : std::filesystem::current_path();
// Initialize LlamaClient with default config
LlamaConfig llama_config;
llama_config.llama_cli_path = "~/llama.cpp/build/bin/llama-cli";
llama_config.model_path = "~/llama.cpp/models/tinyllama-1.1b.Q4_K_M.gguf";
llama_config.rpc_servers = "100.104.53.21:50052";
llama_config.use_rpc = true;
llama_config.context_size = 4096;
llama_config.n_predict = 256;
llama_config.temperature = 0.7f;
llama_client_.SetConfig(llama_config);
llama_client_.CheckHealth();
ui::RefreshBrowserEntries(state_);
SeedDefaultState();
// Create graphics context
context_ = std::make_unique<studio::core::GraphicsContext>("AFS Studio", 1400, 900);
if (context_->IsValid()) {
fonts_ = studio::core::AssetLoader::LoadFonts();
themes::ApplyHafsTheme();
shortcut_manager_.LoadFromDisk();
// Better default heights for richness
state_.chart_height = 220.0f;
state_.plot_height = 220.0f;
state_.chart_columns = 2;
} else {
LOG_ERROR("Failed to initialize graphics context");
}
}
int App::Run() {
if (!context_ || !context_->IsValid()) return 1;
RefreshData("startup");
double last_time = glfwGetTime();
while (!context_->ShouldClose()) {
context_->PollEvents();
double current_time = glfwGetTime();
float dt = static_cast<float>(current_time - last_time);
last_time = current_time;
TickSimulatedMetrics(dt);
if (state_.auto_refresh && (current_time - state_.last_refresh_time > state_.refresh_interval_sec)) {
RefreshData("auto");
}
if (state_.should_refresh) {
RefreshData("manual");
state_.should_refresh = false;
}
RenderFrame();
}
return 0;
}
void App::RefreshData(const char* reason) {
bool ok = loader_.Refresh();
state_.last_refresh_time = glfwGetTime();
SyncDataBackedState();
const auto& status = loader_.GetLastStatus();
std::string msg;
if (status.error_count > 0) {
msg = "Data refreshed with errors (";
msg += reason;
msg += "): ";
msg += status.last_error.empty() ? "see logs" : status.last_error;
} else if (!status.AnyOk() && !status.FoundCount()) {
msg = "No data sources found (" + std::string(reason) + ")";
} else if (!ok) {
msg = "Data refresh failed (" + std::string(reason) + ")";
} else {
msg = "Data refreshed (" + std::string(reason) + ")";
}
ui::AppendLog(state_, "system", msg, "system");
LOG_INFO(msg);
// Sync domain visibility
for (const auto& [domain, visible] : loader_.GetDomainVisibility()) {
if (state_.domain_visibility.find(domain) == state_.domain_visibility.end()) {
state_.domain_visibility[domain] = visible;
}
}
}
void App::SyncDataBackedState() {
const auto& coverage = loader_.GetCoverage();
const auto& trends = loader_.GetQualityTrends();
const auto& runs = loader_.GetTrainingRuns();
const auto& generators = loader_.GetGeneratorStats();
// Sync Agents
auto* indexer = ui::FindAgentByName(state_.agents, "Region Indexer");
if (!indexer) {
state_.agents.emplace_back();
indexer = &state_.agents.back();
indexer->name = "Region Indexer";
indexer->role = "Librarian";
}
indexer->data_backed = true;
indexer->enabled = true;
indexer->tasks_completed = coverage.total_samples;
indexer->queue_depth = coverage.sparse_regions;
indexer->success_rate = ui::Clamp01(coverage.coverage_score);
indexer->status = indexer->queue_depth > 0 ? "Busy" : "Idle";
float quality_mean = 0.0f;
int insufficient = 0;
for (const auto& trend : trends) {
quality_mean += trend.mean;
if (trend.trend_direction == "insufficient") ++insufficient;
}
if (!trends.empty()) quality_mean /= static_cast<float>(trends.size());
auto* evaluator = ui::FindAgentByName(state_.agents, "Quality Monitor");
if (!evaluator) {
state_.agents.emplace_back();
evaluator = &state_.agents.back();
evaluator->name = "Quality Monitor";
evaluator->role = "Evaluator";
}
evaluator->data_backed = true;
evaluator->enabled = true;
evaluator->tasks_completed = static_cast<int>(trends.size());
evaluator->queue_depth = insufficient;
evaluator->success_rate = ui::Clamp01(quality_mean);
evaluator->status = evaluator->queue_depth > 0 ? "Review" : "Idle";
float avg_loss = 0.0f;
for (const auto& run : runs) avg_loss += run.final_loss;
if (!runs.empty()) avg_loss /= static_cast<float>(runs.size());
auto* trainer = ui::FindAgentByName(state_.agents, "Trainer Coordinator");
if (!trainer) {
state_.agents.emplace_back();
trainer = &state_.agents.back();
trainer->name = "Trainer Coordinator";
trainer->role = "Trainer";
}
trainer->data_backed = true;
trainer->enabled = true;
trainer->tasks_completed = static_cast<int>(runs.size());
trainer->success_rate = avg_loss > 0.0f ? ui::Clamp01(1.0f / (1.0f + avg_loss)) : 0.0f;
trainer->status = "Active";
// Sync Missions
state_.missions.erase(std::remove_if(state_.missions.begin(), state_.missions.end(), [](const MissionState& m) { return m.data_backed; }), state_.missions.end());
for (const auto& run : runs) {
MissionState mission;
mission.data_backed = true;
mission.owner = run.model_name.empty() ? "Trainer" : run.model_name;
mission.name = run.run_id.size() > 12 ? run.run_id.substr(0, 12) : run.run_id;
mission.status = "Complete";
mission.priority = run.final_loss > avg_loss ? 4 : 3;
mission.progress = 1.0f;
state_.missions.push_back(std::move(mission));
}
}
void App::SeedDefaultState() {
ui::AppendLog(state_, "system", "AFS Studio environment ready.", "system");
state_.sparkline_data.resize(30, 0.0f);
for (float& f : state_.sparkline_data) f = (float)(rand() % 100) / 100.0f;
}
void App::TickSimulatedMetrics(float dt) {
state_.pulse_timer += dt;
if (!state_.simulate_activity) return;
for (auto& agent : state_.agents) {
if (agent.data_backed || !agent.enabled) continue;
agent.activity_phase += dt * (0.5f + (float)(rand() % 100) / 100.0f);
agent.cpu_pct = 20.0f + 15.0f * (1.0f + sinf(agent.activity_phase));
agent.mem_pct = 15.0f + 5.0f * (1.0f + cosf(agent.activity_phase * 0.7f));
}
}
void App::RenderFrame() {
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
const ImGuiIO& io = ImGui::GetIO();
if (shortcut_manager_.IsTriggered(ui::ActionId::Refresh, io)) state_.should_refresh = true;
// Graph View Navigation Shortcuts
if (shortcut_manager_.IsTriggered(ui::ActionId::ToggleGraphBrowser, io)) {
state_.show_graph_browser = !state_.show_graph_browser;
}
if (shortcut_manager_.IsTriggered(ui::ActionId::ToggleCompanionPanels, io)) {
state_.show_companion_panels = !state_.show_companion_panels;
}
if (shortcut_manager_.IsTriggered(ui::ActionId::NavigateBack, io)) {
graph_navigator_.NavigateBack(state_);
}
if (shortcut_manager_.IsTriggered(ui::ActionId::NavigateForward, io)) {
graph_navigator_.NavigateForward(state_);
}
if (shortcut_manager_.IsTriggered(ui::ActionId::BookmarkGraph, io)) {
if (state_.active_graph != PlotKind::None) {
graph_navigator_.ToggleBookmark(state_, state_.active_graph);
}
}
// New: Layout Presets
if (ImGui::IsKeyPressed(ImGuiKey_F1)) { state_.layout_preset = 0; state_.force_reset_layout = true; }
if (ImGui::IsKeyPressed(ImGuiKey_F2)) { state_.layout_preset = 1; state_.force_reset_layout = true; }
if (ImGui::IsKeyPressed(ImGuiKey_F3)) { state_.layout_preset = 2; state_.force_reset_layout = true; }
auto refresh_cb = [this](const char* reason) { state_.should_refresh = true; };
auto quit_cb = [this]() { glfwSetWindowShouldClose(context_->GetWindow(), true); };
ui::RenderMenuBar(state_, refresh_cb, quit_cb, shortcut_manager_, &show_sample_review_, &show_shortcuts_window_);
RenderLayout();
if (show_sample_review_) sample_review_.Render(&show_sample_review_);
ui::RenderShortcutsWindow(shortcut_manager_, &show_shortcuts_window_);
shortcut_manager_.SaveIfDirty();
RenderExpandedPlot();
RenderFloaters();
if (state_.show_demo_window) {
ImGui::ShowDemoWindow(&state_.show_demo_window);
ImPlot::ShowDemoWindow();
}
// Finalize ImGui Frame
ImGui::Render();
int w, h;
glfwGetFramebufferSize(context_->GetWindow(), &w, &h);
glViewport(0, 0, w, h);
glClearColor(0.07f, 0.07f, 0.09f, 1.00f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
GLFWwindow* backup_current_context = glfwGetCurrentContext();
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
glfwMakeContextCurrent(backup_current_context);
}
context_->SwapBuffers();
}
void App::RenderLayout() {
bool docking_active = ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_DockingEnable;
if (docking_active) {
ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
ImGuiViewport* viewport = ImGui::GetMainViewport();
float status_bar_height = state_.show_status_strip ? 24.0f : 0.0f;
ImVec2 dockspace_size = ImVec2(viewport->WorkSize.x, viewport->WorkSize.y - status_bar_height);
if (state_.force_reset_layout || !ImGui::DockBuilderGetNode(dockspace_id)) {
state_.force_reset_layout = false;
ImGui::DockBuilderRemoveNode(dockspace_id);
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGuiID dock_main_id = dockspace_id;
// Single-graph view layout:
// Left: Graph Browser (15%)
// Center: Active Graph View (60%)
// Right: Companion Panels (25%)
ImGuiID dock_left_id = ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Left, 0.18f, nullptr, &dock_main_id);
ImGuiID dock_right_id = ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Right, 0.25f, nullptr, &dock_main_id);
// Dock assignments
ImGui::DockBuilderDockWindow("GraphBrowser", dock_left_id);
ImGui::DockBuilderDockWindow("GraphView", dock_main_id);
ImGui::DockBuilderDockWindow("CompanionPanels", dock_right_id);
ImGui::DockBuilderFinish(dockspace_id);
}
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground;
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(dockspace_size);
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin("MainDockSpaceHost", nullptr, window_flags);
ImGui::PopStyleVar(3);
ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None);
ImGui::End();
// Graph Browser - Left sidebar
if (state_.show_graph_browser) {
ImGui::Begin("GraphBrowser", &state_.show_graph_browser,
ImGuiWindowFlags_NoCollapse);
graph_browser_.Render(state_);
ImGui::End();
}
// Graph View - Center content area
ImGui::Begin("GraphView", nullptr,
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
// Navigation toolbar
graph_navigator_.RenderToolbar(state_, graph_browser_);
ImGui::Separator();
// Render active graph or prompt
if (state_.active_graph != PlotKind::None) {
ImGui::BeginChild("GraphContent", ImVec2(0, 0), false,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ui::RenderPlotByKind(state_.active_graph, state_, loader_);
ImGui::EndChild();
} else {
ImGui::Dummy(ImVec2(0, 100));
ImGui::TextDisabled("Select a graph from the browser to begin");
}
ImGui::End();
// Companion Panels - Right sidebar
if (state_.show_companion_panels && state_.active_graph != PlotKind::None) {
ImGui::Begin("CompanionPanels", &state_.show_companion_panels,
ImGuiWindowFlags_NoCollapse);
companion_panels_.Render(state_, loader_);
ImGui::End();
}
// Render Status Bar as a fixed window at the very bottom
if (state_.show_status_strip) {
ImGui::SetNextWindowPos(ImVec2(viewport->WorkPos.x, viewport->WorkPos.y + viewport->WorkSize.y - status_bar_height));
ImGui::SetNextWindowSize(ImVec2(viewport->WorkSize.x, status_bar_height));
ImGui::SetNextWindowViewport(viewport->ID);
ImGuiWindowFlags status_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNav;
ImGui::Begin("StatusBar", nullptr, status_flags);
ui::RenderStatusBar(state_, loader_, data_path_);
ImGui::End();
}
}
// Legacy panels (keeping for backwards compatibility, can be removed later)
if (state_.show_inspector && state_.active_graph == PlotKind::None) {
ImGui::Begin("InspectorPanel", &state_.show_inspector);
ui::RenderInspectorPanel(state_, loader_, fonts_.header, data_path_);
ImGui::End();
}
if (state_.show_dataset_panel) {
ImGui::Begin("DatasetPanel", &state_.show_dataset_panel);
ui::RenderDatasetPanel(state_, loader_);
ImGui::End();
}
// New Chat Panel Viewport
if (state_.show_chat_panel) {
ImGui::Begin("ChatPanel", &state_.show_chat_panel);
ui::RenderChatPanel(state_, llama_client_, [this](const std::string& a, const std::string& m, const std::string& k) {
ui::AppendLog(state_, a, m, k);
});
ImGui::End();
}
// Legacy modular chart panels (deprecated in favor of graph view)
if (state_.show_quality_trends) {
if (ImGui::Begin("Quality Trends", &state_.show_quality_trends)) {
quality_trends_chart_.Render(state_, loader_);
}
ImGui::End();
}
if (state_.show_generator_efficiency) {
if (ImGui::Begin("Generator Efficiency", &state_.show_generator_efficiency)) {
generator_efficiency_chart_.Render(state_, loader_);
}
ImGui::End();
}
if (state_.show_coverage_density) {
if (ImGui::Begin("Coverage Density", &state_.show_coverage_density)) {
coverage_density_chart_.Render(state_, loader_);
}
ImGui::End();
}
// Removed old workspace content window - graph view replaces it
}
void App::RenderDashboardView() {
ui::RenderSummaryRow(state_, loader_, fonts_.ui, fonts_.header);
ImGui::Spacing();
if (state_.focus_chart != PlotKind::None) {
// Focus Mode Layout
float focus_height = ImGui::GetContentRegionAvail().y * 0.65f;
if (focus_height < 400.0f) focus_height = 400.0f;
ImGui::BeginChild("FocusArea", ImVec2(0, focus_height), true);
ui::RenderPlotByKind(state_.focus_chart, state_, loader_);
ImGui::EndChild();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Secondary Charts in a scrollable horizontal row or smaller grid
ImGui::BeginChild("SecondaryCharts", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysHorizontalScrollbar);
static const std::vector<PlotKind> dashboard_plots = {
PlotKind::QualityTrends, PlotKind::GeneratorEfficiency,
PlotKind::CoverageDensity, PlotKind::TrainingLoss,
PlotKind::AgentThroughput, PlotKind::LatentSpace
};
if (ImGui::BeginTable("SecondaryGrid", (int)dashboard_plots.size(), ImGuiTableFlags_SizingFixedFit)) {
for (auto kind : dashboard_plots) {
if (kind == state_.focus_chart) continue;
ImGui::TableNextColumn();
ImGui::BeginChild(std::string("Sec" + std::to_string((int)kind)).c_str(), ImVec2(350, 200), true);
ui::RenderPlotByKind(kind, state_, loader_);
ImGui::EndChild();
}
ImGui::EndTable();
}
ImGui::EndChild();
} else {
// Standard Grid Layout
int columns = state_.chart_columns;
if (ImGui::BeginTable("DashboardGrid", columns, ImGuiTableFlags_Resizable | ImGuiTableFlags_Hideable)) {
ImGui::TableNextColumn(); ui::RenderQualityChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderGeneratorChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderCoverageChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderTrainingChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderAgentThroughputChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderLatentSpaceChart(state_, loader_);
ImGui::EndTable();
}
}
}
void App::RenderAnalysisView() {
if (ImGui::BeginTable("AnalysisGrid", 2, ImGuiTableFlags_Resizable)) {
ImGui::TableNextColumn(); ui::RenderQualityChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderTrainingLossChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderGeneratorMixChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderEmbeddingQualityChart(state_, loader_);
ImGui::EndTable();
}
}
void App::RenderOptimizationView() {
if (ImGui::BeginTable("OptimizationGrid", 2, ImGuiTableFlags_Resizable)) {
ImGui::TableNextColumn(); ui::RenderEffectivenessChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderThresholdOptimizationChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderRejectionChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderDomainCoverageChart(state_, loader_);
ImGui::EndTable();
}
}
void App::RenderSystemsView() {
if (ImGui::BeginTable("SystemsGrid", 2, ImGuiTableFlags_Resizable)) {
ImGui::TableNextColumn(); ui::RenderAgentUtilizationChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderMissionProgressChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderMissionQueueChart(state_, loader_);
ImGui::TableNextColumn(); ui::RenderAgentThroughputChart(state_, loader_);
ImGui::EndTable();
}
}
void App::RenderCustomGridView() {
ui::RenderComparisonView(state_, loader_, fonts_.ui, fonts_.header);
}
void App::RenderTrainingView() {
if (ImGui::BeginTabBar("TrainingTabs")) {
if (ImGui::BeginTabItem("Dashboard")) { RenderDashboardView(); ImGui::EndTabItem(); }
if (ImGui::BeginTabItem("Remote Training")) { training_dashboard_widget_.Render(); ImGui::EndTabItem(); }
if (ImGui::BeginTabItem("Agents")) { ui::RenderAgentsTab(state_, fonts_.ui, fonts_.header, nullptr); ImGui::EndTabItem(); }
if (ImGui::BeginTabItem("Missions")) { ui::RenderMissionsTab(state_, nullptr); ImGui::EndTabItem(); }
if (ImGui::BeginTabItem("Services")) { ui::RenderServicesTab(state_, nullptr); ImGui::EndTabItem(); }
if (ImGui::BeginTabItem("Tables")) { ui::RenderTablesTab(state_, loader_); ImGui::EndTabItem(); }
ImGui::EndTabBar();
}
}
void App::RenderContextView() {
ui::RenderContextTab(state_, text_editor_, memory_editor_, nullptr);
}
void App::RenderModelsView() {
model_registry_widget_.Render();
}
void App::RenderExpandedPlot() {
if (state_.expanded_plot == PlotKind::None) return;
ImGui::OpenPopup("Expanded Plot");
if (ImGui::BeginPopupModal("Expanded Plot", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ui::RenderPlotByKind(state_.expanded_plot, state_, loader_);
if (ImGui::Button("Close")) state_.expanded_plot = PlotKind::None;
ImGui::EndPopup();
}
}
void App::RenderFloaters() {
auto it = state_.active_floaters.begin();
while (it != state_.active_floaters.end()) {
PlotKind kind = *it;
std::string title = std::string("Floater##") + std::to_string(static_cast<int>(kind));
bool open = true;
ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
if (ImGui::Begin(title.c_str(), &open)) {
ui::RenderPlotByKind(kind, state_, loader_);
}
ImGui::End();
if (!open) {
it = state_.active_floaters.erase(it);
} else {
++it;
}
}
}
} // namespace viz
} // namespace afs

98
apps/studio/src/app.h Normal file
View File

@@ -0,0 +1,98 @@
#pragma once
#include <filesystem>
#include <memory>
#include <string>
#include <functional>
#include <imgui.h>
#include "data_loader.h"
#include "models/state.h"
#include "core/context.h"
#include "core/assets.h"
#include "ui/shortcuts.h"
#include "ui/components/model_registry.h"
#include "ui/components/training_dashboard.h"
#include "widgets/text_editor.h"
#include "widgets/imgui_memory_editor.h"
#include "widgets/sample_review.h"
#include "ui/charts/quality_trends.h"
#include "ui/charts/generator_efficiency.h"
#include "ui/charts/coverage_density.h"
#include "ui/components/graph_browser.h"
#include "ui/components/graph_navigator.h"
#include "ui/components/companion_panels.h"
#include "core/llama_client.h"
namespace afs {
namespace viz {
class App {
public:
explicit App(const std::string& data_path);
~App() = default;
int Run();
DataLoader& loader() { return loader_; }
const DataLoader& loader() const { return loader_; }
private:
void RefreshData(const char* reason);
void SeedDefaultState();
void SyncDataBackedState();
void TickSimulatedMetrics(float dt);
void RenderFrame();
void RenderLayout();
// Workspace Views
void RenderDashboardView();
void RenderAnalysisView();
void RenderOptimizationView();
void RenderSystemsView();
void RenderCustomGridView();
void RenderChatView();
void RenderTrainingView();
void RenderContextView();
void RenderModelsView();
void RenderExpandedPlot();
void RenderFloaters();
// Infrastructure
std::string data_path_;
DataLoader loader_;
AppState state_;
std::unique_ptr<studio::core::GraphicsContext> context_;
// Editors & Widgets
TextEditor text_editor_;
MemoryEditorWidget memory_editor_;
SampleReviewWidget sample_review_;
ui::ShortcutManager shortcut_manager_;
studio::ui::ModelRegistryWidget model_registry_widget_;
studio::ui::TrainingDashboardWidget training_dashboard_widget_;
// Modular Charts
ui::QualityTrendsChart quality_trends_chart_;
ui::GeneratorEfficiencyChart generator_efficiency_chart_;
ui::CoverageDensityChart coverage_density_chart_;
// Graph View System
ui::GraphBrowser graph_browser_;
ui::GraphNavigator graph_navigator_;
ui::CompanionPanels companion_panels_;
// LLM Chat Client
LlamaClient llama_client_;
// State flags
bool show_sample_review_ = false;
bool show_shortcuts_window_ = false;
// Typography
studio::core::AssetLoader::Fonts fonts_;
};
} // namespace viz
} // namespace afs

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,68 @@
#include "assets.h"
#include "logger.h"
#include "icons.h"
#include <vector>
namespace afs {
namespace studio {
namespace core {
AssetLoader::Fonts AssetLoader::LoadFonts() {
ImGuiIO& io = ImGui::GetIO();
std::filesystem::path font_dir = FindFontDir();
if (font_dir.empty()) {
LOG_WARN("Could not find font directory, using default ImGui font.");
return Fonts{};
}
Fonts fonts;
float base_size = 15.0f;
float header_size = 18.0f;
float mono_size = 14.0f;
auto MergeIcons = [&](float size) {
static const ImWchar icons_ranges[] = { (ImWchar)ICON_MIN_MD, (ImWchar)0xFFFF, 0 };
ImFontConfig icons_config;
icons_config.MergeMode = true;
icons_config.PixelSnapH = true;
icons_config.GlyphMinAdvanceX = size;
return io.Fonts->AddFontFromFileTTF((font_dir / "MaterialIcons-Regular.ttf").string().c_str(), size, &icons_config, icons_ranges);
};
fonts.ui = io.Fonts->AddFontFromFileTTF((font_dir / "Karla-Regular.ttf").string().c_str(), base_size);
MergeIcons(base_size);
fonts.header = io.Fonts->AddFontFromFileTTF((font_dir / "Roboto-Medium.ttf").string().c_str(), header_size);
MergeIcons(header_size);
fonts.mono = io.Fonts->AddFontFromFileTTF((font_dir / "Cousine-Regular.ttf").string().c_str(), mono_size);
MergeIcons(mono_size);
fonts.icons = fonts.ui;
LOG_INFO("AssetLoader: Successfully loaded fonts from " + font_dir.string());
return fonts;
}
std::filesystem::path AssetLoader::FindFontDir() {
std::filesystem::path current = std::filesystem::current_path();
std::vector<std::filesystem::path> search_paths = {
current / "assets" / "font",
current / "src" / "assets" / "font",
current / ".." / ".." / ".." / "apps" / "studio" / "src" / "assets" / "font",
current / ".." / ".." / "apps" / "studio" / "src" / "assets" / "font"
};
for (const auto& path : search_paths) {
if (std::filesystem::exists(path / "Roboto-Medium.ttf")) {
return path;
}
}
return "";
}
} // namespace core
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,28 @@
#pragma once
#include <string>
#include <filesystem>
#include <imgui.h>
namespace afs {
namespace studio {
namespace core {
class AssetLoader {
public:
struct Fonts {
ImFont* ui = nullptr;
ImFont* header = nullptr;
ImFont* mono = nullptr;
ImFont* icons = nullptr;
};
static Fonts LoadFonts();
private:
static std::filesystem::path FindFontDir();
};
} // namespace core
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,94 @@
#include "context.h"
#include "logger.h"
#define GL_SILENCE_DEPRECATION
#include <GLFW/glfw3.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <implot.h>
namespace afs {
namespace studio {
namespace core {
namespace {
void GlfwErrorCallback(int error, const char* description) {
LOG_ERROR("GLFW Error " + std::to_string(error) + ": " + description);
}
} // namespace
GraphicsContext::GraphicsContext(const std::string& title, int width, int height) {
if (!InitGLFW(title, width, height)) return;
if (!InitImGui()) return;
}
GraphicsContext::~GraphicsContext() {
Shutdown();
}
bool GraphicsContext::InitGLFW(const std::string& title, int width, int height) {
glfwSetErrorCallback(GlfwErrorCallback);
if (!glfwInit()) return false;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
window_ = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
if (!window_) {
LOG_ERROR("Failed to create GLFW window.");
glfwTerminate();
return false;
}
glfwMakeContextCurrent(window_);
glfwSwapInterval(1);
return true;
}
bool GraphicsContext::InitImGui() {
IMGUI_CHECKVERSION();
imgui_ctx_ = ImGui::CreateContext();
implot_ctx_ = ImPlot::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
if (!ImGui_ImplGlfw_InitForOpenGL(window_, true)) return false;
if (!ImGui_ImplOpenGL3_Init("#version 150")) return false;
return true;
}
bool GraphicsContext::ShouldClose() const {
return window_ ? glfwWindowShouldClose(window_) : true;
}
void GraphicsContext::SwapBuffers() {
if (window_) glfwSwapBuffers(window_);
}
void GraphicsContext::PollEvents() {
glfwPollEvents();
}
void GraphicsContext::Shutdown() {
if (imgui_ctx_) {
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImPlot::DestroyContext(implot_ctx_);
ImGui::DestroyContext(imgui_ctx_);
}
if (window_) {
glfwDestroyWindow(window_);
glfwTerminate();
}
}
} // namespace core
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,37 @@
#pragma once
#include <string>
#include <imgui.h>
struct GLFWwindow;
struct ImPlotContext;
namespace afs {
namespace studio {
namespace core {
class GraphicsContext {
public:
GraphicsContext(const std::string& title, int width, int height);
~GraphicsContext();
bool IsValid() const { return window_ != nullptr; }
GLFWwindow* GetWindow() const { return window_; }
bool ShouldClose() const;
void SwapBuffers();
void PollEvents();
private:
bool InitGLFW(const std::string& title, int width, int height);
bool InitImGui();
void Shutdown();
GLFWwindow* window_ = nullptr;
ImGuiContext* imgui_ctx_ = nullptr;
ImPlotContext* implot_ctx_ = nullptr;
};
} // namespace core
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,207 @@
#include "deployment_actions.h"
#include <array>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <memory>
namespace afs {
namespace studio {
DeploymentActions::DeploymentActions() {
// Find afs CLI
const char* home = std::getenv("HOME");
if (home) {
std::filesystem::path path(home);
// Try common locations
std::vector<std::filesystem::path> candidates = {
path / "Code" / "afs" / ".venv" / "bin" / "afs",
path / ".local" / "bin" / "afs",
std::filesystem::path("/usr/local/bin/afs"),
};
for (const auto& p : candidates) {
if (std::filesystem::exists(p)) {
afs_cli_path_ = p.string();
break;
}
}
// Find llama.cpp
llama_cpp_path_ = (path / "Code" / "llama.cpp").string();
}
}
ActionResult DeploymentActions::ExecuteCommand(
const std::vector<std::string>& args, int timeout_seconds) {
ActionResult result;
result.status = ActionStatus::kRunning;
// Build command string
std::string cmd;
for (const auto& arg : args) {
if (!cmd.empty()) cmd += " ";
// Simple shell escaping
if (arg.find(' ') != std::string::npos) {
cmd += "\"" + arg + "\"";
} else {
cmd += arg;
}
}
cmd += " 2>&1";
// Execute command
std::array<char, 4096> buffer;
std::string output;
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) {
result.status = ActionStatus::kFailed;
result.error = "Failed to execute command: " + cmd;
return result;
}
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
output += buffer.data();
}
int ret = pclose(pipe);
result.exit_code = WEXITSTATUS(ret);
result.output = output;
if (result.exit_code == 0) {
result.status = ActionStatus::kCompleted;
result.progress = 1.0f;
} else {
result.status = ActionStatus::kFailed;
result.error = "Command failed with exit code " +
std::to_string(result.exit_code);
}
return result;
}
ActionResult DeploymentActions::PullModel(const std::string& model_id,
const std::string& source,
ProgressCallback callback) {
if (afs_cli_path_.empty()) {
ActionResult result;
result.status = ActionStatus::kFailed;
result.error = "afs CLI not found";
return result;
}
std::vector<std::string> args = {afs_cli_path_, "models", "pull", model_id};
if (source != "auto") {
args.push_back("--source");
args.push_back(source);
}
if (callback) callback("Pulling model...", 0.1f);
auto result = ExecuteCommand(args, 600); // 10 minute timeout
if (callback) callback("Pull complete", 1.0f);
return result;
}
ActionResult DeploymentActions::ConvertToGGUF(const std::string& model_id,
const std::string& quantization,
ProgressCallback callback) {
if (afs_cli_path_.empty()) {
ActionResult result;
result.status = ActionStatus::kFailed;
result.error = "afs CLI not found";
return result;
}
std::vector<std::string> args = {afs_cli_path_, "models", "convert",
model_id, "gguf"};
if (!quantization.empty()) {
args.push_back("--quant");
args.push_back(quantization);
}
if (callback) callback("Converting to GGUF...", 0.2f);
auto result = ExecuteCommand(args, 1800); // 30 minute timeout
if (callback) callback("Conversion complete", 1.0f);
return result;
}
ActionResult DeploymentActions::DeployToOllama(const std::string& model_id,
const std::string& ollama_name,
const std::string& quantization,
ProgressCallback callback) {
if (afs_cli_path_.empty()) {
ActionResult result;
result.status = ActionStatus::kFailed;
result.error = "afs CLI not found";
return result;
}
std::vector<std::string> args = {afs_cli_path_, "models", "deploy",
model_id, "ollama"};
if (!ollama_name.empty()) {
args.push_back("--name");
args.push_back(ollama_name);
}
if (!quantization.empty()) {
args.push_back("--quant");
args.push_back(quantization);
}
if (callback) callback("Deploying to Ollama...", 0.3f);
auto result = ExecuteCommand(args, 1800);
if (callback) callback("Deployment complete", 1.0f);
return result;
}
ActionResult DeploymentActions::TestModel(const std::string& model_id,
DeploymentBackend backend,
const std::string& test_prompt) {
if (afs_cli_path_.empty()) {
ActionResult result;
result.status = ActionStatus::kFailed;
result.error = "afs CLI not found";
return result;
}
std::string backend_str;
switch (backend) {
case DeploymentBackend::kOllama:
backend_str = "ollama";
break;
case DeploymentBackend::kLlamaCpp:
backend_str = "llama.cpp";
break;
case DeploymentBackend::kHalextNode:
backend_str = "halext-node";
break;
}
std::vector<std::string> args = {afs_cli_path_, "models", "test", model_id,
backend_str};
return ExecuteCommand(args, 60); // 1 minute timeout
}
bool DeploymentActions::IsOllamaRunning() const {
// Check if Ollama is running
FILE* pipe = popen("pgrep -x ollama 2>/dev/null", "r");
if (!pipe) return false;
char buffer[128];
bool running = fgets(buffer, sizeof(buffer), pipe) != nullptr;
pclose(pipe);
return running;
}
bool DeploymentActions::IsLlamaCppAvailable() const {
return !llama_cpp_path_.empty() &&
std::filesystem::exists(llama_cpp_path_ + "/llama-quantize");
}
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,101 @@
#pragma once
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <vector>
namespace afs {
namespace studio {
// Result of an async action
enum class ActionStatus {
kPending,
kRunning,
kCompleted,
kFailed
};
struct ActionResult {
ActionStatus status = ActionStatus::kPending;
std::string output;
std::string error;
int exit_code = 0;
float progress = 0.0f;
};
// Deployment target backends
enum class DeploymentBackend {
kOllama,
kLlamaCpp,
kHalextNode
};
// Quantization options for GGUF conversion
struct QuantizationOption {
std::string name;
std::string description;
float size_ratio; // Relative to F16
};
// Available quantization options
inline std::vector<QuantizationOption> GetQuantizationOptions() {
return {
{"Q3_K_S", "Smallest size, reduced quality", 0.35f},
{"Q4_K_M", "Good balance (recommended)", 0.45f},
{"Q5_K_M", "Better quality, larger size", 0.55f},
{"Q8_0", "Best quality, largest size", 0.65f},
{"F16", "Full precision (no quantization)", 1.0f},
};
}
// Deployment actions - executes afs CLI commands
class DeploymentActions {
public:
using ProgressCallback = std::function<void(const std::string&, float)>;
DeploymentActions();
// Pull model from remote location
ActionResult PullModel(const std::string& model_id,
const std::string& source = "auto",
ProgressCallback callback = nullptr);
// Convert model to GGUF format
ActionResult ConvertToGGUF(const std::string& model_id,
const std::string& quantization = "Q4_K_M",
ProgressCallback callback = nullptr);
// Deploy to Ollama
ActionResult DeployToOllama(const std::string& model_id,
const std::string& ollama_name = "",
const std::string& quantization = "Q4_K_M",
ProgressCallback callback = nullptr);
// Test deployed model
ActionResult TestModel(const std::string& model_id,
DeploymentBackend backend,
const std::string& test_prompt = "");
// Check if Ollama is running
bool IsOllamaRunning() const;
// Check if llama.cpp is available
bool IsLlamaCppAvailable() const;
// Get last error
const std::string& GetLastError() const { return last_error_; }
private:
// Execute command and capture output
ActionResult ExecuteCommand(const std::vector<std::string>& args,
int timeout_seconds = 300);
std::string afs_cli_path_;
std::string llama_cpp_path_;
std::string last_error_;
};
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,95 @@
#include "filesystem.h"
#include "logger.h"
#include <fstream>
#include <sstream>
#include <cstdlib>
namespace afs {
namespace studio {
namespace core {
std::filesystem::path FileSystem::ResolvePath(const std::string& path_str) {
if (path_str.empty()) return {};
if (path_str[0] == '~') {
const char* home = std::getenv("HOME");
if (home) {
return std::filesystem::path(home) / path_str.substr(2);
}
}
return std::filesystem::path(path_str);
}
bool FileSystem::Exists(const std::filesystem::path& path) {
try {
std::error_code ec;
bool exists = std::filesystem::exists(path, ec);
if (ec) {
// "Device not configured" and other disk errors should just return false
return false;
}
return exists;
} catch (...) {
return false;
}
}
std::optional<std::string> FileSystem::ReadFile(const std::filesystem::path& path) {
if (!Exists(path)) {
LOG_WARN("File not found: " + path.string());
return std::nullopt;
}
std::ifstream file(path, std::ios::in | std::ios::binary);
if (!file.is_open()) {
LOG_ERROR("Failed to open file: " + path.string());
return std::nullopt;
}
std::ostringstream ss;
ss << file.rdbuf();
return ss.str();
}
std::optional<nlohmann::json> FileSystem::ReadJson(const std::filesystem::path& path, std::string* error) {
auto content = ReadFile(path);
if (!content) {
if (error) *error = "Could not read file";
return std::nullopt;
}
try {
return nlohmann::json::parse(*content);
} catch (const nlohmann::json::exception& e) {
std::string err = "JSON parse error in " + path.string() + ": " + e.what();
LOG_ERROR(err);
if (error) *error = err;
return std::nullopt;
}
}
bool FileSystem::WriteFile(const std::filesystem::path& path, const std::string& content) {
std::ofstream file(path, std::ios::out | std::ios::binary);
if (!file.is_open()) {
LOG_ERROR("Failed to open file for writing: " + path.string());
return false;
}
file << content;
return true;
}
bool FileSystem::EnsureDirectory(const std::filesystem::path& path) {
if (Exists(path)) return true;
try {
return std::filesystem::create_directories(path);
} catch (const std::filesystem::filesystem_error& e) {
LOG_ERROR("Failed to create directory " + path.string() + ": " + e.what());
return false;
}
}
} // namespace core
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,35 @@
#pragma once
#include <filesystem>
#include <string>
#include <optional>
#include <nlohmann/json.hpp>
namespace afs {
namespace studio {
namespace core {
class FileSystem {
public:
/// Resolve a home-relative path (e.g., ~/.context) to an absolute path.
static std::filesystem::path ResolvePath(const std::string& path_str);
/// Check if a path exists.
static bool Exists(const std::filesystem::path& path);
/// Read the entire content of a file as a string.
static std::optional<std::string> ReadFile(const std::filesystem::path& path);
/// Read and parse a JSON file.
static std::optional<nlohmann::json> ReadJson(const std::filesystem::path& path, std::string* error = nullptr);
/// Write a string to a file.
static bool WriteFile(const std::filesystem::path& path, const std::string& content);
/// Ensure a directory exists.
static bool EnsureDirectory(const std::filesystem::path& path);
};
} // namespace core
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,378 @@
#include "llama_client.h"
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <poll.h>
#include <sstream>
#include <iostream>
#include <filesystem>
namespace afs::viz {
LlamaClient::LlamaClient() {
status_message_ = "Not configured";
}
LlamaClient::~LlamaClient() {
StopGeneration();
if (generation_thread_.joinable()) {
generation_thread_.join();
}
}
void LlamaClient::SetConfig(const LlamaConfig& config) {
std::lock_guard<std::mutex> lock(mutex_);
config_ = config;
status_message_ = "Configured, checking health...";
}
std::string LlamaClient::ExpandPath(const std::string& path) {
if (path.empty()) return path;
if (path[0] == '~') {
const char* home = getenv("HOME");
if (home) {
return std::string(home) + path.substr(1);
}
}
return path;
}
bool LlamaClient::CheckHealth() {
std::lock_guard<std::mutex> lock(mutex_);
std::string cli_path = ExpandPath(config_.llama_cli_path);
// Check if llama-cli exists
if (!std::filesystem::exists(cli_path)) {
status_message_ = "llama-cli not found: " + cli_path;
is_ready_ = false;
return false;
}
// Check if model exists (if specified)
if (!config_.model_path.empty()) {
std::string model_path = ExpandPath(config_.model_path);
if (!std::filesystem::exists(model_path)) {
status_message_ = "Model not found: " + model_path;
is_ready_ = false;
return false;
}
}
// Check RPC connectivity if enabled
if (config_.use_rpc && !config_.rpc_servers.empty()) {
// Parse first server
size_t colon = config_.rpc_servers.find(':');
if (colon != std::string::npos) {
std::string host = config_.rpc_servers.substr(0, colon);
std::string port_str = config_.rpc_servers.substr(colon + 1);
// Remove any comma for multiple servers
size_t comma = port_str.find(',');
if (comma != std::string::npos) {
port_str = port_str.substr(0, comma);
}
// Quick TCP check
std::string cmd = "nc -z -w 2 " + host + " " + port_str + " 2>/dev/null";
int result = system(cmd.c_str());
if (result != 0) {
status_message_ = "RPC server unreachable: " + host + ":" + port_str;
is_ready_ = false;
return false;
}
}
}
status_message_ = "Ready";
is_ready_ = true;
return true;
}
std::string LlamaClient::GetStatusMessage() const {
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(mutex_));
return status_message_;
}
std::string LlamaClient::BuildPrompt(const std::string& user_message) {
std::ostringstream prompt;
// Build chat history in ChatML format
prompt << "<|im_start|>system\nYou are a helpful AI assistant.<|im_end|>\n";
// History already contains the current user message (added before calling BuildPrompt)
// So we just iterate through all history
for (const auto& msg : history_) {
prompt << "<|im_start|>" << msg.role << "\n" << msg.content << "<|im_end|>\n";
}
prompt << "<|im_start|>assistant\n";
return prompt.str();
}
void LlamaClient::SendMessage(const std::string& message,
TokenCallback on_token,
CompletionCallback on_complete) {
if (is_generating_) {
if (on_complete) {
on_complete(false, "Already generating");
}
return;
}
if (!is_ready_) {
if (on_complete) {
on_complete(false, "Client not ready: " + status_message_);
}
return;
}
// Add user message to history
history_.push_back({"user", message});
// Build prompt
std::string prompt = BuildPrompt(message);
// Start generation in background thread
if (generation_thread_.joinable()) {
generation_thread_.join();
}
is_generating_ = true;
stop_requested_ = false;
generation_thread_ = std::thread(&LlamaClient::GenerationThread, this,
prompt, on_token, on_complete);
}
void LlamaClient::GenerationThread(const std::string& prompt,
TokenCallback on_token,
CompletionCallback on_complete) {
std::string cli_path = ExpandPath(config_.llama_cli_path);
std::string model_path = ExpandPath(config_.model_path);
// Build command arguments
std::vector<std::string> args;
args.push_back(cli_path);
if (config_.use_rpc && !config_.rpc_servers.empty()) {
args.push_back("--rpc");
args.push_back(config_.rpc_servers);
}
args.push_back("-m");
args.push_back(model_path);
args.push_back("-c");
args.push_back(std::to_string(config_.context_size));
args.push_back("-n");
args.push_back(std::to_string(config_.n_predict));
args.push_back("--temp");
args.push_back(std::to_string(config_.temperature));
args.push_back("--top-p");
args.push_back(std::to_string(config_.top_p));
args.push_back("-p");
args.push_back(prompt);
args.push_back("--no-display-prompt");
args.push_back("--single-turn"); // Process prompt and exit, don't wait for input
args.push_back("-e"); // Escape sequences
// Create pipe for stdout
int stdout_pipe[2];
if (pipe(stdout_pipe) < 0) {
is_generating_ = false;
if (on_complete) on_complete(false, "Failed to create pipe");
return;
}
// Fork and exec
pid_t pid = fork();
if (pid < 0) {
close(stdout_pipe[0]);
close(stdout_pipe[1]);
is_generating_ = false;
if (on_complete) on_complete(false, "Failed to fork");
return;
}
if (pid == 0) {
// Child process
close(stdout_pipe[0]);
dup2(stdout_pipe[1], STDOUT_FILENO);
dup2(stdout_pipe[1], STDERR_FILENO);
close(stdout_pipe[1]);
// Convert args to char**
std::vector<char*> argv;
for (auto& arg : args) {
argv.push_back(const_cast<char*>(arg.c_str()));
}
argv.push_back(nullptr);
execvp(argv[0], argv.data());
_exit(127);
}
// Parent process
close(stdout_pipe[1]);
current_pid_ = pid;
// Set non-blocking
int flags = fcntl(stdout_pipe[0], F_GETFL, 0);
fcntl(stdout_pipe[0], F_SETFL, flags | O_NONBLOCK);
std::string accumulated_response;
std::string pending_buffer; // Buffer to accumulate partial lines
char buffer[256];
bool in_preamble = true; // Skip all preamble until we see actual generation
{
std::lock_guard<std::mutex> lock(mutex_);
status_message_ = "Loading model...";
}
while (!stop_requested_) {
struct pollfd pfd;
pfd.fd = stdout_pipe[0];
pfd.events = POLLIN;
int poll_result = poll(&pfd, 1, 100); // 100ms timeout
if (poll_result > 0 && (pfd.revents & POLLIN)) {
ssize_t n = read(stdout_pipe[0], buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
std::string chunk(buffer);
// Skip preamble (ggml logs, loading messages, spinner, banner)
if (in_preamble) {
// Check for signs we're still in preamble
bool is_preamble =
chunk.find("ggml_") != std::string::npos ||
chunk.find("Loading") != std::string::npos ||
chunk.find("llama_") != std::string::npos ||
chunk.find("GPU") != std::string::npos ||
chunk.find("Metal") != std::string::npos ||
chunk.find("tensor") != std::string::npos ||
chunk.find("simd") != std::string::npos ||
chunk.find("MTL") != std::string::npos ||
chunk.find("build") != std::string::npos ||
chunk.find("rpc") != std::string::npos ||
chunk.find("CUDA") != std::string::npos ||
chunk.find("Backend") != std::string::npos ||
chunk.find("modalities") != std::string::npos ||
chunk.find("available commands") != std::string::npos ||
chunk.find("/exit") != std::string::npos ||
chunk.find("/regen") != std::string::npos ||
chunk.find("/clear") != std::string::npos ||
chunk.find("/read") != std::string::npos ||
// ASCII art banner
chunk.find("") != std::string::npos ||
chunk.find("") != std::string::npos ||
chunk.find("") != std::string::npos ||
// Spinner characters (often appear alone)
(chunk.length() <= 3 && (
chunk.find("|") != std::string::npos ||
chunk.find("-") != std::string::npos ||
chunk.find("\\") != std::string::npos ||
chunk.find("/") != std::string::npos));
if (is_preamble) {
// Update status with loading progress hints
if (chunk.find("Loading") != std::string::npos) {
std::lock_guard<std::mutex> lock(mutex_);
status_message_ = "Loading model...";
} else if (chunk.find("rpc") != std::string::npos) {
std::lock_guard<std::mutex> lock(mutex_);
status_message_ = "Connecting to RPC...";
}
continue;
}
// We've passed the preamble
in_preamble = false;
{
std::lock_guard<std::mutex> lock(mutex_);
status_message_ = "Generating...";
}
}
accumulated_response += chunk;
if (on_token) on_token(chunk);
} else if (n == 0) {
break; // EOF
}
} else if (poll_result == 0) {
// Timeout, check if process is still running
int status;
pid_t result = waitpid(pid, &status, WNOHANG);
if (result == pid) {
// Process finished, read any remaining output
ssize_t remaining;
while ((remaining = read(stdout_pipe[0], buffer, sizeof(buffer) - 1)) > 0) {
buffer[remaining] = '\0';
std::string chunk(buffer);
accumulated_response += chunk;
if (on_token) on_token(chunk);
}
break;
}
}
}
close(stdout_pipe[0]);
// Stop if requested
if (stop_requested_ && current_pid_ > 0) {
kill(current_pid_, SIGTERM);
usleep(100000); // 100ms
kill(current_pid_, SIGKILL);
}
// Wait for process
int status;
waitpid(pid, &status, 0);
current_pid_ = -1;
// Add assistant response to history
if (!accumulated_response.empty()) {
// Clean up response (remove ChatML end token if present)
size_t end_pos = accumulated_response.find("<|im_end|>");
if (end_pos != std::string::npos) {
accumulated_response = accumulated_response.substr(0, end_pos);
}
history_.push_back({"assistant", accumulated_response});
}
{
std::lock_guard<std::mutex> lock(mutex_);
status_message_ = "Ready";
}
is_generating_ = false;
if (on_complete) {
if (stop_requested_) {
on_complete(false, "Stopped by user");
} else {
on_complete(true, "");
}
}
}
void LlamaClient::StopGeneration() {
stop_requested_ = true;
if (current_pid_ > 0) {
kill(current_pid_, SIGTERM);
}
}
void LlamaClient::ClearHistory() {
std::lock_guard<std::mutex> lock(mutex_);
history_.clear();
}
} // namespace afs::viz

View File

@@ -0,0 +1,81 @@
#pragma once
#include <string>
#include <vector>
#include <deque>
#include <functional>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
namespace afs::viz {
struct ChatMessage {
std::string role; // "user", "assistant", "system"
std::string content;
};
struct LlamaConfig {
std::string llama_cli_path = "~/llama.cpp/build/bin/llama-cli";
std::string model_path;
std::string rpc_servers; // Comma-separated list of host:port
int context_size = 4096;
int n_predict = 256;
float temperature = 0.7f;
float top_p = 0.9f;
bool use_rpc = true;
};
// Callback for streaming tokens
using TokenCallback = std::function<void(const std::string&)>;
using CompletionCallback = std::function<void(bool success, const std::string& error)>;
class LlamaClient {
public:
LlamaClient();
~LlamaClient();
// Configuration
void SetConfig(const LlamaConfig& config);
const LlamaConfig& GetConfig() const { return config_; }
// Connection management
bool CheckHealth();
bool IsReady() const { return is_ready_; }
bool IsGenerating() const { return is_generating_; }
std::string GetStatusMessage() const;
// Chat interface
void SendMessage(const std::string& message,
TokenCallback on_token,
CompletionCallback on_complete);
void StopGeneration();
void ClearHistory();
// History access
const std::vector<ChatMessage>& GetHistory() const { return history_; }
private:
void GenerationThread(const std::string& prompt,
TokenCallback on_token,
CompletionCallback on_complete);
std::string BuildPrompt(const std::string& user_message);
std::string ExpandPath(const std::string& path);
LlamaConfig config_;
std::vector<ChatMessage> history_;
std::atomic<bool> is_ready_{false};
std::atomic<bool> is_generating_{false};
std::atomic<bool> stop_requested_{false};
std::thread generation_thread_;
std::mutex mutex_;
std::string status_message_;
// Process management
pid_t current_pid_ = -1;
};
} // namespace afs::viz

View File

@@ -0,0 +1,53 @@
#include "logger.h"
#include <chrono>
#include <iomanip>
#include <sstream>
namespace afs {
namespace studio {
namespace core {
Logger& Logger::GetInstance() {
static Logger instance;
return instance;
}
void Logger::Log(LogLevel level, const std::string& message) {
std::lock_guard<std::mutex> lock(mutex_);
std::string ts = GetTimestamp();
entries_.push_back({level, message, ts});
// Output to console as well
std::ostream& out = (level == LogLevel::kError || level == LogLevel::kWarn) ? std::cerr : std::cout;
out << "[" << ts << "] [" << LevelToString(level) << "] " << message << std::endl;
// Cap entries to prevent memory leaks in long-running app
if (entries_.size() > 1000) {
entries_.erase(entries_.begin(), entries_.begin() + 100);
}
}
std::string Logger::GetTimestamp() {
auto now = std::chrono::system_clock::now();
auto in_time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&in_time_t), "%H:%M:%S");
return ss.str();
}
const char* Logger::LevelToString(LogLevel level) {
switch (level) {
case LogLevel::kTrace: return "TRACE";
case LogLevel::kDebug: return "DEBUG";
case LogLevel::kInfo: return "INFO";
case LogLevel::kWarn: return "WARN";
case LogLevel::kError: return "ERROR";
default: return "UNKNOWN";
}
}
} // namespace core
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,63 @@
#pragma once
#include <string>
#include <vector>
#include <mutex>
#include <iostream>
namespace afs {
namespace studio {
namespace core {
enum class LogLevel {
kTrace,
kDebug,
kInfo,
kWarn,
kError
};
struct LogEntry {
LogLevel level;
std::string message;
std::string timestamp;
};
class Logger {
public:
static Logger& GetInstance();
void Log(LogLevel level, const std::string& message);
void Trace(const std::string& message) { Log(LogLevel::kTrace, message); }
void Debug(const std::string& message) { Log(LogLevel::kDebug, message); }
void Info(const std::string& message) { Log(LogLevel::kInfo, message); }
void Warn(const std::string& message) { Log(LogLevel::kWarn, message); }
void Error(const std::string& message) { Log(LogLevel::kError, message); }
const std::vector<LogEntry>& GetEntries() const { return entries_; }
void Clear() { std::lock_guard<std::mutex> lock(mutex_); entries_.clear(); }
private:
Logger() = default;
~Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
std::string GetTimestamp();
const char* LevelToString(LogLevel level);
std::vector<LogEntry> entries_;
mutable std::mutex mutex_;
};
// Global convenience macros
#define LOG_TRACE(msg) ::afs::studio::core::Logger::GetInstance().Trace(msg)
#define LOG_DEBUG(msg) ::afs::studio::core::Logger::GetInstance().Debug(msg)
#define LOG_INFO(msg) ::afs::studio::core::Logger::GetInstance().Info(msg)
#define LOG_WARN(msg) ::afs::studio::core::Logger::GetInstance().Warn(msg)
#define LOG_ERROR(msg) ::afs::studio::core::Logger::GetInstance().Error(msg)
} // namespace core
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,258 @@
#include "registry_reader.h"
#include "logger.h"
#include "filesystem.h"
#include <fstream>
namespace afs {
namespace studio {
RegistryReader::RegistryReader() : registry_path_(ResolveDefaultPath()) {}
RegistryReader::RegistryReader(const std::filesystem::path& registry_path)
: registry_path_(registry_path) {}
std::filesystem::path RegistryReader::ResolveDefaultPath() const {
return core::FileSystem::ResolvePath("~/.context/models/registry.json");
}
bool RegistryReader::Exists() const {
return core::FileSystem::Exists(registry_path_);
}
bool RegistryReader::Load(std::string* error) {
last_error_.clear();
models_.clear();
if (!Exists()) {
last_error_ = "Registry file not found: " + registry_path_.string();
LOG_ERROR(last_error_);
if (error) *error = last_error_;
return false;
}
LOG_INFO("RegistryReader: Loading from " + registry_path_.string());
std::ifstream file(registry_path_);
if (!file.is_open()) {
last_error_ = "Failed to open registry file";
if (error) *error = last_error_;
return false;
}
models_.clear();
try {
nlohmann::json root = nlohmann::json::parse(file);
// Parse registry metadata
if (root.contains("updated_at")) {
last_load_time_ = root["updated_at"].get<std::string>();
}
// Parse models
if (root.contains("models") && root["models"].is_object()) {
for (auto& [model_id, model_json] : root["models"].items()) {
ModelMetadata model;
if (ParseModel(model_json, &model)) {
model.model_id = model_id; // Ensure ID matches key
models_.push_back(std::move(model));
}
}
}
LOG_INFO("RegistryReader: Successfully loaded " + std::to_string(models_.size()) + " models");
return true;
} catch (const nlohmann::json::exception& e) {
last_error_ = std::string("JSON parse error: ") + e.what();
if (error) *error = last_error_;
return false;
}
}
bool RegistryReader::Reload(std::string* error) {
return Load(error);
}
const ModelMetadata* RegistryReader::GetModel(
const std::string& model_id) const {
for (const auto& model : models_) {
if (model.model_id == model_id) {
return &model;
}
}
return nullptr;
}
std::vector<const ModelMetadata*> RegistryReader::FilterByRole(
const std::string& role) const {
std::vector<const ModelMetadata*> result;
for (const auto& model : models_) {
if (model.role == role) {
result.push_back(&model);
}
}
return result;
}
std::vector<const ModelMetadata*> RegistryReader::FilterByLocation(
const std::string& location) const {
std::vector<const ModelMetadata*> result;
for (const auto& model : models_) {
if (model.locations.count(location) > 0) {
result.push_back(&model);
}
}
return result;
}
std::vector<const ModelMetadata*> RegistryReader::FilterByBackend(
const std::string& backend) const {
std::vector<const ModelMetadata*> result;
for (const auto& model : models_) {
for (const auto& b : model.deployed_backends) {
if (b == backend) {
result.push_back(&model);
break;
}
}
}
return result;
}
bool RegistryReader::ParseModel(const nlohmann::json& json,
ModelMetadata* model) const {
if (!json.is_object()) return false;
// Helper to safely get string
auto get_string = [&](const char* key) -> std::string {
if (json.contains(key) && json[key].is_string()) {
return json[key].get<std::string>();
}
return "";
};
// Helper to safely get optional string
auto get_optional_string =
[&](const char* key) -> std::optional<std::string> {
if (json.contains(key) && json[key].is_string()) {
return json[key].get<std::string>();
}
return std::nullopt;
};
// Helper to safely get int
auto get_int = [&](const char* key) -> int {
if (json.contains(key) && json[key].is_number()) {
return json[key].get<int>();
}
return 0;
};
// Helper to safely get optional float
auto get_optional_float = [&](const char* key) -> std::optional<float> {
if (json.contains(key) && json[key].is_number()) {
return json[key].get<float>();
}
return std::nullopt;
};
// Identity
model->model_id = get_string("model_id");
model->display_name = get_string("display_name");
model->version = get_string("version");
// Training info
model->base_model = get_string("base_model");
model->role = get_string("role");
model->group = get_string("group");
model->training_date = get_string("training_date");
model->training_duration_minutes = get_int("training_duration_minutes");
// Dataset info
model->dataset_name = get_string("dataset_name");
model->dataset_path = get_string("dataset_path");
model->train_samples = get_int("train_samples");
model->val_samples = get_int("val_samples");
model->test_samples = get_int("test_samples");
// Dataset quality
if (json.contains("dataset_quality") && json["dataset_quality"].is_object()) {
const auto& q = json["dataset_quality"];
if (q.contains("acceptance_rate") && q["acceptance_rate"].is_number()) {
model->dataset_quality.acceptance_rate = q["acceptance_rate"].get<float>();
}
if (q.contains("rejection_rate") && q["rejection_rate"].is_number()) {
model->dataset_quality.rejection_rate = q["rejection_rate"].get<float>();
}
if (q.contains("avg_diversity") && q["avg_diversity"].is_number()) {
model->dataset_quality.avg_diversity = q["avg_diversity"].get<float>();
}
}
// Training metrics
model->final_loss = get_optional_float("final_loss");
model->best_loss = get_optional_float("best_loss");
model->eval_loss = get_optional_float("eval_loss");
model->perplexity = get_optional_float("perplexity");
// Hardware
model->hardware = get_string("hardware");
model->device = get_string("device");
// Files
model->model_path = get_string("model_path");
model->checkpoint_path = get_optional_string("checkpoint_path");
model->adapter_path = get_optional_string("adapter_path");
// Formats
if (json.contains("formats") && json["formats"].is_array()) {
for (const auto& fmt : json["formats"]) {
if (fmt.is_string()) {
model->formats.push_back(fmt.get<std::string>());
}
}
}
// Locations
if (json.contains("locations") && json["locations"].is_object()) {
for (auto& [loc, path] : json["locations"].items()) {
if (path.is_string()) {
model->locations[loc] = path.get<std::string>();
}
}
}
model->primary_location = get_string("primary_location");
// Serving
if (json.contains("deployed_backends") &&
json["deployed_backends"].is_array()) {
for (const auto& backend : json["deployed_backends"]) {
if (backend.is_string()) {
model->deployed_backends.push_back(backend.get<std::string>());
}
}
}
model->ollama_model_name = get_optional_string("ollama_model_name");
model->halext_node_id = get_optional_string("halext_node_id");
// Metadata
model->git_commit = get_optional_string("git_commit");
model->notes = get_string("notes");
if (json.contains("tags") && json["tags"].is_array()) {
for (const auto& tag : json["tags"]) {
if (tag.is_string()) {
model->tags.push_back(tag.get<std::string>());
}
}
}
model->created_at = get_string("created_at");
model->updated_at = get_string("updated_at");
return true;
}
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,136 @@
#pragma once
#include <nlohmann/json.hpp>
#include <filesystem>
#include <map>
#include <optional>
#include <string>
#include <vector>
namespace afs {
namespace studio {
// Model location types
enum class ModelLocation { kMac, kWindows, kCloud, kHalext };
// Serving backend types
enum class ServingBackend { kOllama, kLlamaCpp, kVllm, kTransformers, kHalextNode };
// Dataset quality metrics
struct DatasetQuality {
float acceptance_rate = 0.0f;
float rejection_rate = 0.0f;
float avg_diversity = 0.0f;
};
// Model metadata matching Python ModelMetadata dataclass
struct ModelMetadata {
// Identity
std::string model_id;
std::string display_name;
std::string version;
// Training info
std::string base_model;
std::string role;
std::string group;
std::string training_date;
int training_duration_minutes = 0;
// Dataset info
std::string dataset_name;
std::string dataset_path;
int train_samples = 0;
int val_samples = 0;
int test_samples = 0;
DatasetQuality dataset_quality;
// Training metrics
std::optional<float> final_loss;
std::optional<float> best_loss;
std::optional<float> eval_loss;
std::optional<float> perplexity;
// Hardware
std::string hardware;
std::string device;
// Files
std::string model_path;
std::optional<std::string> checkpoint_path;
std::optional<std::string> adapter_path;
// Formats available
std::vector<std::string> formats;
// Locations (location -> path)
std::map<std::string, std::string> locations;
std::string primary_location;
// Serving
std::vector<std::string> deployed_backends;
std::optional<std::string> ollama_model_name;
std::optional<std::string> halext_node_id;
// Metadata
std::optional<std::string> git_commit;
std::string notes;
std::vector<std::string> tags;
std::string created_at;
std::string updated_at;
};
// Registry reader class
class RegistryReader {
public:
RegistryReader();
explicit RegistryReader(const std::filesystem::path& registry_path);
// Load registry from disk
bool Load(std::string* error = nullptr);
// Reload registry (refresh from disk)
bool Reload(std::string* error = nullptr);
// Get all models
const std::vector<ModelMetadata>& GetModels() const { return models_; }
// Get model by ID
const ModelMetadata* GetModel(const std::string& model_id) const;
// Filter models by role
std::vector<const ModelMetadata*> FilterByRole(const std::string& role) const;
// Filter models by location
std::vector<const ModelMetadata*> FilterByLocation(
const std::string& location) const;
// Filter models by backend
std::vector<const ModelMetadata*> FilterByBackend(
const std::string& backend) const;
// Get registry path
const std::filesystem::path& GetPath() const { return registry_path_; }
// Check if registry exists
bool Exists() const;
// Get last load time
const std::string& GetLastLoadTime() const { return last_load_time_; }
// Get last error
const std::string& GetLastError() const { return last_error_; }
private:
std::filesystem::path ResolveDefaultPath() const;
bool ParseModel(const nlohmann::json& json, ModelMetadata* model) const;
std::filesystem::path registry_path_;
std::vector<ModelMetadata> models_;
std::string last_load_time_;
std::string last_error_;
};
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,264 @@
#include "training_monitor.h"
#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <filesystem>
#include "filesystem.h"
#include "logger.h"
namespace afs {
namespace studio {
TrainingMonitor::TrainingMonitor() {
// Default Windows mount for medical-mechanica D:
const char* home = std::getenv("HOME");
if (home) {
config_.windows_mount_path =
std::filesystem::path(home) / "Mounts" / "mm-d" / "afs_training";
}
}
TrainingMonitor::TrainingMonitor(const TrainingMonitorConfig& config)
: config_(config) {}
std::filesystem::path TrainingMonitor::ResolveWindowsMount() const {
// Check if mount is accessible
if (core::FileSystem::Exists(config_.windows_mount_path)) {
std::error_code ec;
if (std::filesystem::is_directory(config_.windows_mount_path, ec)) {
return config_.windows_mount_path;
}
}
return {};
}
std::filesystem::path TrainingMonitor::FindLatestCheckpoint(
const std::filesystem::path& model_dir) {
std::filesystem::path latest;
std::filesystem::file_time_type latest_time;
try {
std::error_code ec;
auto it = std::filesystem::directory_iterator(model_dir, ec);
if (!ec) {
for (const auto& entry : it) {
std::error_code entry_ec;
if (entry.is_directory(entry_ec)) {
std::string name = entry.path().filename().string();
// Look for checkpoint-* directories
if (name.find("checkpoint-") == 0) {
auto time = entry.last_write_time(entry_ec);
if (!entry_ec && (latest.empty() || time > latest_time)) {
latest = entry.path();
latest_time = time;
}
}
}
}
}
} catch (...) {
// Directory iteration failed
}
return latest;
}
bool TrainingMonitor::Poll(std::string* error) {
last_error_.clear();
last_poll_time_ = std::chrono::steady_clock::now();
// Try mount first
std::filesystem::path mount = ResolveWindowsMount();
if (!mount.empty()) {
// Look for active training dirs
std::filesystem::path models_dir = mount / "models";
if (core::FileSystem::Exists(models_dir)) {
// Find the most recently modified model directory
std::filesystem::path latest_model;
std::filesystem::file_time_type latest_time;
try {
std::error_code ec;
auto it = std::filesystem::directory_iterator(models_dir, ec);
if (!ec) {
for (const auto& entry : it) {
std::error_code entry_ec;
if (entry.is_directory(entry_ec)) {
auto time = entry.last_write_time(entry_ec);
if (!entry_ec && (latest_model.empty() || time > latest_time)) {
latest_model = entry.path();
latest_time = time;
}
}
}
}
} catch (...) {
// Directory iteration failed
}
if (!latest_model.empty()) {
// Look for trainer_state.json in latest checkpoint
std::filesystem::path checkpoint = FindLatestCheckpoint(latest_model);
if (!checkpoint.empty()) {
std::filesystem::path state_file = checkpoint / "trainer_state.json";
if (core::FileSystem::Exists(state_file)) {
return LoadFromPath(state_file, error);
}
}
// Try trainer_state.json in model root
std::filesystem::path root_state = latest_model / "trainer_state.json";
if (core::FileSystem::Exists(root_state)) {
return LoadFromPath(root_state, error);
}
}
}
last_error_ = "No active training found in mount";
if (error) *error = last_error_;
state_.status = TrainingStatus::kIdle;
return true; // Not an error, just no active training
}
last_error_ = "Windows mount not accessible: " + config_.windows_mount_path.string();
if (error) *error = last_error_;
return false;
}
bool TrainingMonitor::LoadFromPath(const std::filesystem::path& path,
std::string* error) {
std::ifstream file(path);
if (!file.is_open()) {
last_error_ = "Failed to open: " + path.string();
if (error) *error = last_error_;
return false;
}
try {
nlohmann::json json = nlohmann::json::parse(file);
if (!ParseTrainerState(json)) {
last_error_ = "Failed to parse trainer state";
if (error) *error = last_error_;
return false;
}
state_.source_path = path.string();
state_.source_location = "windows";
state_.is_remote = true;
return true;
} catch (const nlohmann::json::exception& e) {
last_error_ = std::string("JSON parse error: ") + e.what();
if (error) *error = last_error_;
return false;
}
}
bool TrainingMonitor::ParseTrainerState(const nlohmann::json& json) {
// Reset state
state_ = TrainingState{};
// Helper to safely get values
auto get_int = [&](const char* key) -> int {
if (json.contains(key) && json[key].is_number()) {
return json[key].get<int>();
}
return 0;
};
auto get_float = [&](const char* key) -> float {
if (json.contains(key) && json[key].is_number()) {
return json[key].get<float>();
}
return 0.0f;
};
auto get_string = [&](const char* key) -> std::string {
if (json.contains(key) && json[key].is_string()) {
return json[key].get<std::string>();
}
return "";
};
// Parse basic info
state_.current_epoch = get_int("epoch");
state_.total_epochs = get_int("num_train_epochs");
state_.current_step = get_int("global_step");
state_.total_steps = get_int("max_steps");
// Calculate progress
if (state_.total_steps > 0) {
state_.progress_percent =
static_cast<float>(state_.current_step) / state_.total_steps * 100.0f;
}
// Determine status
if (state_.current_step > 0 && state_.current_step < state_.total_steps) {
state_.status = TrainingStatus::kRunning;
} else if (state_.current_step >= state_.total_steps && state_.total_steps > 0) {
state_.status = TrainingStatus::kCompleted;
} else {
state_.status = TrainingStatus::kIdle;
}
// Parse loss history
if (json.contains("log_history") && json["log_history"].is_array()) {
for (const auto& entry : json["log_history"]) {
if (entry.contains("loss") && entry["loss"].is_number()) {
LossPoint point;
if (entry.contains("step") && entry["step"].is_number()) {
point.step = entry["step"].get<int>();
}
point.loss = entry["loss"].get<float>();
if (entry.contains("eval_loss") && entry["eval_loss"].is_number()) {
point.eval_loss = entry["eval_loss"].get<float>();
}
state_.loss_history.push_back(point);
// Track current and best loss
state_.current_loss = point.loss;
if (state_.best_loss == 0.0f || point.loss < state_.best_loss) {
state_.best_loss = point.loss;
state_.best_step = point.step;
}
}
}
}
// Parse timing info
if (json.contains("total_flos") && json["total_flos"].is_number()) {
// Estimate based on FLOPS if available
}
// Extract model name from path if available
if (json.contains("best_model_checkpoint") &&
json["best_model_checkpoint"].is_string()) {
std::string path = json["best_model_checkpoint"].get<std::string>();
// Extract model name from path
size_t pos = path.rfind('/');
if (pos == std::string::npos) pos = path.rfind('\\');
if (pos != std::string::npos && pos > 0) {
size_t start = path.rfind('/', pos - 1);
if (start == std::string::npos) start = path.rfind('\\', pos - 1);
if (start != std::string::npos) {
state_.model_name = path.substr(start + 1, pos - start - 1);
}
}
}
return true;
}
bool TrainingMonitor::ShouldRefresh() const {
if (!config_.auto_refresh) return false;
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
now - last_poll_time_);
return elapsed.count() >= config_.refresh_interval_seconds;
}
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,148 @@
#pragma once
#include <nlohmann/json.hpp>
#include <chrono>
#include <deque>
#include <filesystem>
#include <map>
#include <optional>
#include <string>
#include <vector>
namespace afs {
namespace studio {
// Training run status
enum class TrainingStatus {
kUnknown,
kIdle,
kRunning,
kCompleted,
kFailed,
kPaused
};
// Single epoch progress data
struct EpochProgress {
int epoch = 0;
int step = 0;
int total_steps = 0;
float loss = 0.0f;
float learning_rate = 0.0f;
std::string timestamp;
};
// Loss data point for charts
struct LossPoint {
int step = 0;
float loss = 0.0f;
float eval_loss = 0.0f;
};
// Training state from trainer_state.json
struct TrainingState {
// Identity
std::string run_name;
std::string model_name;
std::string base_model;
// Progress
TrainingStatus status = TrainingStatus::kUnknown;
int current_epoch = 0;
int total_epochs = 0;
int current_step = 0;
int total_steps = 0;
float progress_percent = 0.0f;
// Timing
std::string started_at;
std::string updated_at;
int elapsed_minutes = 0;
int estimated_remaining_minutes = 0;
// Metrics
float current_loss = 0.0f;
float best_loss = 0.0f;
int best_step = 0;
std::optional<float> eval_loss;
std::optional<float> perplexity;
// Loss history for plotting
std::vector<LossPoint> loss_history;
// Hardware
std::string device;
std::optional<float> gpu_memory_percent;
std::optional<float> gpu_utilization_percent;
// Source
std::string source_path;
std::string source_location; // "windows", "mac", "cloud"
bool is_remote = false;
};
// Configuration for training monitor
struct TrainingMonitorConfig {
// Windows mount point
std::filesystem::path windows_mount_path;
std::string windows_training_dir = "D:/afs_training";
// SSH config (fallback if mount not available)
std::string ssh_host = "medical-mechanica";
std::string ssh_user = "Administrator";
// Auto-refresh
bool auto_refresh = true;
int refresh_interval_seconds = 10;
// What to monitor
std::vector<std::string> watched_paths;
};
// Training monitor - reads training state from local or remote
class TrainingMonitor {
public:
TrainingMonitor();
explicit TrainingMonitor(const TrainingMonitorConfig& config);
// Poll for current training state
bool Poll(std::string* error = nullptr);
// Get current training state
const TrainingState& GetState() const { return state_; }
// Check if any training is active
bool IsTrainingActive() const {
return state_.status == TrainingStatus::kRunning;
}
// Get last poll time
const std::chrono::steady_clock::time_point& GetLastPollTime() const {
return last_poll_time_;
}
// Check if refresh is needed based on interval
bool ShouldRefresh() const;
// Configuration
const TrainingMonitorConfig& GetConfig() const { return config_; }
void SetConfig(const TrainingMonitorConfig& config) { config_ = config; }
// Error handling
const std::string& GetLastError() const { return last_error_; }
private:
bool LoadFromPath(const std::filesystem::path& path, std::string* error);
bool ParseTrainerState(const nlohmann::json& json);
std::filesystem::path FindLatestCheckpoint(const std::filesystem::path& model_dir);
std::filesystem::path ResolveWindowsMount() const;
TrainingMonitorConfig config_;
TrainingState state_;
std::chrono::steady_clock::time_point last_poll_time_;
std::string last_error_;
};
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,614 @@
#include "data_loader.h"
#include "core/logger.h"
#include "core/filesystem.h"
#include <algorithm>
#include <cmath>
#include <cctype>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <utility>
#include <optional>
#include <nlohmann/json.hpp>
namespace afs {
namespace viz {
namespace {
using json = nlohmann::json;
constexpr size_t kTrendWindow = 5;
std::optional<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;
}
auto legacy_path = studio::core::FileSystem::ResolvePath("~/src/trunk/scawful/research/afs_scawful");
if (studio::core::FileSystem::Exists(legacy_path)) {
return legacy_path;
}
return std::nullopt;
}
constexpr float kTrendDeltaThreshold = 0.05f;
bool IsWhitespaceOnly(const std::string& s) {
return std::all_of(s.begin(), s.end(), [](unsigned char c) {
return std::isspace(c);
});
}
} // namespace
DataLoader::DataLoader(const std::string& data_path,
FileReader file_reader,
PathExists path_exists)
: data_path_(data_path) {
// Set default handlers if not provided
if (file_reader) {
file_reader_ = std::move(file_reader);
} else {
file_reader_ = [](const std::string& p, std::string* c, std::string* e) {
auto content = studio::core::FileSystem::ReadFile(p);
if (content) {
*c = *content;
return true;
}
if (e) *e = "Failed to read file";
return false;
};
}
if (path_exists) {
path_exists_ = std::move(path_exists);
} else {
path_exists_ = [](const std::string& p) {
try {
return studio::core::FileSystem::Exists(p);
} catch (...) {
return false;
}
};
}
}
bool DataLoader::Refresh() {
last_error_.clear();
last_status_ = LoadStatus{};
if (!path_exists_(data_path_)) {
last_error_ = "Data path does not exist: " + data_path_;
LOG_ERROR(last_error_);
last_status_.error_count = 1;
last_status_.last_error = last_error_;
last_status_.last_error_source = "data_path";
return false;
}
LOG_INFO("DataLoader: Refreshing from " + data_path_);
auto next_quality_trends = quality_trends_;
auto next_generator_stats = generator_stats_;
auto next_rejection_summary = rejection_summary_;
auto next_embedding_regions = embedding_regions_;
auto next_coverage = coverage_;
auto next_training_runs = training_runs_;
auto next_optimization_data = optimization_data_;
auto next_curated_hacks = curated_hacks_;
auto next_resource_index = resource_index_;
LoadResult quality = LoadQualityFeedback(&next_quality_trends,
&next_generator_stats,
&next_rejection_summary);
last_status_.quality_found = quality.found;
last_status_.quality_ok = quality.ok;
if (quality.found && !quality.ok) {
last_status_.error_count += 1;
if (last_status_.last_error.empty()) {
last_status_.last_error = quality.error;
last_status_.last_error_source = "quality_feedback.json";
}
}
if (quality.ok) {
quality_trends_ = std::move(next_quality_trends);
generator_stats_ = std::move(next_generator_stats);
rejection_summary_ = std::move(next_rejection_summary);
// Initialize domain visibility for new domains
for (const auto& trend : quality_trends_) {
if (domain_visibility_.find(trend.domain) == domain_visibility_.end()) {
domain_visibility_[trend.domain] = true;
}
}
}
LoadResult active = LoadActiveLearning(&next_embedding_regions, &next_coverage);
last_status_.active_found = active.found;
last_status_.active_ok = active.ok;
if (active.found && !active.ok) {
last_status_.error_count += 1;
if (last_status_.last_error.empty()) {
last_status_.last_error = active.error;
last_status_.last_error_source = "active_learning.json";
}
}
if (active.ok) {
embedding_regions_ = std::move(next_embedding_regions);
coverage_ = std::move(next_coverage);
}
LoadResult training = LoadTrainingFeedback(&next_training_runs,
&next_optimization_data);
last_status_.training_found = training.found;
last_status_.training_ok = training.ok;
if (training.found && !training.ok) {
last_status_.error_count += 1;
if (last_status_.last_error.empty()) {
last_status_.last_error = training.error;
last_status_.last_error_source = "training_feedback.json";
}
}
if (training.ok) {
training_runs_ = std::move(next_training_runs);
optimization_data_ = std::move(next_optimization_data);
}
LoadResult curated = LoadCuratedHacks(&next_curated_hacks);
if (!curated.found) {
curated_hacks_.clear();
curated_hacks_error_ = "curated_hacks.json not found";
} else if (!curated.ok) {
curated_hacks_error_ = curated.error;
} else {
curated_hacks_ = std::move(next_curated_hacks);
curated_hacks_error_.clear();
}
LoadResult resource = LoadResourceIndex(&next_resource_index);
if (!resource.found) {
resource_index_ = ResourceIndexData{};
resource_index_error_ = "resource_index.json not found";
} else if (!resource.ok) {
resource_index_error_ = resource.error;
} else {
resource_index_ = std::move(next_resource_index);
resource_index_error_.clear();
}
// Update Mounts status
mounts_.clear();
const char* home = std::getenv("HOME");
std::string home_str = home ? home : "";
auto add_mount = [&](const std::string& name, std::string path) {
if (path.size() >= 2 && path[0] == '~' && path[1] == '/') {
path = home_str + path.substr(1);
}
mounts_.push_back({name, path, path_exists_(path)});
};
add_mount("Code", "~/Code");
auto scawful_root = ResolveHafsScawfulRoot();
if (scawful_root) {
add_mount("afs_scawful", scawful_root->string());
}
add_mount("usdasm", "~/Code/usdasm");
add_mount("Medical Mechanica (D)", "/Users/scawful/Mounts/mm-d/afs_training");
add_mount("Oracle-of-Secrets", "~/Code/Oracle-of-Secrets");
add_mount("yaze", "~/Code/yaze");
add_mount("System Context", "~/.context");
has_data_ = !quality_trends_.empty() || !generator_stats_.empty() ||
!embedding_regions_.empty() || !training_runs_.empty() ||
!optimization_data_.domain_effectiveness.empty() ||
!optimization_data_.threshold_sensitivity.empty();
last_error_ = last_status_.last_error;
return last_status_.AnyOk() || (!(last_status_.FoundCount() > 0) && has_data_);
}
DataLoader::LoadResult DataLoader::LoadQualityFeedback(
std::vector<QualityTrendData>* quality_trends,
std::vector<GeneratorStatsData>* generator_stats,
RejectionSummary* rejection_summary) {
LoadResult result;
std::string path = data_path_ + "/quality_feedback.json";
if (!path_exists_(path)) {
LOG_WARN("quality_feedback.json not found at " + path);
return result;
}
LOG_INFO("DataLoader: Loading " + path);
result.found = true;
std::string content;
std::string read_error;
if (!file_reader_(path, &content, &read_error) || content.empty() || IsWhitespaceOnly(content)) {
result.ok = false;
result.error = read_error.empty() ? "quality_feedback.json is empty" : read_error;
return result;
}
try {
json data = json::parse(content);
std::vector<QualityTrendData> next_quality_trends;
std::vector<GeneratorStatsData> next_generator_stats;
RejectionSummary next_rejection_summary;
if (data.contains("generator_stats") && data["generator_stats"].is_object()) {
bool only_one = data["generator_stats"].size() == 1;
for (auto& [name, stats] : data["generator_stats"].items()) {
GeneratorStatsData gs;
std::string processed_name = name;
if (processed_name == "unknown" && only_one) {
processed_name = "Core Engine";
}
// Strip common suffixes for cleaner display
size_t pos = processed_name.find("DataGenerator");
if (pos != std::string::npos) {
processed_name = processed_name.substr(0, pos);
}
gs.name = processed_name;
gs.samples_generated = stats.value("samples_generated", 0);
gs.samples_accepted = stats.value("samples_accepted", 0);
gs.samples_rejected = stats.value("samples_rejected", 0);
gs.avg_quality = stats.value("avg_quality_score", 0.0f);
int total = gs.samples_accepted + gs.samples_rejected;
gs.acceptance_rate = total > 0 ? static_cast<float>(gs.samples_accepted) / total : 0.0f;
if (stats.contains("rejection_reasons") && stats["rejection_reasons"].is_object()) {
for (auto& [reason, count] : stats["rejection_reasons"].items()) {
int c = count.get<int>();
gs.rejection_reasons[reason] = c;
next_rejection_summary.reasons[reason] += c;
next_rejection_summary.total_rejections += c;
}
}
next_generator_stats.push_back(std::move(gs));
}
}
if (data.contains("rejection_history") && data["rejection_history"].is_array()) {
std::map<std::pair<std::string, std::string>, QualityTrendData> trends_map;
for (auto& entry : data["rejection_history"]) {
std::string domain = entry.value("domain", "unknown");
if (entry.contains("scores") && entry["scores"].is_object()) {
for (auto& [metric, value] : entry["scores"].items()) {
auto key = std::make_pair(domain, metric);
if (trends_map.find(key) == trends_map.end()) {
trends_map[key] = QualityTrendData{domain, metric};
}
trends_map[key].values.push_back(value.get<float>());
}
}
}
for (auto& [key, trend] : trends_map) {
if (!trend.values.empty()) {
float sum = 0.0f;
for (float v : trend.values) sum += v;
trend.mean = sum / trend.values.size();
if (trend.values.size() < kTrendWindow) {
trend.trend_direction = "insufficient";
} else {
float recent = 0.0f, older = 0.0f;
for (size_t i = trend.values.size() - kTrendWindow; i < trend.values.size(); ++i) recent += trend.values[i];
for (size_t i = 0; i < kTrendWindow && i < trend.values.size(); ++i) older += trend.values[i];
recent /= kTrendWindow;
older /= std::min((size_t)kTrendWindow, trend.values.size());
float diff = recent - older;
if (diff > kTrendDeltaThreshold) trend.trend_direction = "improving";
else if (diff < -kTrendDeltaThreshold) trend.trend_direction = "declining";
else trend.trend_direction = "stable";
}
}
next_quality_trends.push_back(std::move(trend));
}
}
if (quality_trends) *quality_trends = std::move(next_quality_trends);
if (generator_stats) *generator_stats = std::move(next_generator_stats);
if (rejection_summary) *rejection_summary = std::move(next_rejection_summary);
LOG_INFO("DataLoader: Successfully loaded data");
result.ok = true;
} catch (const json::exception& e) {
result.ok = false;
result.error = std::string("JSON error in quality_feedback.json: ") + e.what();
LOG_ERROR(result.error);
}
return result;
}
DataLoader::LoadResult DataLoader::LoadActiveLearning(
std::vector<EmbeddingRegionData>* embedding_regions,
CoverageData* coverage) {
LoadResult result;
std::string path = data_path_ + "/active_learning.json";
if (!path_exists_(path)) return result;
LOG_INFO("DataLoader: Loading " + path);
result.found = true;
std::string content;
std::string read_error;
if (!file_reader_(path, &content, &read_error) || content.empty() || IsWhitespaceOnly(content)) {
result.ok = false;
result.error = read_error.empty() ? "active_learning.json is empty" : read_error;
return result;
}
try {
json data = json::parse(content);
std::vector<EmbeddingRegionData> next_embedding_regions;
CoverageData next_coverage;
if (data.contains("regions") && data["regions"].is_array()) {
int idx = 0;
for (auto& region : data["regions"]) {
EmbeddingRegionData erd;
erd.index = idx++;
erd.sample_count = region.value("sample_count", 0);
erd.domain = region.value("domain", "unknown");
erd.avg_quality = region.value("avg_quality", 0.0f);
next_embedding_regions.push_back(std::move(erd));
}
}
next_coverage.num_regions = data.value("num_regions", 0);
if (embedding_regions) *embedding_regions = std::move(next_embedding_regions);
if (coverage) *coverage = std::move(next_coverage);
LOG_INFO("DataLoader: Successfully loaded active learning data");
result.ok = true;
} catch (const json::exception& e) {
result.ok = false;
result.error = std::string("JSON error in active_learning.json: ") + e.what();
LOG_ERROR(result.error);
}
return result;
}
DataLoader::LoadResult DataLoader::LoadTrainingFeedback(
std::vector<TrainingRunData>* training_runs,
OptimizationData* optimization_data) {
LoadResult result;
std::string path = data_path_ + "/training_feedback.json";
if (!path_exists_(path)) return result;
LOG_INFO("DataLoader: Loading " + path);
result.found = true;
std::string content;
std::string read_error;
if (!file_reader_(path, &content, &read_error) || content.empty() || IsWhitespaceOnly(content)) {
result.ok = false;
result.error = read_error.empty() ? "training_feedback.json is empty" : read_error;
return result;
}
try {
json data = json::parse(content);
std::vector<TrainingRunData> next_training_runs;
OptimizationData next_optimization_data;
if (data.contains("training_runs") && data["training_runs"].is_object()) {
for (auto& [id, run] : data["training_runs"].items()) {
TrainingRunData trd;
trd.run_id = id;
trd.model_name = run.value("model_name", "unknown");
trd.samples_count = run.value("samples_count", 0);
trd.final_loss = run.value("final_loss", 0.0f);
trd.start_time = run.value("start_time", "");
if (run.contains("domain_distribution") && run["domain_distribution"].is_object()) {
for (auto& [domain, count] : run["domain_distribution"].items()) {
trd.domain_distribution[domain] = count.get<int>();
}
}
next_training_runs.push_back(std::move(trd));
}
}
if (data.contains("domain_effectiveness") && data["domain_effectiveness"].is_object()) {
for (auto& [domain, val] : data["domain_effectiveness"].items()) {
next_optimization_data.domain_effectiveness[domain] = val.get<float>();
}
}
if (data.contains("quality_threshold_effectiveness") && data["quality_threshold_effectiveness"].is_object()) {
for (auto& [thresh, val] : data["quality_threshold_effectiveness"].items()) {
next_optimization_data.threshold_sensitivity[thresh] = val.get<float>();
}
}
if (training_runs) *training_runs = std::move(next_training_runs);
if (optimization_data) *optimization_data = std::move(next_optimization_data);
LOG_INFO("DataLoader: Successfully loaded training feedback data");
result.ok = true;
} catch (const json::exception& e) {
result.ok = false;
result.error = std::string("JSON error in training_feedback.json: ") + e.what();
LOG_ERROR(result.error);
}
return result;
}
DataLoader::LoadResult DataLoader::LoadCuratedHacks(
std::vector<CuratedHackEntry>* curated_hacks) {
LoadResult result;
std::string path = data_path_ + "/curated_hacks.json";
if (!path_exists_(path)) {
LOG_WARN("curated_hacks.json not found at " + path);
return result;
}
LOG_INFO("DataLoader: Loading " + path);
result.found = true;
std::string content;
std::string read_error;
if (!file_reader_(path, &content, &read_error) || content.empty() ||
IsWhitespaceOnly(content)) {
result.ok = false;
result.error =
read_error.empty() ? "curated_hacks.json is empty" : read_error;
return result;
}
try {
json data = json::parse(content);
if (!data.contains("hacks") || !data["hacks"].is_array()) {
result.ok = false;
result.error = "curated_hacks.json missing 'hacks' array";
return result;
}
curated_hacks->clear();
for (const auto& hack : data["hacks"]) {
CuratedHackEntry entry;
entry.name = hack.value("name", "");
entry.path = hack.value("path", "");
entry.notes = hack.value("notes", "");
entry.review_status = hack.value("review_status", "");
entry.weight = hack.value("weight", 1.0f);
entry.eligible_files = hack.value("eligible_files", 0);
entry.selected_files = hack.value("selected_files", 0);
entry.org_ratio = hack.value("org_ratio", 0.0f);
entry.address_ratio = hack.value("address_ratio", 0.0f);
entry.avg_comment_ratio = hack.value("avg_comment_ratio", 0.0f);
entry.status = hack.value("status", "");
entry.error = hack.value("error", "");
auto read_string_array = [](const json& arr) {
std::vector<std::string> out;
if (!arr.is_array()) return out;
for (const auto& value : arr) {
if (value.is_string()) out.push_back(value.get<std::string>());
}
return out;
};
if (hack.contains("authors")) entry.authors = read_string_array(hack["authors"]);
if (hack.contains("include_globs")) entry.include_globs = read_string_array(hack["include_globs"]);
if (hack.contains("exclude_globs")) entry.exclude_globs = read_string_array(hack["exclude_globs"]);
if (hack.contains("sample_files")) entry.sample_files = read_string_array(hack["sample_files"]);
curated_hacks->push_back(std::move(entry));
}
result.ok = true;
} catch (const std::exception& e) {
result.ok = false;
result.error = std::string("Failed to parse curated_hacks.json: ") + e.what();
}
return result;
}
DataLoader::LoadResult DataLoader::LoadResourceIndex(ResourceIndexData* resource_index) {
LoadResult result;
std::string path = data_path_ + "/resource_index.json";
if (!path_exists_(path)) {
LOG_WARN("resource_index.json not found at " + path);
return result;
}
LOG_INFO("DataLoader: Loading " + path);
result.found = true;
std::string content;
std::string read_error;
if (!file_reader_(path, &content, &read_error) || content.empty() ||
IsWhitespaceOnly(content)) {
result.ok = false;
result.error = read_error.empty() ? "resource_index.json is empty" : read_error;
return result;
}
try {
json data = json::parse(content);
if (!data.contains("metadata")) {
result.ok = false;
result.error = "resource_index.json missing metadata";
return result;
}
const auto& meta = data["metadata"];
resource_index->total_files = meta.value("total_files", 0);
resource_index->duplicates_found = meta.value("duplicates_found", 0);
resource_index->duration_seconds = meta.value("duration_seconds", 0.0f);
resource_index->indexed_at = meta.value("indexed_at", "");
resource_index->by_source.clear();
resource_index->by_type.clear();
if (meta.contains("by_source")) {
for (auto it = meta["by_source"].begin(); it != meta["by_source"].end(); ++it) {
resource_index->by_source[it.key()] = it.value().get<int>();
}
}
if (meta.contains("by_type")) {
for (auto it = meta["by_type"].begin(); it != meta["by_type"].end(); ++it) {
resource_index->by_type[it.key()] = it.value().get<int>();
}
}
result.ok = true;
} catch (const std::exception& e) {
result.ok = false;
result.error = std::string("Failed to parse resource_index.json: ") + e.what();
}
return result;
}
void DataLoader::MountDrive(const std::string& name) {
auto scawful_root = ResolveHafsScawfulRoot();
std::filesystem::path script_path = scawful_root
? *scawful_root / "scripts" / "mount_windows.sh"
: studio::core::FileSystem::ResolvePath("~/src/trunk/scawful/research/afs_scawful/scripts/mount_windows.sh");
if (studio::core::FileSystem::Exists(script_path)) {
LOG_INFO("DataLoader: Triggering mount using " + script_path.string());
// The script takes an optional argument, but 'mount' is default.
std::string cmd = "bash \"" + script_path.string() + "\" mount 2>&1";
FILE* pipe = popen(cmd.c_str(), "r");
if (pipe) {
char buffer[256];
while (fgets(buffer, sizeof(buffer), pipe)) {
std::string line(buffer);
if (!line.empty() && line.back() == '\n') line.pop_back();
LOG_INFO("Mount output: " + line);
}
pclose(pipe);
}
} else {
LOG_ERROR("DataLoader: Mount script not found: " + script_path.string());
}
}
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,231 @@
#pragma once
#include <functional>
#include <map>
#include <string>
#include <vector>
namespace afs {
namespace viz {
/// Quality trend for a single domain/metric.
struct QualityTrendData {
std::string domain;
std::string metric;
std::vector<float> values;
float mean = 0.0f;
std::string trend_direction; // "improving", "declining", "stable", "insufficient"
};
/// Statistics for a single generator.
struct GeneratorStatsData {
std::string name;
int samples_generated = 0;
int samples_accepted = 0;
int samples_rejected = 0;
std::map<std::string, int> rejection_reasons;
float acceptance_rate = 0.0f;
float avg_quality = 0.0f;
};
/// Embedding space region data.
struct EmbeddingRegionData {
int index = 0;
int sample_count = 0;
std::string domain;
float avg_quality = 0.0f;
};
/// Training run metadata.
struct TrainingRunData {
std::string run_id;
std::string model_name;
std::string base_model;
std::string dataset_path;
std::string start_time;
std::string end_time;
std::string notes;
float final_loss = 0.0f;
int samples_count = 0;
std::map<std::string, int> domain_distribution;
std::map<std::string, float> eval_metrics;
};
/// Embedding coverage summary.
struct CoverageData {
int total_samples = 0;
int num_regions = 0;
float coverage_score = 0.0f;
int sparse_regions = 0;
std::map<std::string, float> domain_coverage;
};
/// Aggregated rejection reasons across all generators.
struct RejectionSummary {
std::map<std::string, int> reasons;
int total_rejections = 0;
};
/// Curated hack entry metadata.
struct CuratedHackEntry {
std::string name;
std::string path;
std::vector<std::string> authors;
std::string notes;
std::string review_status;
float weight = 1.0f;
std::vector<std::string> include_globs;
std::vector<std::string> exclude_globs;
int eligible_files = 0;
int selected_files = 0;
float org_ratio = 0.0f;
float address_ratio = 0.0f;
float avg_comment_ratio = 0.0f;
std::vector<std::string> sample_files;
std::string status;
std::string error;
};
/// Resource index summary for data sources.
struct ResourceIndexData {
int total_files = 0;
int duplicates_found = 0;
float duration_seconds = 0.0f;
std::string indexed_at;
std::map<std::string, int> by_source;
std::map<std::string, int> by_type;
};
/// Optimization metrics.
struct OptimizationData {
std::map<std::string, float> domain_effectiveness;
std::map<std::string, float> threshold_sensitivity;
};
/// Local mount information for the system.
struct MountData {
std::string name;
std::string path;
bool active = false;
};
/// Data load status for the last refresh cycle.
struct LoadStatus {
bool quality_found = false;
bool quality_ok = false;
bool active_found = false;
bool active_ok = false;
bool training_found = false;
bool training_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);
}
int OkCount() const {
return static_cast<int>(quality_ok) + static_cast<int>(active_ok) +
static_cast<int>(training_ok);
}
bool AnyOk() const { return quality_ok || active_ok || training_ok; }
};
/// Loads training data from JSON files.
class DataLoader {
public:
using FileReader = std::function<bool(const std::string&,
std::string*,
std::string*)>;
using PathExists = std::function<bool(const std::string&)>;
explicit DataLoader(const std::string& data_path,
FileReader file_reader = {},
PathExists path_exists = {});
/// Reload all data from disk. Returns true on success.
bool Refresh();
// Accessors
const std::vector<QualityTrendData>& GetQualityTrends() const {
return quality_trends_;
}
const std::vector<GeneratorStatsData>& GetGeneratorStats() const {
return generator_stats_;
}
const std::vector<EmbeddingRegionData>& GetEmbeddingRegions() const {
return embedding_regions_;
}
const std::vector<TrainingRunData>& GetTrainingRuns() const {
return training_runs_;
}
const CoverageData& GetCoverage() const { return coverage_; }
const RejectionSummary& GetRejectionSummary() const {
return rejection_summary_;
}
const OptimizationData& GetOptimizationData() const {
return optimization_data_;
}
const std::vector<CuratedHackEntry>& GetCuratedHacks() const {
return curated_hacks_;
}
const std::string& GetCuratedHacksError() const {
return curated_hacks_error_;
}
const ResourceIndexData& GetResourceIndex() const { return resource_index_; }
const std::string& GetResourceIndexError() const { return resource_index_error_; }
const std::vector<MountData>& GetMounts() const {
return mounts_;
}
const std::map<std::string, bool>& GetDomainVisibility() const { return domain_visibility_; }
const LoadStatus& GetLastStatus() const { return last_status_; }
bool HasData() const { return has_data_; }
std::string GetLastError() const { return last_error_; }
/// Trigger mounting of external drives.
void MountDrive(const std::string& name);
private:
struct LoadResult {
bool found = false;
bool ok = false;
std::string error;
};
LoadResult LoadQualityFeedback(std::vector<QualityTrendData>* quality_trends,
std::vector<GeneratorStatsData>* generator_stats,
RejectionSummary* rejection_summary);
LoadResult LoadActiveLearning(
std::vector<EmbeddingRegionData>* embedding_regions,
CoverageData* coverage);
LoadResult LoadTrainingFeedback(std::vector<TrainingRunData>* training_runs,
OptimizationData* optimization_data);
LoadResult LoadCuratedHacks(std::vector<CuratedHackEntry>* curated_hacks);
LoadResult LoadResourceIndex(ResourceIndexData* resource_index);
std::string data_path_;
FileReader file_reader_;
PathExists path_exists_;
bool has_data_ = false;
std::string last_error_;
LoadStatus last_status_;
std::vector<QualityTrendData> quality_trends_;
std::vector<GeneratorStatsData> generator_stats_;
std::vector<EmbeddingRegionData> embedding_regions_;
std::vector<TrainingRunData> training_runs_;
CoverageData coverage_;
RejectionSummary rejection_summary_;
OptimizationData optimization_data_;
std::vector<CuratedHackEntry> curated_hacks_;
std::string curated_hacks_error_;
ResourceIndexData resource_index_;
std::string resource_index_error_;
std::vector<MountData> mounts_;
std::map<std::string, bool> domain_visibility_;
};
} // namespace viz
} // namespace afs

2197
apps/studio/src/icons.h Normal file

File diff suppressed because it is too large Load Diff

41
apps/studio/src/main.cc Normal file
View File

@@ -0,0 +1,41 @@
/// AFS Training Data Visualization - Main Entry Point
///
/// Usage: afs_viz [data_path]
/// data_path: Path to training data directory (default: ~/.context/training)
///
/// Build:
/// cmake -B build -S src/cc -DAFS_BUILD_VIZ=ON
/// cmake --build build
///
/// Keys:
/// F5 - Refresh data
/// Ctrl+Q - Quit
/// Ctrl+/ - Shortcut editor
#include <iostream>
#include <string>
#include "app.h"
#include "core/logger.h"
#include "core/filesystem.h"
int main(int argc, char* argv[]) {
using afs::studio::core::FileSystem;
// Determine data path
std::string data_path_str;
if (argc > 1) {
data_path_str = argv[1];
} else {
data_path_str = "~/.context/training";
}
std::filesystem::path data_path = FileSystem::ResolvePath(data_path_str);
LOG_INFO("AFS Studio Starting...");
LOG_INFO("Data path: " + data_path.string());
LOG_INFO("Press F5 to refresh data");
afs::viz::App app(data_path.string());
return app.Run();
}

View File

@@ -0,0 +1,257 @@
#pragma once
#include <string>
#include <vector>
#include <deque>
#include <filesystem>
#include <array>
#include <map>
#include <imgui.h>
namespace afs {
namespace viz {
enum class Workspace { Dashboard, Analysis, Optimization, Systems, Custom, Training, Context, Models };
enum class ThemeProfile { Cobalt, Amber, Emerald, Cyberpunk, Monochrome, Solarized, Nord, Dracula, Default = Cobalt };
enum class PlotKind {
None,
QualityTrends,
GeneratorEfficiency,
CoverageDensity,
TrainingLoss,
LossVsSamples,
DomainCoverage,
EmbeddingQuality,
AgentThroughput,
MissionQueue,
QualityDirection,
GeneratorMix,
EmbeddingDensity,
AgentUtilization,
MissionProgress,
EvalMetrics,
Rejections,
KnowledgeGraph,
LatentSpace,
Effectiveness,
Thresholds,
MountsStatus,
};
enum class GraphViewMode { Single, Compare, Overview };
enum class GraphCategory {
Training,
Quality,
System,
Coverage,
Embedding,
Optimization,
All
};
struct MetricCard {
std::string label;
std::string value;
std::string sub_text;
ImVec4 color;
bool positive_trend = true;
};
struct AgentState {
std::string name;
std::string role;
std::string status;
bool enabled = true;
bool data_backed = false;
int queue_depth = 0;
int tasks_completed = 0;
float success_rate = 0.0f;
float avg_latency_ms = 0.0f;
float cpu_pct = 0.0f;
float mem_pct = 0.0f;
float activity_phase = 0.0f;
};
struct MissionState {
std::string name;
std::string owner;
std::string status;
bool data_backed = false;
int priority = 2;
float progress = 0.0f;
};
struct LogEntry {
std::string agent;
std::string message;
std::string kind;
};
struct FileEntry {
std::string name;
std::filesystem::path path;
bool is_directory;
uintmax_t size;
bool has_context = false;
};
struct ContextItem {
std::string name;
std::filesystem::path path;
std::string type;
bool enabled = true;
};
struct AppState {
// App-level flags
bool should_refresh = false;
bool show_demo_window = false;
bool auto_refresh = false;
bool simulate_activity = true;
bool verbose_logs = false;
bool compact_charts = false;
bool show_status_strip = true;
bool show_controls = true;
bool show_inspector = true;
bool show_dataset_panel = true;
bool show_systems_panel = true;
bool show_chat_panel = false;
bool show_quality_trends = false; // Default off, let layout init handle it
bool show_generator_efficiency = false;
bool show_coverage_density = false;
bool enable_viewports = true;
bool enable_docking = true;
bool reset_layout_on_workspace_change = false;
bool allow_workspace_scroll = false;
bool enable_plot_interaction = true;
bool plot_interaction_requires_modifier = true;
bool auto_chart_columns = true;
bool show_agent_details = true;
bool show_knowledge_graph = false;
// Visual/Grid Config
float refresh_interval_sec = 8.0f;
float chart_height = 170.0f;
float plot_height = 170.0f;
float agent_activity_scale = 1.0f;
float embedding_sample_rate = 0.6f;
float quality_threshold = 0.7f;
float mission_priority_bias = 1.0f;
int mission_concurrency = 4;
int chart_columns = 3;
int spawn_agent_count = 1;
int spawn_mission_count = 1;
int new_mission_priority = 3;
int log_agent_index = 0;
int selected_agent_index = -1;
int selected_run_index = -1;
std::string selected_run_id;
std::vector<std::string> compared_run_ids;
int selected_generator_index = -1;
std::string selected_generator_name;
PlotKind focus_plot = PlotKind::None;
std::map<std::string, bool> domain_visibility;
Workspace current_workspace = Workspace::Dashboard;
ThemeProfile current_theme = ThemeProfile::Cobalt;
bool force_reset_layout = false;
bool lock_layout = false;
double last_refresh_time = 0.0;
// Advanced Interaction
std::vector<PlotKind> active_floaters;
int layout_preset = 0; // 0: Default, 1: Analyst, 2: System
PlotKind inspector_context = PlotKind::None;
// Graph View System
GraphViewMode graph_view_mode = GraphViewMode::Single;
PlotKind active_graph = PlotKind::None;
std::vector<PlotKind> graph_history;
std::vector<PlotKind> graph_bookmarks;
int graph_history_index = -1;
bool show_graph_browser = true;
bool show_companion_panels = true;
GraphCategory browser_filter = GraphCategory::All;
std::string graph_search_query;
// Per-graph filter settings
struct GraphFilterSettings {
std::vector<std::string> active_domains;
std::string active_run_id;
float time_range_start = 0.0f;
float time_range_end = 1.0f;
};
std::map<PlotKind, GraphFilterSettings> graph_filters;
// Comparison State
int compare_run_a = -1;
int compare_run_b = -1;
bool show_comparison_view = false;
// Chart Styling
float line_weight = 2.4f;
// Data Collections
std::vector<AgentState> agents;
std::vector<MissionState> missions;
std::deque<LogEntry> logs;
std::vector<float> sparkline_data;
// Input Buffers
std::array<char, 64> new_agent_name{};
std::array<char, 64> new_agent_role{};
std::array<char, 96> new_mission_name{};
std::array<char, 64> new_mission_owner{};
std::array<char, 128> log_filter{};
std::array<char, 96> run_filter{};
std::array<char, 96> generator_filter{};
std::array<char, 256> chat_input{};
std::array<char, 1024> system_prompt{};
std::array<char, 1024> user_prompt{};
// Trainer Config
float trainer_lr = 0.0005f;
int trainer_epochs = 10;
int trainer_batch_size = 32;
float generator_temp = 0.7f;
float rejection_threshold = 0.65f;
// Context Browser
std::filesystem::path current_browser_path;
std::vector<FileEntry> browser_entries;
std::vector<ContextItem> selected_context;
std::string context_filter;
std::filesystem::path selected_file_path;
bool show_hidden_files = false;
// Editor State
std::vector<uint8_t> binary_data;
bool is_binary_view = false;
// UI State Components
bool show_advanced_tables = true;
bool show_sparklines = true;
bool use_pulse_animations = true;
bool show_plot_legends = true;
bool show_plot_markers = true;
bool data_scientist_mode = false;
bool show_all_charts = true;
float pulse_timer = 0.0f;
int custom_grid_rows = 2;
int custom_grid_columns = 2;
PlotKind focus_chart = PlotKind::None; // Preferred over expanded_plot for dash integration
PlotKind expanded_plot = PlotKind::None;
bool is_rendering_expanded_plot = false;
std::vector<PlotKind> custom_grid_slots;
// Knowledge Graph
std::vector<std::string> knowledge_concepts;
std::vector<float> knowledge_nodes_x;
std::vector<float> knowledge_nodes_y;
struct Edge { int from, to; };
std::vector<Edge> knowledge_edges;
};
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,125 @@
#pragma once
#include "imgui.h"
#include "../models/state.h" // For ThemeProfile enum
namespace afs::viz::themes {
/// Apply the AFS dark theme to ImGui with specific profiles.
inline void ApplyHafsTheme(ThemeProfile profile = ThemeProfile::Cobalt) {
ImGuiStyle& style = ImGui::GetStyle();
// Rounding & Padding for a GIMP-like technical look
style.WindowRounding = 0.0f; // Professional tools often use square windows
style.FrameRounding = 2.0f;
style.GrabRounding = 2.0f;
style.PopupRounding = 2.0f;
style.ScrollbarRounding = 2.0f;
style.TabRounding = 0.0f; // Square tabs
style.WindowPadding = ImVec2(8, 8); // Tighter
style.FramePadding = ImVec2(6, 4); // Compact input fields
style.ItemSpacing = ImVec2(8, 4); // Denser layout
style.ItemInnerSpacing = ImVec2(4, 4);
style.CellPadding = ImVec2(4, 2);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
ImVec4* colors = style.Colors;
// Base background (Deep neutral)
colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.08f, 1.00f);
colors[ImGuiCol_ChildBg] = ImVec4(0.06f, 0.06f, 0.08f, 0.90f); // Darker, less transparent
colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.10f, 0.98f);
colors[ImGuiCol_Border] = ImVec4(1.0f, 1.0f, 1.0f, 0.08f);
// Profile specific colors
ImVec4 primary, secondary, accent;
if (profile == ThemeProfile::Cobalt) {
primary = ImVec4(0.0f, 0.48f, 1.0f, 1.00f); // Vivid Azure
secondary = ImVec4(0.12f, 0.14f, 0.18f, 1.00f); // Darker Midnight for contrast
accent = ImVec4(0.0f, 0.85f, 1.0f, 1.0f); // Neon Cyan
} else if (profile == ThemeProfile::Amber) {
primary = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); // Bright Orange
secondary = ImVec4(0.20f, 0.10f, 0.05f, 1.00f); // Deep Rust
accent = ImVec4(1.00f, 0.90f, 0.30f, 1.00f); // Electric Gold
} else if (profile == ThemeProfile::Emerald) {
primary = ImVec4(0.00f, 0.85f, 0.45f, 1.00f); // Neon Green
secondary = ImVec4(0.05f, 0.15f, 0.10f, 1.00f); // Dark Jungle
accent = ImVec4(0.40f, 1.00f, 0.60f, 1.00f); // Bright Mint
} else if (profile == ThemeProfile::Cyberpunk) {
primary = ImVec4(1.0f, 0.0f, 0.5f, 1.0f); // Hot Pink
secondary = ImVec4(0.1f, 0.0f, 0.2f, 1.0f); // Deep Purple
accent = ImVec4(0.0f, 1.0f, 1.0f, 1.0f); // Cyan
} else if (profile == ThemeProfile::Monochrome) {
primary = ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
secondary = ImVec4(0.1f, 0.1f, 0.1f, 1.00f);
accent = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
} else if (profile == ThemeProfile::Solarized) {
primary = ImVec4(0.15f, 0.45f, 0.55f, 1.0f); // Blue
secondary = ImVec4(0.03f, 0.21f, 0.26f, 1.00f); // Base03
accent = ImVec4(0.52f, 0.60f, 0.00f, 1.0f); // Green
} else if (profile == ThemeProfile::Nord) {
primary = ImVec4(0.53f, 0.75f, 0.82f, 1.0f); // Frost
secondary = ImVec4(0.18f, 0.20f, 0.25f, 1.00f); // Polar Night
accent = ImVec4(0.56f, 0.80f, 0.71f, 1.0f); // Frost/Teal
} else if (profile == ThemeProfile::Dracula) {
primary = ImVec4(0.74f, 0.57f, 0.97f, 1.0f); // Purple
secondary = ImVec4(0.16f, 0.17f, 0.24f, 1.00f); // Background
accent = ImVec4(1.00f, 0.47f, 0.77f, 1.0f); // Pink
} else { // Emerald (Fallback if Default or Cobalt)
primary = ImVec4(0.00f, 0.85f, 0.45f, 1.00f); // Neon Green
secondary = ImVec4(0.05f, 0.15f, 0.10f, 1.00f); // Dark Jungle
accent = ImVec4(0.40f, 1.00f, 0.60f, 1.00f); // Bright Mint
}
// Apply profile to components
colors[ImGuiCol_Header] = secondary;
colors[ImGuiCol_HeaderHovered] = primary;
colors[ImGuiCol_HeaderActive] = accent;
colors[ImGuiCol_Button] = secondary;
colors[ImGuiCol_ButtonHovered] = primary;
colors[ImGuiCol_ButtonActive] = accent;
colors[ImGuiCol_FrameBg] = ImVec4(1.0f, 1.0f, 1.0f, 0.04f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(1.0f, 1.0f, 1.0f, 0.10f);
colors[ImGuiCol_FrameBgActive] = ImVec4(1.0f, 1.0f, 1.0f, 0.16f);
colors[ImGuiCol_Tab] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f);
colors[ImGuiCol_TabHovered] = primary;
colors[ImGuiCol_TabActive] = secondary;
colors[ImGuiCol_TabUnfocused] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f);
colors[ImGuiCol_TabUnfocusedActive] = secondary;
colors[ImGuiCol_TitleBg] = colors[ImGuiCol_WindowBg];
colors[ImGuiCol_TitleBgActive] = colors[ImGuiCol_WindowBg];
colors[ImGuiCol_TitleBgCollapsed] = colors[ImGuiCol_WindowBg];
colors[ImGuiCol_PlotLines] = primary;
colors[ImGuiCol_PlotLinesHovered] = accent;
colors[ImGuiCol_PlotHistogram] = primary;
colors[ImGuiCol_PlotHistogramHovered] = accent;
colors[ImGuiCol_Text] = ImVec4(0.95f, 0.95f, 1.00f, 1.00f);
colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f);
colors[ImGuiCol_Separator] = colors[ImGuiCol_Border];
}
/// Apply a light theme variant.
inline void ApplyHafsLightTheme() {
ImGui::StyleColorsLight();
ImGuiStyle& style = ImGui::GetStyle();
style.WindowRounding = 4.0f;
style.FrameRounding = 2.0f;
style.GrabRounding = 2.0f;
ImVec4* colors = style.Colors;
colors[ImGuiCol_PlotLines] = ImVec4(0.20f, 0.50f, 0.80f, 1.00f);
colors[ImGuiCol_PlotHistogram] = ImVec4(0.20f, 0.70f, 0.50f, 1.00f);
}
} // namespace afs::viz::themes

View File

@@ -0,0 +1,25 @@
#pragma once
#include <string>
#include <implot.h>
#include "../../models/state.h"
#include "../../data_loader.h"
namespace afs::viz::ui {
class Chart {
public:
virtual ~Chart() = default;
// Render the chart content.
// Return true if the chart is visible/active, false if closed.
virtual void Render(AppState& state, const DataLoader& loader) = 0;
// Optional: Get the chart title/ID
virtual std::string GetTitle() const = 0;
// Optional: Get the chart kind
virtual PlotKind GetKind() const = 0;
};
} // namespace afs::viz::ui

View File

@@ -0,0 +1,85 @@
#include "coverage_density.h"
#include "../core.h"
#include <vector>
namespace afs::viz::ui {
void CoverageDensityChart::Render(AppState& state, const DataLoader& loader) {
RenderChartHeader(PlotKind::CoverageDensity,
"DENSITY COVERAGE",
"Displays sample counts across latent space regions. Sparse regions (<50% of avg) indicate under-sampled scenarios.",
state);
const auto& regions = loader.GetEmbeddingRegions();
if (regions.empty()) {
ImGui::TextDisabled("No embedding coverage data available");
return;
}
// Scatter plot of region densities
std::vector<float> dense_x, dense_y, sparse_x, sparse_y;
float total = 0.0f;
for (const auto& r : regions) total += static_cast<float>(r.sample_count);
float avg = total / static_cast<float>(regions.size());
for (size_t i = 0; i < regions.size(); ++i) {
float x = static_cast<float>(i);
float y = static_cast<float>(regions[i].sample_count);
if (y < avg * 0.5f) {
sparse_x.push_back(x);
sparse_y.push_back(y);
} else {
dense_x.push_back(x);
dense_y.push_back(y);
}
}
ImPlotFlags plot_flags = BasePlotFlags(state, true);
ApplyPremiumPlotStyles("##Coverage", state);
if (ImPlot::BeginPlot("##Coverage", ImGui::GetContentRegionAvail(), plot_flags)) {
ImPlotAxisFlags axis_flags = static_cast<ImPlotAxisFlags>(GetPlotAxisFlags(state));
ImPlot::SetupAxes("Region Index", "Samples", axis_flags, axis_flags);
if (state.show_plot_legends) {
ImPlot::SetupLegend(ImPlotLocation_NorthEast, ImPlotLegendFlags_None);
}
HandlePlotContextMenu(PlotKind::CoverageDensity, state);
// Low Density Zone Overlay
double lx[2] = {-10, static_cast<double>(regions.size() + 10)};
double ly1[2] = {0, 0};
double ly2[2] = {avg * 0.5, avg * 0.5};
ImPlot::SetNextFillStyle(ImVec4(1, 0.5f, 0, 0.1f));
ImPlot::PlotShaded("Sparse Zone", lx, ly1, ly2, 2);
ImVec4 healthy_color = GetSeriesColor(2);
ImVec4 risk_color = GetSeriesColor(7);
ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 4, healthy_color);
ImPlot::PlotScatter("Healthy", dense_x.data(), dense_y.data(), (int)dense_x.size());
ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 4, risk_color);
ImPlot::PlotScatter("At Risk", sparse_x.data(), sparse_y.data(), (int)sparse_x.size());
// Custom Tooltip
if (ImPlot::IsPlotHovered()) {
ImPlotPoint mouse = ImPlot::GetPlotMousePos();
int idx = (int)std::round(mouse.x);
if (idx >= 0 && idx < (int)regions.size()) {
ImGui::BeginTooltip();
ImGui::Text("Region Index: %d", idx);
ImGui::Text("Samples: %d", regions[idx].sample_count);
ImGui::Text("Avg Quality: %.3f", regions[idx].avg_quality);
ImGui::Separator();
ImGui::TextDisabled("Status: %s", regions[idx].sample_count < avg * 0.5f ? "Sparse (Under-sampled)" : "Healthy Density");
ImGui::EndTooltip();
}
}
ImPlot::EndPlot();
}
ImPlot::PopStyleColor(2);
ImPlot::PopStyleVar(6);
}
} // namespace afs::viz::ui

View File

@@ -0,0 +1,16 @@
#pragma once
#include "chart.h"
#include "../../data_loader.h"
#include "../../models/state.h"
namespace afs::viz::ui {
class CoverageDensityChart : public Chart {
public:
void Render(AppState& state, const DataLoader& loader) override;
std::string GetTitle() const override { return "Density Coverage"; }
PlotKind GetKind() const override { return PlotKind::CoverageDensity; }
};
} // namespace afs::viz::ui

View File

@@ -0,0 +1,97 @@
#include "generator_efficiency.h"
#include "../core.h"
#include <algorithm>
#include <vector>
#include <string>
namespace afs::viz::ui {
void GeneratorEfficiencyChart::Render(AppState& state, const DataLoader& loader) {
RenderChartHeader(PlotKind::GeneratorEfficiency,
"GENERATOR EFFICIENCY",
"Acceptance rates for active data generators. Rates < 40% (Warning Zone) indicate generators struggling with current model constraints.",
state);
const auto& stats = loader.GetGeneratorStats();
if (stats.empty()) {
ImGui::TextDisabled("No generator stats available");
return;
}
struct GeneratorRow {
std::string name;
float rate = 0.0f;
};
std::vector<GeneratorRow> rows;
rows.reserve(stats.size());
for (const auto& s : stats) {
std::string name = s.name;
size_t pos = name.find("DataGenerator");
if (pos != std::string::npos) name = name.substr(0, pos);
rows.push_back({name, s.acceptance_rate * 100.0f});
}
std::sort(rows.begin(), rows.end(),
[](const auto& a, const auto& b) { return a.rate > b.rate; });
std::vector<const char*> labels;
std::vector<float> rates;
std::vector<std::string> label_storage;
labels.reserve(rows.size());
rates.reserve(rows.size());
label_storage.reserve(rows.size());
for (const auto& row : rows) {
label_storage.push_back(row.name);
rates.push_back(row.rate);
}
for (const auto& s : label_storage) labels.push_back(s.c_str());
if (!rows.empty()) {
const auto& top = rows.front();
const auto& bottom = rows.back();
ImGui::TextDisabled("Top: %s (%.1f%%) | Bottom: %s (%.1f%%)",
top.name.c_str(), top.rate, bottom.name.c_str(), bottom.rate);
}
ImPlotFlags plot_flags = BasePlotFlags(state, false);
ApplyPremiumPlotStyles("##GeneratorStats", state);
if (ImPlot::BeginPlot("##GeneratorStats", ImGui::GetContentRegionAvail(), plot_flags)) {
ImPlotAxisFlags axis_flags = static_cast<ImPlotAxisFlags>(GetPlotAxisFlags(state));
ImPlot::SetupAxes("Generator", "Acceptance %", axis_flags, axis_flags);
ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, 100.0, ImPlotCond_Once);
if (!labels.empty()) {
ImPlot::SetupAxisTicks(ImAxis_X1, 0, static_cast<double>(labels.size() - 1),
static_cast<int>(labels.size()), labels.data());
}
HandlePlotContextMenu(PlotKind::GeneratorEfficiency, state);
// Warning Zone Overlay
double wx[2] = {-1, 100};
double wy1[2] = {0, 0};
double wy2[2] = {40, 40};
ImPlot::SetNextFillStyle(ImVec4(1, 0, 0, 0.1f));
ImPlot::PlotShaded("Low Efficiency", wx, wy1, wy2, 2);
ImPlot::SetNextFillStyle(GetSeriesColor(1), 0.8f);
ImPlot::PlotBars("Rate", rates.data(), static_cast<int>(rates.size()), 0.67);
// Custom Tooltip
if (ImPlot::IsPlotHovered()) {
ImPlotPoint mouse = ImPlot::GetPlotMousePos();
int idx = (int)std::round(mouse.x);
if (idx >= 0 && idx < (int)rates.size()) {
ImGui::BeginTooltip();
ImGui::Text("Generator: %s", label_storage[idx].c_str());
ImGui::Text("Acceptance Rate: %.1f%%", rates[idx]);
ImGui::Separator();
ImGui::TextDisabled("Status: %s", rates[idx] < 40.0f ? "WARNING (Low Efficiency)" : "Healthy");
ImGui::EndTooltip();
}
}
ImPlot::EndPlot();
}
ImPlot::PopStyleColor(2);
ImPlot::PopStyleVar(6);
}
} // namespace afs::viz::ui

View File

@@ -0,0 +1,16 @@
#pragma once
#include "chart.h"
#include "../../data_loader.h"
#include "../../models/state.h"
namespace afs::viz::ui {
class GeneratorEfficiencyChart : public Chart {
public:
void Render(AppState& state, const DataLoader& loader) override;
std::string GetTitle() const override { return "Generator Efficiency"; }
PlotKind GetKind() const override { return PlotKind::GeneratorEfficiency; }
};
} // namespace afs::viz::ui

View File

@@ -0,0 +1,118 @@
#include "quality_trends.h"
#include "../core.h"
#include <algorithm>
#include <vector>
namespace afs::viz::ui {
void QualityTrendsChart::Render(AppState& state, const DataLoader& loader) {
RenderChartHeader(PlotKind::QualityTrends,
"QUALITY TRENDS",
"Displays model performance metrics across active training domains. Solid lines indicate scores; shaded area indicates the Optimal Strategy Zone (>0.85).",
state);
const auto& trends = loader.GetQualityTrends();
if (trends.empty()) {
ImGui::TextDisabled("No quality trend data available");
return;
}
size_t max_len = 0;
for (const auto& trend : trends) {
max_len = std::max(max_len, trend.values.size());
}
std::vector<float> mean_values;
if (max_len > 0) {
mean_values.assign(max_len, 0.0f);
std::vector<int> counts(max_len, 0);
for (const auto& trend : trends) {
if (state.domain_visibility.count(trend.domain) && !state.domain_visibility.at(trend.domain)) continue;
for (size_t i = 0; i < trend.values.size(); ++i) {
mean_values[i] += trend.values[i];
counts[i] += 1;
}
}
for (size_t i = 0; i < max_len; ++i) {
if (counts[i] > 0) mean_values[i] /= static_cast<float>(counts[i]);
}
}
ImPlotFlags plot_flags = BasePlotFlags(state, true);
ApplyPremiumPlotStyles("##QualityTrends", state);
if (ImPlot::BeginPlot("##QualityTrends", ImGui::GetContentRegionAvail(), plot_flags)) {
ImPlotAxisFlags axis_flags = static_cast<ImPlotAxisFlags>(GetPlotAxisFlags(state));
ImPlot::SetupAxes("Time Step", "Score (0-1)", axis_flags, axis_flags);
ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, 1.1, ImPlotCond_Always);
if (state.show_plot_legends) {
ImPlot::SetupLegend(ImPlotLocation_NorthEast, ImPlotLegendFlags_None);
}
HandlePlotContextMenu(PlotKind::QualityTrends, state);
// Help markers and goal regions...
double goal_x[2] = {-100, 1000};
double goal_y1[2] = {static_cast<double>(state.quality_threshold), static_cast<double>(state.quality_threshold)};
double goal_y2[2] = {1.1, 1.1};
ImPlot::SetNextFillStyle(ImVec4(0, 1, 0, 0.05f));
ImPlot::PlotShaded("Goal Region", goal_x, goal_y1, goal_y2, 2);
ImPlot::SetNextLineStyle(ImVec4(0, 1, 0, 0.4f), 1.0f);
ImPlot::PlotLine("Requirement", goal_x, goal_y1, 2);
int color_index = 0;
for (const auto& trend : trends) {
if (trend.values.empty()) continue;
if (state.domain_visibility.count(trend.domain) && !state.domain_visibility.at(trend.domain)) {
color_index++;
continue;
}
std::string label = trend.domain + " (" + trend.metric + ")";
ImVec4 series_color = GetSeriesColor(color_index++);
ImPlot::SetNextLineStyle(series_color, 2.2f);
if (state.show_plot_markers) {
ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 4.0f, series_color);
}
ImPlot::SetNextFillStyle(series_color, 0.12f);
ImPlot::PlotLine(label.c_str(), trend.values.data(), (int)trend.values.size());
// Annotation for latest value
if (!trend.values.empty()) {
float last_val = trend.values.back();
ImPlot::Annotation((double)(trend.values.size() - 1), (double)last_val,
series_color, ImVec2(10, -10), true, "%.2f", last_val);
}
}
if (!mean_values.empty()) {
ImPlot::SetNextLineStyle(ImVec4(1, 1, 1, 0.7f), 2.0f);
ImPlot::PlotLine("Mean", mean_values.data(), static_cast<int>(mean_values.size()));
}
// Custom Tooltip
if (ImPlot::IsPlotHovered()) {
ImPlotPoint mouse = ImPlot::GetPlotMousePos();
int x = (int)std::round(mouse.x);
if (x >= 0 && x < (int)max_len) {
ImGui::BeginTooltip();
ImGui::Text("Step: %d", x);
ImGui::Separator();
for (const auto& trend : trends) {
if (x < (int)trend.values.size()) {
if (state.domain_visibility.count(trend.domain) && !state.domain_visibility.at(trend.domain)) continue;
ImGui::ColorButton("##color", GetSeriesColor(&trend - &trends[0]), ImGuiColorEditFlags_NoTooltip);
ImGui::SameLine();
ImGui::Text("%s: %.3f", trend.domain.c_str(), trend.values[x]);
}
}
ImGui::EndTooltip();
}
}
ImPlot::EndPlot();
}
ImPlot::PopStyleColor(2);
ImPlot::PopStyleVar(6);
}
} // namespace afs::viz::ui

View File

@@ -0,0 +1,14 @@
#pragma once
#include "chart.h"
namespace afs::viz::ui {
class QualityTrendsChart : public Chart {
public:
void Render(AppState& state, const DataLoader& loader) override;
std::string GetTitle() const override { return "Quality Trends"; }
PlotKind GetKind() const override { return PlotKind::QualityTrends; }
};
} // namespace afs::viz::ui

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
#pragma once
#include <imgui.h>
#include "../../models/state.h"
#include "../../data_loader.h"
namespace afs {
namespace viz {
namespace ui {
void RenderQualityChart(AppState& state, const DataLoader& loader);
void RenderGeneratorChart(AppState& state, const DataLoader& loader);
void RenderCoverageChart(AppState& state, const DataLoader& loader);
void RenderTrainingChart(AppState& state, const DataLoader& loader);
void RenderTrainingLossChart(AppState& state, const DataLoader& loader);
void RenderRejectionChart(AppState& state, const DataLoader& loader);
void RenderQualityDirectionChart(AppState& state, const DataLoader& loader);
void RenderGeneratorMixChart(AppState& state, const DataLoader& loader);
void RenderEmbeddingDensityChart(AppState& state, const DataLoader& loader);
void RenderAgentUtilizationChart(AppState& state, const DataLoader& loader);
void RenderMissionProgressChart(AppState& state, const DataLoader& loader);
void RenderEvalMetricsChart(AppState& state, const DataLoader& loader);
void RenderEffectivenessChart(AppState& state, const DataLoader& loader);
void RenderThresholdOptimizationChart(AppState& state, const DataLoader& loader);
void RenderMountsChart(AppState& state, const DataLoader& loader);
void RenderDomainCoverageChart(AppState& state, const DataLoader& loader);
void RenderEmbeddingQualityChart(AppState& state, const DataLoader& loader);
void RenderAgentThroughputChart(AppState& state, const DataLoader& loader);
void RenderMissionQueueChart(AppState& state, const DataLoader& loader);
void RenderLatentSpaceChart(AppState& state, const DataLoader& loader);
void RenderPlotByKind(PlotKind kind, AppState& state, const DataLoader& loader);
} // namespace ui
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,282 @@
#include "companion_panels.h"
#include "graph_browser.h"
#include "../core.h"
#include "../../icons.h"
#include <imgui.h>
namespace afs::viz::ui {
CompanionPanels::PanelVisibility CompanionPanels::GetPanelVisibility(PlotKind kind) const {
PanelVisibility vis;
// Define which panels each graph type needs
switch (kind) {
case PlotKind::QualityTrends:
case PlotKind::GeneratorEfficiency:
case PlotKind::CoverageDensity:
case PlotKind::TrainingLoss:
case PlotKind::DomainCoverage:
case PlotKind::EmbeddingQuality:
case PlotKind::Rejections:
case PlotKind::EvalMetrics:
case PlotKind::EmbeddingDensity:
case PlotKind::LatentSpace:
vis.filter = true;
vis.data_quality = true;
vis.inspector = true;
vis.controls = true;
break;
case PlotKind::GeneratorMix:
case PlotKind::Effectiveness:
case PlotKind::Thresholds:
case PlotKind::LossVsSamples:
case PlotKind::QualityDirection:
vis.filter = true;
vis.data_quality = true;
vis.controls = true;
break;
case PlotKind::AgentUtilization:
case PlotKind::AgentThroughput:
case PlotKind::MountsStatus:
vis.data_quality = true;
vis.inspector = true;
break;
case PlotKind::MissionProgress:
case PlotKind::MissionQueue:
case PlotKind::KnowledgeGraph:
vis.controls = true;
break;
default:
break;
}
return vis;
}
void CompanionPanels::Render(AppState& state, const DataLoader& loader) {
if (!state.show_companion_panels || state.active_graph == PlotKind::None) {
return;
}
auto visibility = GetPanelVisibility(state.active_graph);
// Render each panel in a collapsible section
if (visibility.filter) {
if (ImGui::CollapsingHeader(ICON_MD_FILTER_ALT " Filters", ImGuiTreeNodeFlags_DefaultOpen)) {
RenderFilterPanel(state, loader);
}
ImGui::Spacing();
}
if (visibility.data_quality) {
if (ImGui::CollapsingHeader(ICON_MD_BAR_CHART " Data Quality", ImGuiTreeNodeFlags_DefaultOpen)) {
RenderDataQualityPanel(state, loader);
}
ImGui::Spacing();
}
if (visibility.inspector) {
if (ImGui::CollapsingHeader(ICON_MD_INFO " Inspector", ImGuiTreeNodeFlags_DefaultOpen)) {
RenderInspectorPanel(state, loader);
}
ImGui::Spacing();
}
if (visibility.controls) {
if (ImGui::CollapsingHeader(ICON_MD_TUNE " Controls", ImGuiTreeNodeFlags_DefaultOpen)) {
RenderControlsPanel(state);
}
}
}
void CompanionPanels::RenderFilterPanel(AppState& state, const DataLoader& loader) {
// Get or create filter settings for this graph
auto& filters = state.graph_filters[state.active_graph];
// Domain filtering
const auto& domain_vis = loader.GetDomainVisibility();
if (!domain_vis.empty()) {
ImGui::Text("Domains:");
ImGui::Indent();
for (const auto& [domain, visible] : domain_vis) {
bool is_active = std::find(filters.active_domains.begin(),
filters.active_domains.end(),
domain) != filters.active_domains.end();
if (filters.active_domains.empty()) {
// If no explicit filter, show all
is_active = true;
}
if (ImGui::Checkbox(domain.c_str(), &is_active)) {
if (is_active) {
if (std::find(filters.active_domains.begin(),
filters.active_domains.end(), domain) == filters.active_domains.end()) {
filters.active_domains.push_back(domain);
}
} else {
filters.active_domains.erase(
std::remove(filters.active_domains.begin(),
filters.active_domains.end(), domain),
filters.active_domains.end()
);
}
}
}
if (ImGui::Button("Clear All")) {
filters.active_domains.clear();
}
ImGui::SameLine();
if (ImGui::Button("Select All")) {
filters.active_domains.clear();
for (const auto& [domain, _] : domain_vis) {
filters.active_domains.push_back(domain);
}
}
ImGui::Unindent();
ImGui::Spacing();
}
// Run filtering (for training graphs)
const auto& runs = loader.GetTrainingRuns();
if (!runs.empty()) {
ImGui::Text("Training Run:");
ImGui::SetNextItemWidth(-FLT_MIN);
if (ImGui::BeginCombo("##RunFilter",
filters.active_run_id.empty() ? "All Runs" : filters.active_run_id.c_str())) {
if (ImGui::Selectable("All Runs", filters.active_run_id.empty())) {
filters.active_run_id.clear();
}
for (const auto& run : runs) {
bool is_selected = filters.active_run_id == run.run_id;
if (ImGui::Selectable(run.run_id.c_str(), is_selected)) {
filters.active_run_id = run.run_id;
}
}
ImGui::EndCombo();
}
}
}
void CompanionPanels::RenderDataQualityPanel(AppState& state, const DataLoader& loader) {
const auto& status = loader.GetLastStatus();
// Data freshness indicator
ImVec4 freshness_color = ImVec4(0.2f, 0.8f, 0.2f, 1.0f); // Green
const char* freshness_text = "Fresh";
double time_since_refresh = ImGui::GetTime() - state.last_refresh_time;
if (time_since_refresh > state.refresh_interval_sec * 2) {
freshness_color = ImVec4(0.8f, 0.2f, 0.2f, 1.0f); // Red
freshness_text = "Stale";
} else if (time_since_refresh > state.refresh_interval_sec) {
freshness_color = ImVec4(0.8f, 0.8f, 0.2f, 1.0f); // Yellow
freshness_text = "Aging";
}
ImGui::TextColored(freshness_color, ICON_MD_CIRCLE);
ImGui::SameLine();
ImGui::Text("%s", freshness_text);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Last refresh: %.1fs ago", time_since_refresh);
ImGui::EndTooltip();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Data source status
ImGui::Text("Data Sources:");
ImGui::Indent();
if (status.quality_ok) {
ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), ICON_MD_CHECK);
ImGui::SameLine();
ImGui::Text("Quality Trends (%d samples)", static_cast<int>(loader.GetQualityTrends().size()));
}
if (status.active_ok) {
ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), ICON_MD_CHECK);
ImGui::SameLine();
const auto& cov = loader.GetCoverage();
ImGui::Text("Coverage (%d samples)", cov.total_samples);
}
if (status.training_ok) {
ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), ICON_MD_CHECK);
ImGui::SameLine();
ImGui::Text("Training Runs (%d)", static_cast<int>(loader.GetTrainingRuns().size()));
}
// Generator stats check removed - not in LoadStatus
ImGui::Unindent();
if (status.error_count > 0) {
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.8f, 0.2f, 0.2f, 1.0f), ICON_MD_WARNING " %d errors", status.error_count);
if (!status.last_error.empty() && ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("%s", status.last_error.c_str());
ImGui::EndTooltip();
}
}
}
void CompanionPanels::RenderInspectorPanel(AppState& state, const DataLoader& loader) {
// Note: ImPlot::IsPlotHovered() can only be called within BeginPlot/EndPlot.
// Since this panel renders separately, we show static info instead.
ImGui::TextDisabled("Hover over graph for details");
// Graph-specific inspection data
ImGui::Separator();
ImGui::Spacing();
// Show currently active graph info
const char* graph_name = "None";
switch (state.active_graph) {
case PlotKind::QualityTrends: graph_name = "Quality Trends"; break;
case PlotKind::GeneratorEfficiency: graph_name = "Generator Efficiency"; break;
case PlotKind::CoverageDensity: graph_name = "Coverage Density"; break;
case PlotKind::TrainingLoss: graph_name = "Training Loss"; break;
case PlotKind::DomainCoverage: graph_name = "Domain Coverage"; break;
case PlotKind::EmbeddingQuality: graph_name = "Embedding Quality"; break;
case PlotKind::Rejections: graph_name = "Rejections"; break;
case PlotKind::EvalMetrics: graph_name = "Eval Metrics"; break;
case PlotKind::AgentUtilization: graph_name = "Agent Utilization"; break;
case PlotKind::MountsStatus: graph_name = "Mounts Status"; break;
default: break;
}
ImGui::Text("Graph: %s", graph_name);
ImGui::TextDisabled("Click points for detailed info");
}
void CompanionPanels::RenderControlsPanel(AppState& state) {
ImGui::Text("Visual Settings:");
ImGui::Spacing();
ImGui::SliderFloat("Line Weight", &state.line_weight, 1.0f, 5.0f, "%.1f");
ImGui::Checkbox("Show Markers", &state.show_plot_markers);
ImGui::Checkbox("Show Legend", &state.show_plot_legends);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Graph Height:");
ImGui::SliderFloat("##PlotHeight", &state.plot_height, 100.0f, 600.0f, "%.0f px");
}
} // namespace afs::viz::ui

View File

@@ -0,0 +1,33 @@
#pragma once
#include <string>
#include "../../models/state.h"
#include "../../data_loader.h"
namespace afs::viz::ui {
class CompanionPanels {
public:
CompanionPanels() = default;
// Render all active companion panels for the current graph
void Render(AppState& state, const DataLoader& loader);
private:
void RenderFilterPanel(AppState& state, const DataLoader& loader);
void RenderDataQualityPanel(AppState& state, const DataLoader& loader);
void RenderInspectorPanel(AppState& state, const DataLoader& loader);
void RenderControlsPanel(AppState& state);
// Helper to determine which panels should be visible
struct PanelVisibility {
bool filter = false;
bool data_quality = false;
bool inspector = false;
bool controls = false;
};
PanelVisibility GetPanelVisibility(PlotKind kind) const;
};
} // namespace afs::viz::ui

View File

@@ -0,0 +1,73 @@
#include "tabs.h"
#include "../../icons.h"
#include "../core.h"
#include <implot.h>
#include <algorithm>
namespace afs {
namespace viz {
namespace ui {
void RenderComparisonView(AppState& state, const DataLoader& loader, ImFont* font_ui, ImFont* font_header) {
const auto& runs = loader.GetTrainingRuns();
// Header
if (font_header) ImGui::PushFont(font_header);
ImGui::Text(ICON_MD_COMPARE " MODEL COMPARISON");
if (font_header) ImGui::PopFont();
ImGui::Separator();
ImGui::Columns(2, "CompareColumns", true);
auto render_selector = [&](int& selected_idx, const char* label) {
std::string preview = (selected_idx >= 0 && selected_idx < runs.size())
? runs[selected_idx].run_id
: "Select Model...";
if (ImGui::BeginCombo(label, preview.c_str())) {
for (int i = 0; i < (int)runs.size(); ++i) {
bool is_selected = (selected_idx == i);
if (ImGui::Selectable(runs[i].run_id.c_str(), is_selected)) {
selected_idx = i;
}
}
ImGui::EndCombo();
}
};
render_selector(state.compare_run_a, "##ModelA");
ImGui::NextColumn();
render_selector(state.compare_run_b, "##ModelB");
ImGui::NextColumn();
ImGui::Separator();
if (state.compare_run_a >= 0 && state.compare_run_b >= 0) {
const auto& run_a = runs[state.compare_run_a];
const auto& run_b = runs[state.compare_run_b];
// Detailed diffing logic would go here
ImGui::Columns(2, "MetricDiff", false);
auto draw_metric = [&](const char* name, float val_a, float val_b) {
ImGui::Text("%s", name);
ImGui::NextColumn();
ImGui::Text("%.4f vs %.4f", val_a, val_b);
float diff = val_b - val_a;
ImGui::SameLine();
if (diff > 0.001f) ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), " (▲%.2f%%)", (diff/val_a)*100.0f);
else if (diff < -0.001f) ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), " (▼%.2f%%)", (std::abs(diff)/val_a)*100.0f);
ImGui::NextColumn();
};
draw_metric("Final Loss", run_a.final_loss, run_b.final_loss);
// ... more metrics
ImGui::Columns(1);
} else {
ImGui::TextDisabled("Select two models above to compare performance metrics.");
}
}
} // namespace ui
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,367 @@
#include "deployment_panel.h"
#include <imgui.h>
#include <cstring>
namespace afs {
namespace studio {
namespace ui {
DeploymentPanel::DeploymentPanel() {
// Default test prompt
std::strncpy(test_prompt_buffer_.data(),
"Write a simple NOP instruction in 65816 assembly:",
test_prompt_buffer_.size() - 1);
}
void DeploymentPanel::Render(const ModelMetadata* selected_model) {
if (!selected_model) {
ImGui::TextDisabled("Select a model to view deployment options.");
return;
}
const auto& model = *selected_model;
// Header
ImGui::Text("%s", model.display_name.empty() ? model.model_id.c_str()
: model.display_name.c_str());
ImGui::Separator();
// Status display
if (is_busy_ || !status_message_.empty()) {
RenderDeploymentStatus();
}
// Quick actions
RenderQuickActions(model);
ImGui::Spacing();
ImGui::Separator();
// Ollama deployment section
if (ImGui::CollapsingHeader("Deploy to Ollama",
ImGuiTreeNodeFlags_DefaultOpen)) {
RenderOllamaDeployment(model);
}
// Conversion options
if (ImGui::CollapsingHeader("Format Conversion")) {
RenderConversionOptions(model);
}
// Test prompt
if (ImGui::CollapsingHeader("Test Model")) {
RenderTestPrompt(model);
}
}
void DeploymentPanel::RenderDeploymentStatus() {
if (is_busy_) {
ImGui::TextColored(ImVec4(0.2f, 0.7f, 0.9f, 1.0f), "%s",
current_operation_.c_str());
ImGui::ProgressBar(progress_, ImVec2(-1, 0));
}
if (!status_message_.empty()) {
bool is_error = !last_error_.empty();
ImVec4 color = is_error ? ImVec4(0.9f, 0.3f, 0.3f, 1.0f)
: ImVec4(0.3f, 0.9f, 0.3f, 1.0f);
ImGui::TextColored(color, "%s", status_message_.c_str());
}
if (!last_error_.empty()) {
ImGui::TextWrapped("Error: %s", last_error_.c_str());
}
ImGui::Spacing();
}
void DeploymentPanel::RenderQuickActions(const ModelMetadata& model) {
// Check current state
bool is_local = model.locations.count("mac") > 0;
bool has_gguf = false;
for (const auto& fmt : model.formats) {
if (fmt == "gguf") {
has_gguf = true;
break;
}
}
bool in_ollama = false;
for (const auto& backend : model.deployed_backends) {
if (backend == "ollama") {
in_ollama = true;
break;
}
}
// Quick action buttons
ImGui::Text("Quick Actions:");
// Pull button
ImGui::BeginDisabled(is_busy_ || is_local);
if (ImGui::Button("Pull to Mac")) {
is_busy_ = true;
current_operation_ = "Pulling model...";
progress_ = 0.1f;
status_message_.clear();
last_error_.clear();
// Execute pull
last_result_ = actions_.PullModel(model.model_id);
is_busy_ = false;
if (last_result_.status == ActionStatus::kCompleted) {
status_message_ = "Model pulled successfully";
} else {
status_message_ = "Pull failed";
last_error_ = last_result_.error;
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (!is_local) {
ImGui::TextDisabled("(not local)");
} else {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "(local)");
}
ImGui::SameLine(0, 20);
// Convert button
ImGui::BeginDisabled(is_busy_ || !is_local || has_gguf);
if (ImGui::Button("Convert to GGUF")) {
is_busy_ = true;
current_operation_ = "Converting to GGUF...";
progress_ = 0.2f;
status_message_.clear();
last_error_.clear();
auto quant_opts = GetQuantizationOptions();
std::string quant = quant_opts[selected_quantization_].name;
last_result_ = actions_.ConvertToGGUF(model.model_id, quant);
is_busy_ = false;
if (last_result_.status == ActionStatus::kCompleted) {
status_message_ = "Conversion complete";
} else {
status_message_ = "Conversion failed";
last_error_ = last_result_.error;
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (has_gguf) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "(has GGUF)");
}
ImGui::SameLine(0, 20);
// Deploy button
ImGui::BeginDisabled(is_busy_ || in_ollama);
if (ImGui::Button("Deploy to Ollama")) {
is_busy_ = true;
current_operation_ = "Deploying to Ollama...";
progress_ = 0.3f;
status_message_.clear();
last_error_.clear();
std::string name(ollama_name_buffer_.data());
auto quant_opts = GetQuantizationOptions();
std::string quant = quant_opts[selected_quantization_].name;
last_result_ = actions_.DeployToOllama(model.model_id, name, quant);
is_busy_ = false;
if (last_result_.status == ActionStatus::kCompleted) {
status_message_ = "Deployed to Ollama";
} else {
status_message_ = "Deployment failed";
last_error_ = last_result_.error;
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (in_ollama) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "(in Ollama)");
}
}
void DeploymentPanel::RenderOllamaDeployment(const ModelMetadata& model) {
ImGui::Indent();
// Ollama name input
ImGui::Text("Ollama Model Name:");
ImGui::SameLine();
ImGui::SetNextItemWidth(200);
ImGui::InputTextWithHint("##OllamaName", model.model_id.c_str(),
ollama_name_buffer_.data(),
ollama_name_buffer_.size());
// Quantization selection
ImGui::Text("Quantization:");
auto quant_opts = GetQuantizationOptions();
for (int i = 0; i < static_cast<int>(quant_opts.size()); ++i) {
ImGui::SameLine();
if (ImGui::RadioButton(quant_opts[i].name.c_str(),
selected_quantization_ == i)) {
selected_quantization_ = i;
}
}
// Current quantization info
const auto& selected_quant = quant_opts[selected_quantization_];
ImGui::TextDisabled("%s (%.0f%% of F16 size)",
selected_quant.description.c_str(),
selected_quant.size_ratio * 100.0f);
// Status indicators
ImGui::Spacing();
bool ollama_running = actions_.IsOllamaRunning();
bool llama_available = actions_.IsLlamaCppAvailable();
ImGui::TextDisabled("Prerequisites:");
ImGui::BulletText("Ollama: %s",
ollama_running ? "Running" : "Not running");
if (!ollama_running) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.9f, 0.6f, 0.2f, 1.0f), "(run: ollama serve)");
}
ImGui::BulletText("llama.cpp: %s",
llama_available ? "Available" : "Not found");
ImGui::Unindent();
}
void DeploymentPanel::RenderConversionOptions(const ModelMetadata& model) {
ImGui::Indent();
// Current formats
ImGui::Text("Available formats:");
for (const auto& fmt : model.formats) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 0.7f, 0.9f, 1.0f), "[%s]", fmt.c_str());
}
if (model.formats.empty()) {
ImGui::SameLine();
ImGui::TextDisabled("(none)");
}
ImGui::Spacing();
// Conversion buttons
ImGui::BeginDisabled(is_busy_);
if (ImGui::Button("Convert to GGUF (Q4_K_M)")) {
is_busy_ = true;
current_operation_ = "Converting to GGUF Q4_K_M...";
last_result_ = actions_.ConvertToGGUF(model.model_id, "Q4_K_M");
is_busy_ = false;
status_message_ = last_result_.status == ActionStatus::kCompleted
? "Conversion complete"
: "Conversion failed";
if (last_result_.status != ActionStatus::kCompleted) {
last_error_ = last_result_.error;
}
}
ImGui::SameLine();
if (ImGui::Button("Convert to GGUF (Q5_K_M)")) {
is_busy_ = true;
current_operation_ = "Converting to GGUF Q5_K_M...";
last_result_ = actions_.ConvertToGGUF(model.model_id, "Q5_K_M");
is_busy_ = false;
status_message_ = last_result_.status == ActionStatus::kCompleted
? "Conversion complete"
: "Conversion failed";
if (last_result_.status != ActionStatus::kCompleted) {
last_error_ = last_result_.error;
}
}
ImGui::EndDisabled();
ImGui::Unindent();
}
void DeploymentPanel::RenderTestPrompt(const ModelMetadata& model) {
ImGui::Indent();
// Check if model is deployed
bool can_test = false;
std::string deployed_to;
for (const auto& backend : model.deployed_backends) {
if (backend == "ollama") {
can_test = true;
deployed_to = "ollama";
break;
}
}
if (!can_test) {
ImGui::TextDisabled("Deploy model to a backend first to test.");
ImGui::Unindent();
return;
}
// Test prompt input
ImGui::Text("Test Prompt:");
ImGui::SetNextItemWidth(-1);
ImGui::InputTextMultiline("##TestPrompt", test_prompt_buffer_.data(),
test_prompt_buffer_.size(), ImVec2(0, 60));
// Run test button
ImGui::BeginDisabled(is_busy_);
if (ImGui::Button("Run Test")) {
is_busy_ = true;
current_operation_ = "Testing model...";
status_message_.clear();
last_error_.clear();
test_output_.clear();
last_result_ = actions_.TestModel(model.model_id, DeploymentBackend::kOllama,
std::string(test_prompt_buffer_.data()));
is_busy_ = false;
if (last_result_.status == ActionStatus::kCompleted) {
status_message_ = "Test completed";
test_output_ = last_result_.output;
} else {
status_message_ = "Test failed";
last_error_ = last_result_.error;
}
}
ImGui::EndDisabled();
// Test output
if (!test_output_.empty()) {
ImGui::Spacing();
ImGui::Text("Response:");
ImGui::BeginChild("TestOutput", ImVec2(0, 100), true);
ImGui::TextWrapped("%s", test_output_.c_str());
ImGui::EndChild();
}
ImGui::Unindent();
}
void RenderDeploymentPanelWindow(DeploymentPanel& panel,
const ModelMetadata* model,
bool* open) {
if (!open || !*open) return;
ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Deployment", open)) {
ImGui::End();
return;
}
panel.Render(model);
ImGui::End();
}
} // namespace ui
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,55 @@
#pragma once
#include "core/deployment_actions.h"
#include "core/registry_reader.h"
#include <array>
#include <string>
namespace afs {
namespace studio {
namespace ui {
// Deployment panel for model actions
class DeploymentPanel {
public:
DeploymentPanel();
// Render the panel (typically shown when a model is selected)
void Render(const ModelMetadata* selected_model);
// Check if an operation is in progress
bool IsBusy() const { return is_busy_; }
private:
void RenderDeploymentStatus();
void RenderQuickActions(const ModelMetadata& model);
void RenderOllamaDeployment(const ModelMetadata& model);
void RenderConversionOptions(const ModelMetadata& model);
void RenderTestPrompt(const ModelMetadata& model);
DeploymentActions actions_;
// UI State
bool is_busy_ = false;
std::string current_operation_;
float progress_ = 0.0f;
std::string status_message_;
std::string last_error_;
ActionResult last_result_;
// Deployment options
int selected_quantization_ = 1; // Default to Q4_K_M
std::array<char, 64> ollama_name_buffer_{};
std::array<char, 512> test_prompt_buffer_{};
std::string test_output_;
};
// Render as standalone window
void RenderDeploymentPanelWindow(DeploymentPanel& panel,
const ModelMetadata* model,
bool* open);
} // namespace ui
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,256 @@
#include "graph_browser.h"
#include "../core.h"
#include "../../icons.h"
#include <imgui.h>
#include <algorithm>
namespace afs::viz::ui {
GraphBrowser::GraphBrowser() {
InitializeGraphRegistry();
}
void GraphBrowser::InitializeGraphRegistry() {
all_graphs_ = {
// 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},
// Quality Category
{PlotKind::QualityTrends, "Quality Trends", "Data quality trends by domain", GraphCategory::Quality, true, true, true, true},
{PlotKind::QualityDirection, "Quality Direction", "Quality improvement/degradation tracking", GraphCategory::Quality, false, true, true, false},
{PlotKind::EmbeddingQuality, "Embedding Quality", "Embedding space quality metrics", GraphCategory::Quality, false, true, true, true},
{PlotKind::Effectiveness, "Effectiveness", "Generator effectiveness analysis", GraphCategory::Quality, false, true, true, false},
// System Category
{PlotKind::AgentUtilization, "Agent Utilization", "Agent resource usage and activity", GraphCategory::System, false, false, true, true},
{PlotKind::AgentThroughput, "Agent Throughput", "Agent task completion rates", GraphCategory::System, false, false, true, true},
{PlotKind::MissionProgress, "Mission Progress", "Mission completion tracking", GraphCategory::System, false, false, false, false},
{PlotKind::MissionQueue, "Mission Queue", "Mission queue depth over time", GraphCategory::System, false, false, false, false},
{PlotKind::MountsStatus, "Mounts Status", "Filesystem mount status", GraphCategory::System, false, false, true, false},
// Coverage Category
{PlotKind::CoverageDensity, "Coverage Density", "Data coverage density heatmap", GraphCategory::Coverage, false, true, true, true},
{PlotKind::DomainCoverage, "Domain Coverage", "Per-domain coverage analysis", GraphCategory::Coverage, false, true, true, true},
// Embedding Category
{PlotKind::EmbeddingDensity, "Embedding Density", "Embedding space density visualization", GraphCategory::Embedding, false, true, true, true},
{PlotKind::LatentSpace, "Latent Space", "2D latent space projection", GraphCategory::Embedding, false, true, true, true},
{PlotKind::KnowledgeGraph, "Knowledge Graph", "Knowledge concept relationships", GraphCategory::Embedding, false, false, false, false},
// Optimization Category
{PlotKind::GeneratorEfficiency, "Generator Efficiency", "Generator performance metrics", GraphCategory::Optimization, true, true, true, true},
{PlotKind::GeneratorMix, "Generator Mix", "Generator usage distribution", GraphCategory::Optimization, false, true, true, false},
{PlotKind::Rejections, "Rejections", "Sample rejection analysis", GraphCategory::Optimization, false, true, true, true},
{PlotKind::Thresholds, "Threshold Optimization", "Quality threshold optimization curves", GraphCategory::Optimization, false, true, true, false},
{PlotKind::EvalMetrics, "Eval Metrics", "Evaluation metric comparisons", GraphCategory::Optimization, true, true, true, false},
};
}
const char* GraphBrowser::GetCategoryName(GraphCategory category) {
switch (category) {
case GraphCategory::Training: return "Training";
case GraphCategory::Quality: return "Quality";
case GraphCategory::System: return "System";
case GraphCategory::Coverage: return "Coverage";
case GraphCategory::Embedding: return "Embedding";
case GraphCategory::Optimization: return "Optimization";
case GraphCategory::All: return "All Graphs";
default: return "Unknown";
}
}
std::vector<GraphInfo> GraphBrowser::GetFilteredGraphs(GraphCategory category, const std::string& search) const {
std::vector<GraphInfo> filtered;
for (const auto& graph : all_graphs_) {
// Category filter
if (category != GraphCategory::All && graph.category != category) {
continue;
}
// Search filter
if (!search.empty()) {
std::string lower_name = graph.name;
std::string lower_search = search;
std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower);
std::transform(lower_search.begin(), lower_search.end(), lower_search.begin(), ::tolower);
if (lower_name.find(lower_search) == std::string::npos) {
continue;
}
}
filtered.push_back(graph);
}
return filtered;
}
const GraphInfo* GraphBrowser::GetGraphInfo(PlotKind kind) const {
for (const auto& graph : all_graphs_) {
if (graph.kind == kind) {
return &graph;
}
}
return nullptr;
}
void GraphBrowser::Render(AppState& state) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
// Search bar
ImGui::SetNextItemWidth(-FLT_MIN);
char search_buf[128];
strncpy(search_buf, state.graph_search_query.c_str(), sizeof(search_buf) - 1);
search_buf[sizeof(search_buf) - 1] = '\0';
if (ImGui::InputTextWithHint("##GraphSearch", ICON_MD_SEARCH " Search graphs...",
search_buf, sizeof(search_buf))) {
state.graph_search_query = search_buf;
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Category filter tabs
if (ImGui::BeginTabBar("GraphCategories", ImGuiTabBarFlags_FittingPolicyScroll)) {
const GraphCategory categories[] = {
GraphCategory::All, GraphCategory::Training, GraphCategory::Quality,
GraphCategory::System, GraphCategory::Coverage, GraphCategory::Embedding,
GraphCategory::Optimization
};
for (auto cat : categories) {
if (ImGui::BeginTabItem(GetCategoryName(cat))) {
state.browser_filter = cat;
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
ImGui::Spacing();
// Bookmarks section
if (!state.graph_bookmarks.empty()) {
if (ImGui::CollapsingHeader(ICON_MD_BOOKMARK " Bookmarks", ImGuiTreeNodeFlags_DefaultOpen)) {
for (auto kind : state.graph_bookmarks) {
const GraphInfo* info = GetGraphInfo(kind);
if (info) {
RenderGraphItem(*info, state);
}
}
}
ImGui::Spacing();
}
// Recent graphs
if (!state.graph_history.empty()) {
if (ImGui::CollapsingHeader(ICON_MD_ACCESS_TIME " Recent", ImGuiTreeNodeFlags_DefaultOpen)) {
// Show last 5 unique graphs
std::vector<PlotKind> recent_unique;
for (auto it = state.graph_history.rbegin();
it != state.graph_history.rend() && recent_unique.size() < 5; ++it) {
if (std::find(recent_unique.begin(), recent_unique.end(), *it) == recent_unique.end()) {
recent_unique.push_back(*it);
}
}
for (auto kind : recent_unique) {
const GraphInfo* info = GetGraphInfo(kind);
if (info) {
RenderGraphItem(*info, state);
}
}
}
ImGui::Spacing();
}
// All graphs (filtered)
ImGui::Separator();
ImGui::Spacing();
auto filtered = GetFilteredGraphs(state.browser_filter, state.graph_search_query);
if (filtered.empty()) {
ImGui::TextDisabled("No graphs found");
} else {
ImGui::BeginChild("GraphList", ImVec2(0, 0), false);
for (const auto& graph : filtered) {
RenderGraphItem(graph, state);
}
ImGui::EndChild();
}
ImGui::PopStyleVar();
}
void GraphBrowser::RenderGraphItem(const GraphInfo& info, AppState& state) {
bool is_active = state.active_graph == info.kind;
bool is_bookmarked = std::find(state.graph_bookmarks.begin(),
state.graph_bookmarks.end(),
info.kind) != state.graph_bookmarks.end();
ImGui::PushID(static_cast<int>(info.kind));
if (is_active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.26f, 0.59f, 0.98f, 0.40f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.26f, 0.59f, 0.98f, 0.60f));
}
if (ImGui::Button(info.name.c_str(), ImVec2(-FLT_MIN, 0))) {
// Navigate to this graph
if (state.active_graph != info.kind) {
// Add to history (truncate forward history if we're not at the end)
if (state.graph_history_index >= 0 &&
state.graph_history_index < static_cast<int>(state.graph_history.size()) - 1) {
state.graph_history.erase(
state.graph_history.begin() + state.graph_history_index + 1,
state.graph_history.end()
);
}
state.graph_history.push_back(info.kind);
state.graph_history_index = static_cast<int>(state.graph_history.size()) - 1;
state.active_graph = info.kind;
}
}
if (is_active) {
ImGui::PopStyleColor(2);
}
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("%s", info.description.c_str());
ImGui::TextDisabled("Category: %s", GetCategoryName(info.category));
if (info.supports_comparison) {
ImGui::TextDisabled(ICON_MD_COMPARE " Supports comparison");
}
ImGui::EndTooltip();
}
// Context menu for bookmarking
if (ImGui::BeginPopupContextItem()) {
if (is_bookmarked) {
if (ImGui::MenuItem(ICON_MD_BOOKMARK " Remove Bookmark")) {
state.graph_bookmarks.erase(
std::remove(state.graph_bookmarks.begin(),
state.graph_bookmarks.end(), info.kind),
state.graph_bookmarks.end()
);
}
} else {
if (ImGui::MenuItem(ICON_MD_BOOKMARK_BORDER " Add Bookmark")) {
state.graph_bookmarks.push_back(info.kind);
}
}
ImGui::EndPopup();
}
ImGui::PopID();
}
} // namespace afs::viz::ui

View File

@@ -0,0 +1,48 @@
#pragma once
#include <string>
#include <vector>
#include "../../models/state.h"
namespace afs::viz::ui {
// Graph metadata for browser display
struct GraphInfo {
PlotKind kind;
std::string name;
std::string description;
GraphCategory category;
bool supports_comparison;
bool needs_filter_panel;
bool needs_data_quality_panel;
bool needs_inspector_panel;
};
class GraphBrowser {
public:
GraphBrowser();
// Render the graph browser sidebar
void Render(AppState& state);
// 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;
// Get graph info by kind
const GraphInfo* GetGraphInfo(PlotKind kind) const;
// Get category name
static const char* GetCategoryName(GraphCategory category);
private:
std::vector<GraphInfo> all_graphs_;
void RenderCategorySection(const char* title, GraphCategory category, AppState& state);
void RenderGraphItem(const GraphInfo& info, AppState& state);
void InitializeGraphRegistry();
};
} // namespace afs::viz::ui

View File

@@ -0,0 +1,153 @@
#include "graph_navigator.h"
#include "graph_browser.h"
#include "../core.h"
#include "../../icons.h"
#include <imgui.h>
#include <algorithm>
namespace afs::viz::ui {
bool GraphNavigator::CanNavigateBack(const AppState& state) const {
return state.graph_history_index > 0;
}
bool GraphNavigator::CanNavigateForward(const AppState& state) const {
return state.graph_history_index >= 0 &&
state.graph_history_index < static_cast<int>(state.graph_history.size()) - 1;
}
bool GraphNavigator::IsBookmarked(const AppState& state, PlotKind kind) const {
return std::find(state.graph_bookmarks.begin(),
state.graph_bookmarks.end(), kind) != state.graph_bookmarks.end();
}
void GraphNavigator::NavigateBack(AppState& state) {
if (CanNavigateBack(state)) {
state.graph_history_index--;
state.active_graph = state.graph_history[state.graph_history_index];
}
}
void GraphNavigator::NavigateForward(AppState& state) {
if (CanNavigateForward(state)) {
state.graph_history_index++;
state.active_graph = state.graph_history[state.graph_history_index];
}
}
void GraphNavigator::NavigateToGraph(AppState& state, PlotKind kind) {
if (state.active_graph == kind) return;
// Truncate forward history
if (state.graph_history_index >= 0 &&
state.graph_history_index < static_cast<int>(state.graph_history.size()) - 1) {
state.graph_history.erase(
state.graph_history.begin() + state.graph_history_index + 1,
state.graph_history.end()
);
}
state.graph_history.push_back(kind);
state.graph_history_index = static_cast<int>(state.graph_history.size()) - 1;
state.active_graph = kind;
}
void GraphNavigator::ToggleBookmark(AppState& state, PlotKind kind) {
auto it = std::find(state.graph_bookmarks.begin(), state.graph_bookmarks.end(), kind);
if (it != state.graph_bookmarks.end()) {
state.graph_bookmarks.erase(it);
} else {
state.graph_bookmarks.push_back(kind);
}
}
void GraphNavigator::RenderToolbar(AppState& state, const GraphBrowser& browser) {
// Navigation buttons
ImGui::BeginDisabled(!CanNavigateBack(state));
if (ImGui::Button(ICON_MD_ARROW_BACK)) {
NavigateBack(state);
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("Back (Alt+Left)");
}
ImGui::SameLine();
ImGui::BeginDisabled(!CanNavigateForward(state));
if (ImGui::Button(ICON_MD_ARROW_FORWARD)) {
NavigateForward(state);
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("Forward (Alt+Right)");
}
ImGui::SameLine();
ImGui::SameLine();
ImGui::Separator();
ImGui::SameLine();
// Bookmark button (disabled if no graph selected)
ImGui::BeginDisabled(state.active_graph == PlotKind::None);
bool is_bookmarked = IsBookmarked(state, state.active_graph);
if (is_bookmarked) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
}
if (ImGui::Button(is_bookmarked ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER)) {
ToggleBookmark(state, state.active_graph);
}
if (is_bookmarked) {
ImGui::PopStyleColor();
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(is_bookmarked ? "Remove Bookmark (Ctrl+D)" : "Add Bookmark (Ctrl+D)");
}
ImGui::SameLine();
ImGui::SameLine();
ImGui::Separator();
ImGui::SameLine();
// Breadcrumbs
RenderBreadcrumbs(state, browser);
}
void GraphNavigator::RenderBreadcrumbs(AppState& state, const GraphBrowser& browser) {
if (state.active_graph == PlotKind::None) {
ImGui::TextDisabled("No graph selected");
return;
}
const GraphInfo* info = browser.GetGraphInfo(state.active_graph);
if (!info) {
ImGui::Text("Unknown Graph");
return;
}
// Category > Graph Name
ImGui::Text(ICON_MD_SHOW_CHART);
ImGui::SameLine();
ImGui::TextDisabled("%s", GraphBrowser::GetCategoryName(info->category));
ImGui::SameLine();
ImGui::Text(">");
ImGui::SameLine();
ImGui::Text("%s", info->name.c_str());
// Show history depth
if (state.graph_history.size() > 0) {
ImGui::SameLine();
ImGui::TextDisabled("(%d / %d)",
state.graph_history_index + 1,
static_cast<int>(state.graph_history.size()));
}
}
} // namespace afs::viz::ui

View File

@@ -0,0 +1,32 @@
#pragma once
#include <string>
#include "../../models/state.h"
namespace afs::viz::ui {
class GraphBrowser; // Forward declaration
class GraphNavigator {
public:
GraphNavigator() = default;
// Render navigation toolbar (breadcrumbs, back/forward)
void RenderToolbar(AppState& state, const GraphBrowser& browser);
// Navigation actions
void NavigateBack(AppState& state);
void NavigateForward(AppState& state);
void NavigateToGraph(AppState& state, PlotKind kind);
void ToggleBookmark(AppState& state, PlotKind kind);
// Check navigation state
bool CanNavigateBack(const AppState& state) const;
bool CanNavigateForward(const AppState& state) const;
bool IsBookmarked(const AppState& state, PlotKind kind) const;
private:
void RenderBreadcrumbs(AppState& state, const GraphBrowser& browser);
};
} // namespace afs::viz::ui

View File

@@ -0,0 +1,185 @@
#include "metrics.h"
#include "../core.h"
#include "../../icons.h"
#include <cmath>
#include <algorithm>
#include <GLFW/glfw3.h> // For glfwGetTime if needed, but AppState has last_refresh_time
namespace afs {
namespace viz {
namespace ui {
void RenderMetricCards(AppState& state, const DataLoader& loader, ImFont* font_ui, ImFont* font_header) {
const auto& trends = loader.GetQualityTrends();
float total_success = 0.0f;
for (const auto& agent : state.agents) {
total_success += agent.success_rate;
}
if (!state.agents.empty())
total_success /= static_cast<float>(state.agents.size());
float avg_quality = 0.0f;
if (!trends.empty()) {
for (const auto& t : trends)
avg_quality += t.mean;
avg_quality /= static_cast<float>(trends.size());
}
char q_buf[32], a_buf[32];
snprintf(q_buf, sizeof(q_buf), "%d%%", static_cast<int>(avg_quality * 100));
snprintf(a_buf, sizeof(a_buf), "%d%%", static_cast<int>(total_success * 100));
MetricCard cards[] = {
{ICON_MD_INSIGHTS " Overall Quality", q_buf, "System Health",
GetThemeColor(ImGuiCol_PlotLines, state.current_theme), true},
{ICON_MD_SPEED " Swarm Velocity", "1.2k/s", "+12% vs last run",
ImVec4(0.4f, 1.0f, 0.6f, 1.0f), true},
{ICON_MD_AUTO_FIX_HIGH " Efficiency", a_buf, "Mean Success Rate", ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
total_success > 0.85f}};
float card_w = (ImGui::GetContentRegionAvail().x - 16) / 3.0f;
for (int i = 0; i < 3; ++i) {
ImGui::PushID(i);
ImGui::BeginChild("Card", ImVec2(card_w, 100), true, ImGuiWindowFlags_NoScrollbar);
// Label
if (font_ui) ImGui::PushFont(font_ui);
ImGui::TextDisabled("%s", cards[i].label.c_str());
if (font_ui) ImGui::PopFont();
// Value
if (font_header) ImGui::PushFont(font_header);
ImGui::TextColored(cards[i].color, "%s", cards[i].value.c_str());
if (font_header) ImGui::PopFont();
// Subtext
ImGui::Spacing();
ImGui::TextDisabled("%s", cards[i].sub_text.c_str());
// Decorative Pulse (Bottom edge)
if (state.use_pulse_animations) {
float p = (1.0f + std::sin(state.pulse_timer * 2.0f + i)) * 0.5f;
ImDrawList* draw = ImGui::GetWindowDrawList();
ImVec2 p_min = ImGui::GetItemRectMin(); // Note: Not used correctly in original, but let's keep pattern
ImVec2 p_max = ImGui::GetWindowPos();
p_max.x += ImGui::GetWindowSize().x;
p_max.y += ImGui::GetWindowSize().y;
draw->AddRectFilled(ImVec2(p_max.x - 40 * p, p_max.y - 2), p_max, ImColor(cards[i].color));
}
ImGui::EndChild();
if (i < 2) ImGui::SameLine();
ImGui::PopID();
}
}
void RenderSummaryRow(AppState& state, const DataLoader& loader, ImFont* font_ui, ImFont* font_header) {
RenderMetricCards(state, loader, font_ui, font_header);
ImGui::Spacing();
// Swarm Topology Overview
ImGui::BeginChild("SwarmTopology", ImVec2(0, 120), true);
if (font_header) ImGui::PushFont(font_header);
ImGui::Text(ICON_MD_HUB " SWARM TOPOLOGY");
if (font_header) ImGui::PopFont();
ImGui::Separator();
if (ImGui::BeginTable("Topology", 5, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoSavedSettings)) {
ImGui::TableSetupColumn("Active Agents", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Queue Depth", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Mission Velocity", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Avg. Success", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Health", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
int active_count = 0;
float total_success = 0.0f;
int total_queue = 0;
for (const auto& a : state.agents) {
if (a.enabled) active_count++;
total_success += a.success_rate;
total_queue += a.queue_depth;
}
if (!state.agents.empty()) total_success /= (float)state.agents.size();
ImGui::TableNextRow();
// Column 0: Active Agents
ImGui::TableSetColumnIndex(0);
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.6f, 1.0f), "%d / %d", active_count, (int)state.agents.size());
// Column 1: Queue Depth
ImGui::TableSetColumnIndex(1);
ImGui::Text("%d Tasks", total_queue);
// Column 2: Mission Velocity
ImGui::TableSetColumnIndex(2);
float avg_progress = 0.0f;
for (const auto& m : state.missions) avg_progress += m.progress;
if (!state.missions.empty()) avg_progress /= (float)state.missions.size();
ImGui::ProgressBar(avg_progress, ImVec2(-1, 0), "");
// Column 3: Success Rate
ImGui::TableSetColumnIndex(3);
ImGui::Text("%.1f%%", total_success * 100.0f);
// Column 4: Health
ImGui::TableSetColumnIndex(4);
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.6f, 1.0f), ICON_MD_VERIFIED_USER " NOMINAL");
ImGui::EndTable();
}
ImGui::EndChild();
}
void RenderStatusBar(AppState& state, const DataLoader& loader, const std::string& data_path) {
ImGui::Separator();
if (ImGui::BeginTable("StatusStrip", 2,
ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableNextColumn();
const auto& status = loader.GetLastStatus();
int sources_found = status.FoundCount();
int sources_ok = status.OkCount();
if (loader.HasData()) {
double seconds_since = std::max(0.0, glfwGetTime() - state.last_refresh_time);
if (status.error_count > 0) {
ImGui::Text(
"Sources: %d/%d ok | Generators: %zu | Regions: %zu | Runs: %zu | Errors: %d | Last refresh: %.0fs | F5 to refresh",
sources_ok,
sources_found,
loader.GetGeneratorStats().size(),
loader.GetEmbeddingRegions().size(),
loader.GetTrainingRuns().size(),
status.error_count,
seconds_since);
} else {
ImGui::Text(
"Sources: %d/%d ok | Generators: %zu | Regions: %zu | Runs: %zu | Last refresh: %.0fs | F5 to refresh",
sources_ok,
sources_found,
loader.GetGeneratorStats().size(),
loader.GetEmbeddingRegions().size(),
loader.GetTrainingRuns().size(),
seconds_since);
}
} else {
if (status.error_count > 0) {
ImGui::TextDisabled("No data loaded - %d error(s) on refresh", status.error_count);
} else {
ImGui::TextDisabled("No data loaded - Press F5 to refresh");
}
}
ImGui::TableNextColumn();
ImGui::Text("Data: %s", data_path.c_str());
ImGui::EndTable();
}
}
} // namespace ui
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,18 @@
#pragma once
#include <imgui.h>
#include <string>
#include "../../models/state.h"
#include "../../data_loader.h"
namespace afs {
namespace viz {
namespace ui {
void RenderMetricCards(AppState& state, const DataLoader& loader, ImFont* font_ui, ImFont* font_header);
void RenderSummaryRow(AppState& state, const DataLoader& loader, ImFont* font_ui, ImFont* font_header);
void RenderStatusBar(AppState& state, const DataLoader& loader, const std::string& data_path);
} // namespace ui
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,418 @@
#include "model_registry.h"
#include "../../icons.h"
#include <imgui.h>
#include <algorithm>
#include <cstring>
namespace afs {
namespace studio {
namespace ui {
// Define static members
constexpr const char* ModelRegistryWidget::kRoleOptions[];
constexpr const char* ModelRegistryWidget::kLocationOptions[];
constexpr const char* ModelRegistryWidget::kBackendOptions[];
ModelRegistryWidget::ModelRegistryWidget() {
Refresh();
}
void ModelRegistryWidget::Refresh() {
std::string error;
if (!registry_.Load(&error)) {
last_error_ = error;
} else {
last_error_.clear();
}
selected_model_index_ = -1;
}
const ModelMetadata* ModelRegistryWidget::GetSelectedModel() const {
const auto& models = registry_.GetModels();
if (selected_model_index_ >= 0 &&
selected_model_index_ < static_cast<int>(models.size())) {
return &models[selected_model_index_];
}
return nullptr;
}
void ModelRegistryWidget::Render() {
RenderToolbar();
// Main content with optional details panel
if (show_details_ && GetSelectedModel()) {
// Split view: list on left, details on right
float details_width = 350.0f;
float list_width = ImGui::GetContentRegionAvail().x - details_width - 10.0f;
ImGui::BeginChild("ModelList", ImVec2(list_width, 0), true);
RenderModelList();
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("ModelDetails", ImVec2(details_width, 0), true);
RenderModelDetails();
ImGui::EndChild();
} else {
// Full width list
ImGui::BeginChild("ModelList", ImVec2(0, 0), true);
RenderModelList();
ImGui::EndChild();
}
}
void ModelRegistryWidget::RenderToolbar() {
// Refresh button
if (ImGui::Button("Refresh")) {
Refresh();
}
ImGui::SameLine();
// Filter text input
ImGui::SetNextItemWidth(200.0f);
ImGui::InputTextWithHint("##Filter", "Filter models...", filter_text_.data(),
filter_text_.size());
ImGui::SameLine();
// Role filter
ImGui::SetNextItemWidth(100.0f);
ImGui::Combo("Role", &filter_role_, kRoleOptions, kRoleCount);
ImGui::SameLine();
// Location filter
ImGui::SetNextItemWidth(100.0f);
ImGui::Combo("Location", &filter_location_, kLocationOptions, kLocationCount);
ImGui::SameLine();
// Backend filter
ImGui::SetNextItemWidth(100.0f);
ImGui::Combo("Backend", &filter_backend_, kBackendOptions, kBackendCount);
ImGui::SameLine();
// Toggle details panel
ImGui::Checkbox("Details", &show_details_);
ImGui::SameLine();
ImGui::Checkbox("Deploy", &show_deployment_);
// Status line
const auto& models = registry_.GetModels();
ImGui::TextDisabled("%zu models registered", models.size());
if (!registry_.GetLastLoadTime().empty()) {
ImGui::SameLine();
ImGui::TextDisabled("| Updated: %s",
registry_.GetLastLoadTime().c_str());
}
// Error display
if (!last_error_.empty()) {
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f), "Error: %s",
last_error_.c_str());
}
ImGui::Separator();
}
void ModelRegistryWidget::RenderModelList() {
const auto& models = registry_.GetModels();
std::string filter_str(filter_text_.data());
// Convert filter to lowercase for case-insensitive match
std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(),
::tolower);
int display_index = 0;
for (size_t i = 0; i < models.size(); ++i) {
const auto& model = models[i];
// Apply filters
if (filter_role_ > 0) {
if (model.role != kRoleOptions[filter_role_]) continue;
}
if (filter_location_ > 0) {
if (model.locations.count(kLocationOptions[filter_location_]) == 0)
continue;
}
if (filter_backend_ > 0) {
bool found = false;
for (const auto& backend : model.deployed_backends) {
if (backend == kBackendOptions[filter_backend_]) {
found = true;
break;
}
}
if (!found) continue;
}
// Apply text filter
if (!filter_str.empty()) {
std::string searchable = model.model_id + " " + model.display_name + " " +
model.role + " " + model.base_model;
std::transform(searchable.begin(), searchable.end(), searchable.begin(),
::tolower);
if (searchable.find(filter_str) == std::string::npos) continue;
}
RenderModelCard(model, static_cast<int>(i));
++display_index;
}
if (display_index == 0) {
ImGui::TextDisabled("No models match the current filters.");
}
}
void ModelRegistryWidget::RenderModelCard(const ModelMetadata& model,
int index) {
ImGui::PushID(index);
bool is_selected = (selected_model_index_ == index);
// Card styling
ImVec4 header_color =
is_selected ? ImVec4(0.2f, 0.4f, 0.8f, 1.0f) : ImVec4(0.15f, 0.15f, 0.15f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Header, header_color);
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
ImVec4(0.25f, 0.45f, 0.75f, 1.0f));
// Collapsing header acts as the card
bool expanded = ImGui::CollapsingHeader(
model.display_name.empty() ? model.model_id.c_str()
: model.display_name.c_str(),
ImGuiTreeNodeFlags_DefaultOpen);
if (ImGui::IsItemClicked()) {
selected_model_index_ = index;
}
ImGui::PopStyleColor(2);
if (expanded) {
ImGui::Indent();
// Role badge
ImVec4 role_color(0.3f, 0.6f, 0.3f, 1.0f);
if (model.role == "asm")
role_color = ImVec4(0.8f, 0.5f, 0.2f, 1.0f);
else if (model.role == "debug")
role_color = ImVec4(0.6f, 0.3f, 0.6f, 1.0f);
else if (model.role == "yaze")
role_color = ImVec4(0.2f, 0.6f, 0.8f, 1.0f);
ImGui::TextColored(role_color, "[%s]", model.role.c_str());
ImGui::SameLine();
ImGui::TextDisabled("%s", model.base_model.c_str());
// Metrics & Badges
ImGui::BeginGroup();
if (model.final_loss.has_value()) {
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.6f, 1.0f), ICON_MD_TRENDING_DOWN " Loss: %.4f", model.final_loss.value());
ImGui::SameLine();
}
if (model.train_samples > 0) {
ImGui::TextDisabled(ICON_MD_FOLDER " %d samples", model.train_samples);
}
ImGui::EndGroup();
// Locations & Backends
if (!model.deployed_backends.empty()) {
ImGui::TextDisabled(ICON_MD_CLOUD_DONE " Deployed:");
for (const auto& backend : model.deployed_backends) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 0.8f, 0.4f, 1.0f), "[%s]", backend.c_str());
}
} else {
ImGui::TextDisabled(ICON_MD_CLOUD_OFF " Not Deployed");
}
// Action Buttons - Premium styling
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.15f, 1.0f));
if (ImGui::Button(ICON_MD_ROCKET_LAUNCH " Deploy")) {
show_deployment_ = true;
}
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.25f, 0.45f, 1.0f));
if (ImGui::Button(ICON_MD_PLAY_ARROW " Test")) {
show_details_ = true;
// Scroll or activate test section in details if needed
}
ImGui::PopStyleColor();
ImGui::SameLine();
if (ImGui::Button(ICON_MD_COMPARE " Compare")) {
// TODO: Add to comparison view
}
ImGui::Unindent();
ImGui::Spacing();
ImGui::Separator();
}
ImGui::PopID();
}
void ModelRegistryWidget::RenderModelDetails() {
const ModelMetadata* model = GetSelectedModel();
if (!model) {
ImGui::TextDisabled("Select a model to view details.");
return;
}
ImGui::Text("%s", model->display_name.c_str());
ImGui::Separator();
// Identity section
ImGui::TextDisabled("ID:");
ImGui::SameLine();
ImGui::TextWrapped("%s", model->model_id.c_str());
ImGui::TextDisabled("Version:");
ImGui::SameLine();
ImGui::Text("%s", model->version.c_str());
ImGui::TextDisabled("Base Model:");
ImGui::SameLine();
ImGui::TextWrapped("%s", model->base_model.c_str());
ImGui::Spacing();
ImGui::Separator();
// Training section
ImGui::Text("Training");
ImGui::TextDisabled("Role:");
ImGui::SameLine();
ImGui::Text("%s", model->role.c_str());
ImGui::TextDisabled("Date:");
ImGui::SameLine();
ImGui::Text("%s", model->training_date.c_str());
ImGui::TextDisabled("Duration:");
ImGui::SameLine();
ImGui::Text("%d minutes", model->training_duration_minutes);
ImGui::TextDisabled("Hardware:");
ImGui::SameLine();
ImGui::Text("%s (%s)", model->hardware.c_str(), model->device.c_str());
ImGui::Spacing();
ImGui::Separator();
// Dataset section
ImGui::Text("Dataset");
ImGui::TextDisabled("Name:");
ImGui::SameLine();
ImGui::Text("%s", model->dataset_name.c_str());
ImGui::TextDisabled("Samples:");
ImGui::SameLine();
ImGui::Text("%d train / %d val / %d test", model->train_samples,
model->val_samples, model->test_samples);
ImGui::TextDisabled("Acceptance:");
ImGui::SameLine();
ImGui::Text("%.1f%%", model->dataset_quality.acceptance_rate * 100.0f);
ImGui::Spacing();
ImGui::Separator();
// Metrics section
ImGui::Text("Metrics");
if (model->final_loss.has_value()) {
ImGui::TextDisabled("Final Loss:");
ImGui::SameLine();
ImGui::Text("%.4f", model->final_loss.value());
}
if (model->best_loss.has_value()) {
ImGui::TextDisabled("Best Loss:");
ImGui::SameLine();
ImGui::Text("%.4f", model->best_loss.value());
}
if (model->perplexity.has_value()) {
ImGui::TextDisabled("Perplexity:");
ImGui::SameLine();
ImGui::Text("%.2f", model->perplexity.value());
}
ImGui::Spacing();
ImGui::Separator();
// Locations section
ImGui::Text("Locations");
for (const auto& [location, path] : model->locations) {
bool is_primary = (location == model->primary_location);
ImGui::BulletText("%s%s", location.c_str(), is_primary ? " (primary)" : "");
ImGui::TextDisabled(" %s", path.c_str());
}
ImGui::Spacing();
ImGui::Separator();
// Deployment section
ImGui::Text("Deployment");
if (model->deployed_backends.empty()) {
ImGui::TextDisabled("Not deployed");
} else {
for (const auto& backend : model->deployed_backends) {
ImGui::BulletText("%s", backend.c_str());
if (backend == "ollama" && model->ollama_model_name.has_value()) {
ImGui::TextDisabled(" Name: %s",
model->ollama_model_name.value().c_str());
}
}
}
// Notes
if (!model->notes.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Text("Notes");
ImGui::TextWrapped("%s", model->notes.c_str());
}
// Tags
if (!model->tags.empty()) {
ImGui::Spacing();
ImGui::TextDisabled("Tags:");
for (const auto& tag : model->tags) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.5f, 0.7f, 0.9f, 1.0f), "#%s", tag.c_str());
}
}
// Deployment panel section
if (show_deployment_) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Text("Deployment Actions");
ImGui::Spacing();
deployment_panel_.Render(model);
}
}
void RenderModelRegistryWindow(ModelRegistryWidget& widget, bool* open) {
if (!open || !*open) return;
ImGui::SetNextWindowSize(ImVec2(900, 600), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Model Registry", open)) {
ImGui::End();
return;
}
widget.Render();
ImGui::End();
}
} // namespace ui
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,69 @@
#pragma once
#include "core/registry_reader.h"
#include "deployment_panel.h"
#include <array>
#include <string>
namespace afs {
namespace studio {
namespace ui {
// Model Registry widget for listing and managing trained models
class ModelRegistryWidget {
public:
ModelRegistryWidget();
// Render the widget (call this from main render loop)
void Render();
// Reload models from registry
void Refresh();
// Get selected model (if any)
const ModelMetadata* GetSelectedModel() const;
private:
void RenderToolbar();
void RenderModelList();
void RenderModelDetails();
void RenderDeploymentPanel();
void RenderModelCard(const ModelMetadata& model, int index);
RegistryReader registry_;
DeploymentPanel deployment_panel_;
int selected_model_index_ = -1;
// Filter state
std::array<char, 64> filter_text_{};
int filter_role_ = 0; // 0 = All
int filter_location_ = 0; // 0 = All
int filter_backend_ = 0; // 0 = All
// Filter options
static constexpr const char* kRoleOptions[] = {
"All", "asm", "debug", "general", "yaze"};
static constexpr int kRoleCount = 5;
static constexpr const char* kLocationOptions[] = {"All", "mac", "windows",
"cloud", "halext"};
static constexpr int kLocationCount = 5;
static constexpr const char* kBackendOptions[] = {
"All", "ollama", "llama.cpp", "vllm", "transformers", "halext-node"};
static constexpr int kBackendCount = 6;
// UI state
bool show_details_ = true;
bool show_deployment_ = true;
std::string last_error_;
};
// Render a standalone model registry window
void RenderModelRegistryWindow(ModelRegistryWidget& widget, bool* open);
} // namespace ui
} // namespace studio
} // namespace afs

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
#pragma once
#include <imgui.h>
#include <string>
#include <vector>
#include <functional>
#include "../../models/state.h"
#include "../../data_loader.h"
#include "../shortcuts.h"
namespace afs {
namespace viz {
namespace ui {
void RenderInspectorPanel(AppState& state, const DataLoader& loader, ImFont* font_header, const std::string& data_path);
void RenderDatasetPanel(AppState& state, const DataLoader& loader);
void RenderSystemsPanel(AppState& state, ImFont* font_header, std::function<void(const char*)> refresh_callback);
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);
void RenderSidebar(AppState& state, const DataLoader& loader, ImFont* font_ui, ImFont* font_header);
} // namespace ui
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,741 @@
#include "tabs.h"
#include "../core.h"
#include "../../icons.h"
#include <implot.h>
#include <algorithm>
#include <fstream>
#include <filesystem>
#include "../../core/logger.h"
namespace afs {
namespace viz {
namespace ui {
void RenderKnobsTab(AppState& state, const DataLoader& loader, const std::string& data_path, std::function<void(const char*)> refresh_callback, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback) {
ImGui::Text("Runtime Controls");
if (ImGui::Button("Refresh Now")) {
if (refresh_callback) refresh_callback("ui");
}
ImGui::SameLine();
ImGui::Checkbox("Auto Refresh", &state.auto_refresh);
ImGui::SliderFloat("Refresh Interval (s)", &state.refresh_interval_sec, 2.0f, 30.0f);
ImGui::Separator();
ImGui::Text("Simulation");
ImGui::Checkbox("Simulate Activity", &state.simulate_activity);
ImGui::SliderFloat("Agent Activity", &state.agent_activity_scale, 0.3f, 3.0f);
ImGui::SliderFloat("Mission Bias", &state.mission_priority_bias, 0.5f, 2.0f);
ImGui::Separator();
ImGui::Text("Visualization");
ImGui::SliderFloat("Chart Height (compact)", &state.chart_height, 130.0f, 240.0f);
ImGui::Checkbox("Compact Charts", &state.compact_charts);
ImGui::Checkbox("Show Status Strip", &state.show_status_strip);
ImGui::Checkbox("Show Controls", &state.show_controls);
ImGui::Checkbox("Show Systems Panel", &state.show_systems_panel);
ImGui::Checkbox("Allow Workspace Scroll", &state.allow_workspace_scroll);
ImGui::Checkbox("Plot Interaction", &state.enable_plot_interaction);
if (state.enable_plot_interaction) {
ImGui::Checkbox("Plot Zoom Requires Shift", &state.plot_interaction_requires_modifier);
if (state.plot_interaction_requires_modifier) {
ImGui::TextDisabled("Hold Shift + scroll/drag to pan/zoom plots.");
}
}
ImGui::Checkbox("Reset Layout on Workspace Change", &state.reset_layout_on_workspace_change);
ImGui::Checkbox("Auto Columns", &state.auto_chart_columns);
if (!state.auto_chart_columns) {
ImGui::SliderInt("Chart Columns", &state.chart_columns, 2, 4);
}
ImGui::SliderFloat("Embedding Sample Rate", &state.embedding_sample_rate, 0.1f, 1.0f);
ImGui::SliderFloat("Quality Threshold", &state.quality_threshold, 0.4f, 0.95f);
ImGui::SliderInt("Mission Concurrency", &state.mission_concurrency, 1, 12);
ImGui::Checkbox("Verbose Logs", &state.verbose_logs);
ImGui::Checkbox("Pulse Animations", &state.use_pulse_animations);
ImGui::Checkbox("Plot Legends", &state.show_plot_legends);
ImGui::Checkbox("Plot Markers", &state.show_plot_markers);
ImGui::Checkbox("Data Scientist Mode", &state.data_scientist_mode);
ImGui::Checkbox("Show All Chart Windows", &state.show_all_charts);
ImGui::Separator();
ImGui::Text("Advanced");
if (ImGui::Button("Purge Mission Queue")) {
state.missions.clear();
if (log_callback) log_callback("system", "Mission queue purged.", "system");
}
ImGui::SameLine();
if (ImGui::Button("Force Reconnect")) {
if (log_callback) log_callback("system", "Forcing backend reconnect...", "system");
}
ImGui::Separator();
ImGui::Text(ICON_MD_MODEL_TRAINING " Agent Training");
ImGui::SliderFloat("Learning Rate", &state.trainer_lr, 0.00001f, 0.001f, "%.5f");
ImGui::SliderInt("Batch Size", &state.trainer_batch_size, 8, 128);
ImGui::SliderInt("Epochs", &state.trainer_epochs, 1, 100);
ImGui::SliderFloat("Gen Temperature", &state.generator_temp, 0.1f, 2.0f);
ImGui::SliderFloat("Rejection Floor", &state.rejection_threshold, 0.2f, 0.95f);
ImGui::Separator();
ImGui::TextDisabled("Data Path");
ImGui::TextWrapped("%s", data_path.c_str());
}
void RenderAgentsTab(AppState& state, ImFont* font_ui, ImFont* font_header, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback) {
ImGui::Text("Background Agents");
ImGui::InputTextWithHint("##AgentName", "Agent name", state.new_agent_name.data(), state.new_agent_name.size());
ImGui::InputTextWithHint("##AgentRole", "Role", state.new_agent_role.data(), state.new_agent_role.size());
if (ImGui::Button("Spawn Agent")) {
std::string name(state.new_agent_name.data());
std::string role(state.new_agent_role.data());
if (name.empty()) name = "Agent " + std::to_string(state.agents.size() + 1);
if (role.empty()) role = "Generalist";
AgentState agent;
agent.name = name;
agent.role = role;
agent.status = "Active";
agent.enabled = true;
agent.activity_phase = 0.3f * static_cast<float>(state.agents.size() + 1);
state.agents.push_back(std::move(agent));
if (log_callback) log_callback(name, "Agent provisioned.", "agent");
}
ImGui::SameLine();
ImGui::SetNextItemWidth(60.0f);
ImGui::InputInt("##SpawnAgentCount", &state.spawn_agent_count);
state.spawn_agent_count = std::max(1, std::min(state.spawn_agent_count, 12));
ImGui::SameLine();
if (ImGui::Button("Spawn Batch")) {
std::string base_nameExtra(state.new_agent_name.data());
if (base_nameExtra.empty()) base_nameExtra = "Agent";
std::string role(state.new_agent_role.data());
if (role.empty()) role = "Generalist";
for (int i = 0; i < state.spawn_agent_count; ++i) {
AgentState agent;
agent.name = base_nameExtra + " " + std::to_string(state.agents.size() + 1);
agent.role = role;
agent.status = "Active";
agent.enabled = true;
agent.activity_phase = 0.3f * static_cast<float>(state.agents.size() + 1);
state.agents.push_back(std::move(agent));
}
if (log_callback) log_callback("system", "Batch spawn complete.", "system");
}
if (ImGui::Button("Pause All")) {
for (auto& agent : state.agents) {
agent.enabled = false;
agent.status = "Paused";
}
if (log_callback) log_callback("system", "All agents paused.", "system");
}
ImGui::SameLine();
if (ImGui::Button("Resume All")) {
for (auto& agent : state.agents) {
agent.enabled = true;
if (agent.status == "Paused") agent.status = "Idle";
}
if (log_callback) log_callback("system", "All agents resumed.", "system");
}
float table_height = ImGui::GetContentRegionAvail().y;
if (ImGui::BeginTable("AgentsTable", 8, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInner | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, table_height))) {
ImGui::TableSetupColumn("Agent");
ImGui::TableSetupColumn("Role");
ImGui::TableSetupColumn("Status");
ImGui::TableSetupColumn("Queue");
ImGui::TableSetupColumn("Success");
ImGui::TableSetupColumn("Latency");
ImGui::TableSetupColumn("CPU/Mem");
ImGui::TableSetupColumn("On");
ImGui::TableHeadersRow();
for (size_t i = 0; i < state.agents.size(); ++i) {
auto& agent = state.agents[i];
ImGui::PushID(static_cast<int>(i));
const char* status = agent.enabled ? (agent.queue_depth > 0 ? "Busy" : "Idle") : "Paused";
ImGui::TableNextRow();
bool is_selected = (state.selected_agent_index == static_cast<int>(i));
float pulse = 0.0f;
if (state.use_pulse_animations && agent.enabled && agent.status != "Idle") {
pulse = 0.5f + 0.5f * std::sin(state.pulse_timer * 6.0f);
}
if (pulse > 0.01f) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(GetStepColor(pulse * 0.2f, state)));
}
if (ImGui::Selectable(agent.name.c_str(), is_selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) {
state.selected_agent_index = static_cast<int>(i);
}
ImGui::TableSetColumnIndex(1); ImGui::Text("%s", agent.role.c_str());
ImGui::TableSetColumnIndex(2); ImGui::Text("%s", status);
ImGui::TableSetColumnIndex(3); ImGui::Text("%d", agent.queue_depth);
ImGui::TableSetColumnIndex(4); ImGui::Text("%.0f%%", agent.success_rate * 100.0f);
ImGui::TableSetColumnIndex(5); ImGui::Text("%.1f ms", agent.avg_latency_ms);
ImGui::TableSetColumnIndex(6); ImGui::Text("%.0f/%.0f", agent.cpu_pct, agent.mem_pct);
ImGui::TableSetColumnIndex(7); ImGui::Checkbox("##enabled", &agent.enabled);
ImGui::PopID();
}
ImGui::EndTable();
}
if (state.selected_agent_index >= 0 && state.selected_agent_index < static_cast<int>(state.agents.size())) {
auto& agent = state.agents[state.selected_agent_index];
ImGui::Separator();
ImGui::Text("Agent Details: %s", agent.name.c_str());
ImGui::Columns(2, "AgentDetailCols", false);
ImGui::Text("Role: %s", agent.role.c_str());
ImGui::Text("Status: %s", agent.status.c_str());
ImGui::Text("Tasks: %d", agent.tasks_completed);
ImGui::NextColumn();
if (state.sparkline_data.size() < 40) {
for(int i=0; i<40; ++i) state.sparkline_data.push_back(0.4f + 0.5f * (float)rand()/RAND_MAX);
}
ImPlot::PushStyleColor(ImPlotCol_Line, ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
if (ImPlot::BeginPlot("##Sparkline", ImVec2(-1, 60), ImPlotFlags_CanvasOnly)) {
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
ImPlot::PlotLine("Activity", state.sparkline_data.data(), 40);
ImPlot::EndPlot();
}
ImPlot::PopStyleColor();
ImGui::Columns(1);
if (ImGui::Button("Reset Agent Stats")) {
agent.tasks_completed = 0;
if (log_callback) log_callback(agent.name, "Stats reset.", "agent");
}
ImGui::SameLine();
if (ImGui::Button("Force Restart")) {
if (log_callback) log_callback(agent.name, "Restarting...", "system");
}
}
}
void RenderMissionsTab(AppState& state, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback) {
ImGui::Text("Mission Queue");
ImGui::InputTextWithHint("##MissionName", "Mission name", state.new_mission_name.data(), state.new_mission_name.size());
ImGui::InputTextWithHint("##MissionOwner", "Owner", state.new_mission_owner.data(), state.new_mission_owner.size());
ImGui::SliderInt("Priority", &state.new_mission_priority, 1, 5);
if (ImGui::Button("Create Mission")) {
std::string name(state.new_mission_name.data());
std::string owner(state.new_mission_owner.data());
if (name.empty()) name = "Mission " + std::to_string(state.missions.size() + 1);
if (owner.empty()) owner = "Ops";
MissionState mission;
mission.name = name;
mission.owner = owner;
mission.status = "Queued";
mission.priority = state.new_mission_priority;
state.missions.push_back(std::move(mission));
if (log_callback) log_callback("system", "Mission queued: " + name, "system");
}
ImGui::SameLine();
ImGui::SetNextItemWidth(60.0f);
ImGui::InputInt("##SpawnMissionCount", &state.spawn_mission_count);
state.spawn_mission_count = std::max(1, std::min(state.spawn_mission_count, 10));
ImGui::SameLine();
if (ImGui::Button("Spawn Batch")) {
std::string base_nameExtra(state.new_mission_name.data());
if (base_nameExtra.empty()) base_nameExtra = "Mission";
std::string owner(state.new_mission_owner.data());
if (owner.empty()) owner = "Ops";
for (int i = 0; i < state.spawn_mission_count; ++i) {
MissionState mission;
mission.name = base_nameExtra + " " + std::to_string(state.missions.size() + 1);
mission.owner = owner;
mission.status = "Queued";
mission.priority = state.new_mission_priority;
state.missions.push_back(std::move(mission));
}
if (log_callback) log_callback("system", "Batch missions queued.", "system");
}
if (ImGui::Button("Clear Completed")) {
state.missions.erase(std::remove_if(state.missions.begin(), state.missions.end(),
[](const MissionState& mission) {
return mission.status == "Complete" && !mission.data_backed;
}), state.missions.end());
}
float table_height = ImGui::GetContentRegionAvail().y;
if (ImGui::BeginTable("MissionsTable", 6, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInner | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, table_height))) {
ImGui::TableSetupColumn("Mission");
ImGui::TableSetupColumn("Owner");
ImGui::TableSetupColumn("Status");
ImGui::TableSetupColumn("Priority");
ImGui::TableSetupColumn("Progress");
ImGui::TableSetupColumn("Data");
ImGui::TableHeadersRow();
for (size_t i = 0; i < state.missions.size(); ++i) {
auto& mission = state.missions[i];
ImGui::PushID(static_cast<int>(i));
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::Text("%s", mission.name.c_str());
ImGui::TableSetColumnIndex(1); ImGui::Text("%s", mission.owner.c_str());
ImGui::TableSetColumnIndex(2); ImGui::Text("%s", mission.status.c_str());
ImGui::TableSetColumnIndex(3); ImGui::Text("%d", mission.priority);
ImGui::TableSetColumnIndex(4); ImGui::ProgressBar(mission.progress, ImVec2(-FLT_MIN, 0.0f));
ImGui::TableSetColumnIndex(5); ImGui::Text("%s", mission.data_backed ? "data" : "live");
ImGui::PopID();
}
ImGui::EndTable();
}
}
void RenderLogsTab(AppState& state, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback) {
ImGui::Text(ICON_MD_CHAT " Agent Chat & Orchestration");
// Provider Selection
const char* providers[] = { "Local (Ollama)", "Remote (OpenWebUI)", "Direct (afs SVC)", "Mock" };
static int current_provider = 0;
ImGui::SetNextItemWidth(200);
ImGui::Combo("Provider", &current_provider, providers, IM_ARRAYSIZE(providers));
ImGui::SameLine();
std::vector<const char*> agent_labels;
agent_labels.push_back("All Agents");
for (const auto& agent : state.agents) {
agent_labels.push_back(agent.name.c_str());
}
if (state.log_agent_index < 0 || state.log_agent_index >= static_cast<int>(agent_labels.size())) {
state.log_agent_index = 0;
}
ImGui::SetNextItemWidth(150);
ImGui::Combo("Target", &state.log_agent_index, agent_labels.data(), static_cast<int>(agent_labels.size()));
ImGui::Separator();
float footer_height = 80.0f;
float log_height = ImGui::GetContentRegionAvail().y - footer_height;
if (log_height < 120.0f) log_height = 120.0f;
ImGui::BeginChild("LogList", ImVec2(0, log_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
std::string filter(state.log_filter.data());
for (const auto& entry : state.logs) {
if (state.log_agent_index > 0 && entry.agent != agent_labels[state.log_agent_index]) continue;
if (!filter.empty()) {
if (entry.message.find(filter) == std::string::npos && entry.agent.find(filter) == std::string::npos) continue;
}
bool is_user = (entry.kind == "user");
bool is_system = (entry.kind == "system");
ImVec2 size = ImGui::GetContentRegionAvail();
float bubble_width = std::min(size.x * 0.75f, 500.0f);
if (is_user) ImGui::SetCursorPosX(size.x - bubble_width - 8.0f);
else ImGui::SetCursorPosX(8.0f);
ImGui::BeginGroup();
// Bubble header
ImGui::TextDisabled("%s", entry.agent.c_str());
// Bubble body
ImVec4 bg_color = is_user ? ImVec4(0.2f, 0.4f, 0.8f, 0.4f) : (is_system ? ImVec4(0.3f, 0.3f, 0.3f, 0.4f) : ImVec4(0.2f, 0.6f, 0.4f, 0.4f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, bg_color);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 8.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10, 8));
std::string child_id = "msg_" + std::to_string(&entry - &state.logs[0]);
float text_h = ImGui::CalcTextSize(entry.message.c_str(), nullptr, false, bubble_width - 20).y + 20;
ImGui::BeginChild(child_id.c_str(), ImVec2(bubble_width, text_h), true, ImGuiWindowFlags_NoScrollbar);
ImGui::TextWrapped("%s", entry.message.c_str());
ImGui::EndChild();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
ImGui::EndGroup();
ImGui::Spacing();
}
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
ImGui::SetScrollHereY(1.0f);
ImGui::EndChild();
ImGui::Separator();
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 100);
if (ImGui::InputTextWithHint("##ChatInput", "Message swarm...", state.chat_input.data(), state.chat_input.size(), ImGuiInputTextFlags_EnterReturnsTrue)) {
goto send_msg;
}
ImGui::SameLine();
if (ImGui::Button("Send", ImVec2(90, 0))) {
send_msg:
std::string message(state.chat_input.data());
if (!message.empty()) {
std::string target = agent_labels[state.log_agent_index];
if (log_callback) log_callback("user", message, "user");
// Simulate response
if (state.log_agent_index > 0) {
if (log_callback) log_callback(target, "Processing request via " + std::string(providers[current_provider]) + ": " + message, "agent");
} else {
if (log_callback) log_callback("Orchestrator", "Coordinating agents via " + std::string(providers[current_provider]) + "...", "system");
}
state.chat_input[0] = '\0';
}
}
}
void RenderContextTab(AppState& state, TextEditor& text_editor, MemoryEditorWidget& memory_editor, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback) {
ImGui::Text("AFS Context Browser");
ImGui::Separator();
if (ImGui::Button("..") || ImGui::Button("Up")) {
if (state.current_browser_path.has_parent_path()) {
state.current_browser_path = state.current_browser_path.parent_path();
state.browser_entries.clear();
}
}
ImGui::SameLine();
ImGui::TextDisabled("Path: %s", state.current_browser_path.string().c_str());
if (state.browser_entries.empty()) RefreshBrowserEntries(state);
float table_height = ImGui::GetContentRegionAvail().y * 0.6f;
if (ImGui::BeginTable("FileBrowser", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImVec2(0, table_height))) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, 60.0f);
ImGui::TableHeadersRow();
for (const auto& entry : state.browser_entries) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
if (entry.is_directory) {
if (ImGui::Selectable((entry.name + "/").c_str(), false)) {
state.current_browser_path = entry.path;
state.browser_entries.clear();
break;
}
} else {
if (ImGui::Selectable(entry.name.c_str(), state.selected_file_path == entry.path)) {
LoadFile(state, entry.path, text_editor);
}
}
ImGui::TableNextColumn();
if (!entry.is_directory) ImGui::TextDisabled("%.1f KB", entry.size / 1024.0f);
ImGui::TableNextColumn();
if (!entry.is_directory) {
if (ImGui::Button(("Add##" + entry.name).c_str())) {
ContextItem item;
item.name = entry.name;
item.path = entry.path;
item.type = entry.path.extension().string();
state.selected_context.push_back(item);
if (log_callback) log_callback("system", "Added to context: " + entry.name, "system");
}
}
}
ImGui::EndTable();
}
ImGui::Separator();
ImGui::Text("File View (%s)", state.selected_file_path.filename().string().c_str());
if (state.is_binary_view) {
memory_editor.DrawContents(state.binary_data.data(), state.binary_data.size());
} else {
text_editor.Render("TextEditor", ImVec2(0, 0), true);
}
ImGui::Separator();
ImGui::Text("Selected Context (%d items)", (int)state.selected_context.size());
ImGui::BeginChild("ContextList", ImVec2(0, 0), true);
for (size_t i = 0; i < state.selected_context.size(); ++i) {
auto& item = state.selected_context[i];
ImGui::PushID((int)i);
ImGui::Checkbox("##on", &item.enabled);
ImGui::SameLine();
ImGui::Text("%s", item.name.c_str());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", item.path.string().c_str());
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 40);
if (ImGui::Button("X")) {
state.selected_context.erase(state.selected_context.begin() + i);
ImGui::PopID();
break;
}
ImGui::PopID();
}
ImGui::EndChild();
}
void RenderAgentPromptTab(AppState& state, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback) {
ImGui::Text("Agent Orchestrator Prompt");
ImGui::Separator();
ImGui::Text("System Prompt");
ImGui::InputTextMultiline("##SysPrompt", state.system_prompt.data(), state.system_prompt.size(), ImVec2(-1, 80));
ImGui::Spacing();
ImGui::Text("User Message");
ImGui::InputTextMultiline("##UserPrompt", state.user_prompt.data(), state.user_prompt.size(), ImVec2(-1, 120));
static int target_agent = 0;
std::vector<const char*> agent_names = {"Orchestrator", "Coordinator"};
for (const auto& a : state.agents) agent_names.push_back(a.name.c_str());
ImGui::Combo("Target", &target_agent, agent_names.data(), (int)agent_names.size());
if (ImGui::Button("Trigger Background Agent", ImVec2(-1, 40))) {
std::string msg = "Sent prompt to " + std::string(agent_names[target_agent]);
if (log_callback) log_callback("user", msg, "user");
if (log_callback) log_callback(agent_names[target_agent], "Analyzing context with " + std::to_string(state.selected_context.size()) + " files...", "agent");
state.user_prompt[0] = '\0';
}
ImGui::Separator();
ImGui::TextDisabled("Quick Context Meta:");
for (const auto& item : state.selected_context) {
if (item.enabled) {
ImGui::TextDisabled(" - %s (%s)", item.name.c_str(), item.type.c_str());
}
}
}
void RenderTablesTab(AppState& state, const DataLoader& loader) {
if (ImGui::BeginTabBar("TableGroups")) {
if (ImGui::BeginTabItem("Generator Detailed")) {
const auto& stats = loader.GetGeneratorStats();
if (ImGui::BeginTable("GenDetailed", 6, ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) {
ImGui::TableSetupColumn("Generator");
ImGui::TableSetupColumn("Accepted");
ImGui::TableSetupColumn("Rejected");
ImGui::TableSetupColumn("Rate %");
ImGui::TableSetupColumn("Avg Q");
ImGui::TableSetupColumn("Status");
ImGui::TableHeadersRow();
for (const auto& s : stats) {
ImGui::TableNextRow();
ImGui::TableNextColumn(); ImGui::Text("%s", s.name.c_str());
ImGui::TableNextColumn(); ImGui::Text("%d", s.samples_accepted);
ImGui::TableNextColumn(); ImGui::Text("%d", s.samples_rejected);
ImGui::TableNextColumn();
float rate = s.acceptance_rate * 100.0f;
ImGui::Text("%.1f%%", rate);
if (rate < 40.0f) { ImGui::SameLine(); ImGui::TextColored(ImVec4(1,0,0,1), "[!] "); }
ImGui::TableNextColumn(); ImGui::Text("%.3f", s.avg_quality);
ImGui::TableNextColumn();
if (s.samples_rejected > s.samples_accepted) ImGui::TextColored(ImVec4(1,0.5,0,1), "Struggling");
else ImGui::TextColored(ImVec4(0,1,0.5,1), "Healthy");
}
ImGui::EndTable();
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Quality Metrics")) {
const auto& trends = loader.GetQualityTrends();
if (ImGui::BeginTable("QualityDetailed", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders)) {
ImGui::TableSetupColumn("Domain");
ImGui::TableSetupColumn("Metric");
ImGui::TableSetupColumn("Mean Score");
ImGui::TableSetupColumn("Trend");
ImGui::TableSetupColumn("Sparkline");
ImGui::TableHeadersRow();
for (size_t i = 0; i < trends.size(); ++i) {
const auto& t = trends[i];
ImGui::TableNextRow();
ImGui::TableNextColumn(); ImGui::Text("%s", t.domain.c_str());
ImGui::TableNextColumn(); ImGui::Text("%s", t.metric.c_str());
ImGui::TableNextColumn(); ImGui::Text("%.4f", t.mean);
ImGui::TableNextColumn();
if (t.trend_direction == "improving") ImGui::TextColored(ImVec4(0,1,0,1), "INC");
else if (t.trend_direction == "declining") ImGui::TextColored(ImVec4(1,0,0,1), "DEC");
else ImGui::Text("---");
ImGui::TableNextColumn();
ImGui::PushID((int)i);
if (ImPlot::BeginPlot("##Spark", ImVec2(-1, 24), ImPlotFlags_CanvasOnly | ImPlotFlags_NoInputs)) {
ImPlot::SetupAxes(nullptr,nullptr,ImPlotAxisFlags_NoDecorations,ImPlotAxisFlags_NoDecorations);
ImPlot::PlotLine("##v", t.values.data(), (int)t.values.size());
ImPlot::EndPlot();
}
ImGui::PopID();
}
ImGui::EndTable();
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Training Runs")) {
const auto& runs = loader.GetTrainingRuns();
if (ImGui::BeginTable("TrainingDetailed", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders)) {
ImGui::TableSetupColumn("Run ID");
ImGui::TableSetupColumn("Model");
ImGui::TableSetupColumn("Samples");
ImGui::TableSetupColumn("Loss");
ImGui::TableSetupColumn("Eval Metrics");
ImGui::TableHeadersRow();
for (const auto& r : runs) {
ImGui::TableNextRow();
ImGui::TableNextColumn(); ImGui::Text("%s", r.run_id.substr(0, 16).c_str());
ImGui::TableNextColumn(); ImGui::Text("%s", r.model_name.c_str());
ImGui::TableNextColumn(); ImGui::Text("%d", r.samples_count);
ImGui::TableNextColumn(); ImGui::Text("%.5f", r.final_loss);
ImGui::TableNextColumn();
for (const auto& [name, val] : r.eval_metrics) {
ImGui::TextDisabled("%s: %.3f", name.c_str(), val);
}
}
ImGui::EndTable();
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
void RenderServicesTab(AppState& state, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback) {
ImGui::Text("Core Services");
auto render_service = [&log_callback](const char* name, const char* status, float health, const char* desc) {
ImGui::PushID(name);
ImGui::BeginGroup();
ImGui::Text("%s", name);
ImGui::TextDisabled("%s", desc);
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::TextColored(status_color, "Status: %s", status);
ImGui::ProgressBar(health, ImVec2(-1.0f, 0.0f));
ImGui::EndGroup();
ImGui::PopID();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
};
render_service("Orchestrator", "Active", 0.98f, "Main mission control & dispatch");
render_service("Knowledge Base", "Active", 0.92f, "Vector DB & Concept Index");
render_service("Trainer", "Active", 0.85f, "Active learning loop coordination");
render_service("Embedding SVC", "Warning", 0.65f, "Latency spike detected in region 42");
if (ImGui::Button("Reset Services")) {
if (log_callback) log_callback("system", "Services reset signal sent.", "system");
}
}
void RefreshBrowserEntries(AppState& state) {
state.browser_entries.clear();
if (state.current_browser_path.has_parent_path()) {
state.browser_entries.push_back({"..", state.current_browser_path.parent_path(), true, 0, false});
}
std::error_code ec;
auto it = std::filesystem::directory_iterator(state.current_browser_path, ec);
if (ec) {
LOG_ERROR("Failed to open directory: " + state.current_browser_path.string() + " (" + ec.message() + ")");
return;
}
try {
for (const auto& entry : it) {
std::error_code entry_ec;
if (entry.is_directory(entry_ec)) {
std::error_code context_ec;
bool has_context = false;
try {
has_context = std::filesystem::exists(entry.path() / ".context", context_ec);
} catch (...) {}
state.browser_entries.push_back({entry.path().filename().string(), entry.path(), true, 0, has_context});
} else if (entry.is_regular_file(entry_ec)) {
state.browser_entries.push_back({entry.path().filename().string(), entry.path(), false, entry.file_size(entry_ec), false});
}
}
} catch (...) {
LOG_WARN("Error during directory iteration in " + state.current_browser_path.string());
}
std::sort(state.browser_entries.begin(), state.browser_entries.end(), [](const FileEntry& a, const FileEntry& b) {
if (a.is_directory != b.is_directory) return a.is_directory > b.is_directory;
return a.name < b.name;
});
}
void LoadFile(AppState& state, const std::filesystem::path& path, TextEditor& text_editor) {
state.selected_file_path = path;
std::string ext = path.extension().string();
static const std::vector<std::string> text_exts = {
".cpp", ".cc", ".c", ".h", ".hpp", ".py", ".md", ".json", ".txt", ".xml", ".org", ".asm", ".s", ".cmake", ".yml", ".yaml", ".sh"
};
bool is_text = false;
for (const auto& e : text_exts) if (ext == e) { is_text = true; break; }
if (is_text) {
state.is_binary_view = false;
std::ifstream t(path);
if (t.is_open()) {
std::string str((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
text_editor.SetText(str);
if (ext == ".cpp" || ext == ".cc" || ext == ".h" || ext == ".hpp") text_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
else if (ext == ".sql") text_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::SQL());
else text_editor.SetLanguageDefinition(TextEditor::LanguageDefinition());
}
} else {
state.is_binary_view = true;
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (file.is_open()) {
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
if (size <= 10 * 1024 * 1024) {
state.binary_data.resize(size);
file.read((char*)state.binary_data.data(), size);
} else state.binary_data.clear();
}
}
}
void RenderMarkdown(const std::string& content, ImFont* font_ui, ImFont* font_header, ThemeProfile current_theme) {
if (font_ui) ImGui::PushFont(font_ui);
const char* p = content.c_str();
const char* end = p + content.size();
while (p < end) {
const char* line_end = strchr(p, '\n');
if (!line_end) line_end = end;
std::string line(p, line_end);
if (line.substr(0, 2) == "# ") {
if (font_header) ImGui::PushFont(font_header);
ImGui::TextColored(GetThemeColor(ImGuiCol_PlotLines, current_theme), "%s", line.substr(2).c_str());
if (font_header) ImGui::PopFont();
} else if (line.substr(0, 3) == "## ") {
if (font_header) ImGui::PushFont(font_header);
ImGui::Text("%s", line.substr(3).c_str());
if (font_header) ImGui::PopFont();
} else if (line.substr(0, 4) == "### ") ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "%s", line.substr(4).c_str());
else if (line.substr(0, 2) == "- ") { ImGui::Bullet(); ImGui::SameLine(); ImGui::TextWrapped("%s", line.substr(2).c_str()); }
else ImGui::TextWrapped("%s", line.c_str());
p = line_end + 1;
}
if (font_ui) ImGui::PopFont();
}
} // namespace ui
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,33 @@
#pragma once
#include <imgui.h>
#include <string>
#include <vector>
#include <filesystem>
#include "../../models/state.h"
#include "../../data_loader.h"
#include "../../widgets/text_editor.h"
#include "../../widgets/imgui_memory_editor.h"
namespace afs {
namespace viz {
namespace ui {
void RenderKnobsTab(AppState& state, const DataLoader& loader, const std::string& data_path, std::function<void(const char*)> refresh_callback, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback);
void RenderAgentsTab(AppState& state, ImFont* font_ui, ImFont* font_header, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback);
void RenderMissionsTab(AppState& state, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback);
void RenderLogsTab(AppState& state, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback);
void RenderContextTab(AppState& state, TextEditor& text_editor, MemoryEditorWidget& memory_editor, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback);
void RenderAgentPromptTab(AppState& state, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback);
void RenderTablesTab(AppState& state, const DataLoader& loader);
void RenderServicesTab(AppState& state, std::function<void(const std::string&, const std::string&, const std::string&)> log_callback);
void RenderComparisonView(AppState& state, const DataLoader& loader, ImFont* font_ui, ImFont* font_header);
// Helpers
void RefreshBrowserEntries(AppState& state);
void LoadFile(AppState& state, const std::filesystem::path& path, TextEditor& text_editor);
void RenderMarkdown(const std::string& content, ImFont* font_ui, ImFont* font_header, ThemeProfile current_theme);
} // namespace ui
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,275 @@
#include "training_dashboard.h"
#include <imgui.h>
#include <implot.h>
#include <algorithm>
#include <cmath>
namespace afs {
namespace studio {
namespace ui {
TrainingDashboardWidget::TrainingDashboardWidget() {
Refresh();
}
void TrainingDashboardWidget::Refresh() {
std::string error;
if (!monitor_.Poll(&error)) {
last_error_ = error;
} else {
last_error_.clear();
}
}
void TrainingDashboardWidget::Render() {
// Auto-refresh check
if (auto_refresh_ && monitor_.ShouldRefresh()) {
Refresh();
}
// Toolbar
if (ImGui::Button("Refresh")) {
Refresh();
}
ImGui::SameLine();
ImGui::Checkbox("Auto", &auto_refresh_);
ImGui::SameLine();
const auto& state = monitor_.GetState();
// Status indicator
ImVec4 status_color;
const char* status_text;
switch (state.status) {
case TrainingStatus::kRunning:
status_color = ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
status_text = "RUNNING";
break;
case TrainingStatus::kCompleted:
status_color = ImVec4(0.3f, 0.6f, 1.0f, 1.0f);
status_text = "COMPLETED";
break;
case TrainingStatus::kFailed:
status_color = ImVec4(0.9f, 0.3f, 0.3f, 1.0f);
status_text = "FAILED";
break;
case TrainingStatus::kPaused:
status_color = ImVec4(0.9f, 0.7f, 0.2f, 1.0f);
status_text = "PAUSED";
break;
default:
status_color = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
status_text = "IDLE";
break;
}
ImGui::TextColored(status_color, "[%s]", status_text);
if (!last_error_.empty()) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f), "%s",
last_error_.c_str());
}
ImGui::Separator();
// Main content
if (state.status == TrainingStatus::kIdle &&
state.loss_history.empty()) {
ImGui::TextDisabled("No active training detected.");
ImGui::TextDisabled("Mount point: %s",
monitor_.GetConfig().windows_mount_path.c_str());
return;
}
// Top section: Status and Progress
RenderStatusCard();
ImGui::Spacing();
// Loss curve chart
RenderLossCurve();
ImGui::Spacing();
// Metrics grid
RenderMetricsGrid();
// Source info
RenderSourceInfo();
}
void TrainingDashboardWidget::RenderStatusCard() {
const auto& state = monitor_.GetState();
// Model name
if (!state.model_name.empty()) {
ImGui::Text("Model: %s", state.model_name.c_str());
}
// Progress bar
ImGui::Text("Progress:");
ImGui::SameLine();
float progress = state.progress_percent / 100.0f;
char overlay[32];
std::snprintf(overlay, sizeof(overlay), "%.1f%% (%d/%d)",
state.progress_percent, state.current_step, state.total_steps);
ImGui::ProgressBar(progress, ImVec2(-1, 0), overlay);
// Epoch info
ImGui::Text("Epoch %d/%d | Step %d/%d", state.current_epoch,
state.total_epochs, state.current_step, state.total_steps);
}
void TrainingDashboardWidget::RenderLossCurve() {
const auto& state = monitor_.GetState();
if (state.loss_history.empty()) {
ImGui::TextDisabled("No loss data available");
return;
}
// Prepare data for ImPlot
std::vector<float> steps, losses, eval_losses;
bool has_eval = false;
for (const auto& point : state.loss_history) {
steps.push_back(static_cast<float>(point.step));
losses.push_back(point.loss);
if (point.eval_loss > 0.0f) {
eval_losses.push_back(point.eval_loss);
has_eval = true;
} else {
eval_losses.push_back(point.loss); // Fallback
}
}
float height = 200.0f;
if (ImPlot::BeginPlot("Training Loss", ImVec2(-1, height))) {
ImPlot::SetupAxes("Step", "Loss");
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, *std::max_element(losses.begin(), losses.end()) * 1.1, ImPlotCond_Always);
ImPlot::SetNextLineStyle(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), 2.0f);
ImPlot::PlotLine("Train Loss", steps.data(), losses.data(),
static_cast<int>(steps.size()));
if (has_eval) {
ImPlot::SetNextLineStyle(ImVec4(0.9f, 0.5f, 0.2f, 1.0f), 2.0f);
ImPlot::PlotLine("Eval Loss", steps.data(), eval_losses.data(),
static_cast<int>(steps.size()));
}
// Mark best loss
if (state.best_step > 0) {
float best_x = static_cast<float>(state.best_step);
float best_y = state.best_loss;
ImPlot::SetNextMarkerStyle(ImPlotMarker_Diamond, 8.0f,
ImVec4(0.2f, 0.9f, 0.3f, 1.0f), 2.0f);
ImPlot::PlotScatter("Best", &best_x, &best_y, 1);
}
ImPlot::EndPlot();
}
}
void TrainingDashboardWidget::RenderMetricsGrid() {
const auto& state = monitor_.GetState();
if (ImGui::BeginTable("MetricsGrid", 4,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Metric");
ImGui::TableSetupColumn("Value");
ImGui::TableSetupColumn("Metric");
ImGui::TableSetupColumn("Value");
ImGui::TableHeadersRow();
// Row 1: Current Loss | Best Loss
ImGui::TableNextColumn();
ImGui::TextDisabled("Current Loss");
ImGui::TableNextColumn();
ImGui::Text("%.4f", state.current_loss);
ImGui::TableNextColumn();
ImGui::TextDisabled("Best Loss");
ImGui::TableNextColumn();
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), "%.4f @ %d",
state.best_loss, state.best_step);
// Row 2: Eval Loss | Perplexity
ImGui::TableNextColumn();
ImGui::TextDisabled("Eval Loss");
ImGui::TableNextColumn();
if (state.eval_loss.has_value()) {
ImGui::Text("%.4f", state.eval_loss.value());
} else {
ImGui::TextDisabled("N/A");
}
ImGui::TableNextColumn();
ImGui::TextDisabled("Perplexity");
ImGui::TableNextColumn();
if (state.perplexity.has_value()) {
ImGui::Text("%.2f", state.perplexity.value());
} else {
ImGui::TextDisabled("N/A");
}
// Row 3: GPU | ETA
ImGui::TableNextColumn();
ImGui::TextDisabled("Device");
ImGui::TableNextColumn();
ImGui::Text("%s", state.device.empty() ? "GPU" : state.device.c_str());
ImGui::TableNextColumn();
ImGui::TextDisabled("ETA");
ImGui::TableNextColumn();
if (state.estimated_remaining_minutes > 0) {
int hours = state.estimated_remaining_minutes / 60;
int mins = state.estimated_remaining_minutes % 60;
if (hours > 0) {
ImGui::Text("%dh %dm", hours, mins);
} else {
ImGui::Text("%dm", mins);
}
} else {
ImGui::TextDisabled("--");
}
ImGui::EndTable();
}
}
void TrainingDashboardWidget::RenderSourceInfo() {
const auto& state = monitor_.GetState();
ImGui::Spacing();
ImGui::TextDisabled("Source: %s (%s)",
state.source_location.empty() ? "local"
: state.source_location.c_str(),
state.is_remote ? "remote" : "local");
if (!state.source_path.empty()) {
ImGui::TextDisabled("Path: %s", state.source_path.c_str());
}
}
void RenderTrainingDashboardWindow(TrainingDashboardWidget& widget,
bool* open) {
if (!open || !*open) return;
ImGui::SetNextWindowSize(ImVec2(700, 500), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Training Monitor", open)) {
ImGui::End();
return;
}
widget.Render();
ImGui::End();
}
} // namespace ui
} // namespace studio
} // namespace afs

View File

@@ -0,0 +1,42 @@
#pragma once
#include "core/training_monitor.h"
#include <string>
namespace afs {
namespace studio {
namespace ui {
// Training dashboard widget for real-time training monitoring
class TrainingDashboardWidget {
public:
TrainingDashboardWidget();
// Render the widget
void Render();
// Manual refresh
void Refresh();
// Get training monitor
TrainingMonitor& GetMonitor() { return monitor_; }
private:
void RenderStatusCard();
void RenderProgressBar();
void RenderLossCurve();
void RenderMetricsGrid();
void RenderSourceInfo();
TrainingMonitor monitor_;
bool auto_refresh_ = true;
std::string last_error_;
};
// Render as standalone window
void RenderTrainingDashboardWindow(TrainingDashboardWidget& widget, bool* open);
} // namespace ui
} // namespace studio
} // namespace afs

291
apps/studio/src/ui/core.cc Normal file
View File

@@ -0,0 +1,291 @@
#include "core.h"
#include <implot.h>
#include <algorithm>
#include <vector>
#include "../icons.h"
namespace afs {
namespace viz {
namespace ui {
void HelpMarker(const char* desc) {
ImGui::TextDisabled("(?)");
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
ImGui::TextUnformatted(desc);
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
}
void ApplyPremiumPlotStyles(const char* plot_id, AppState& state) {
// Custom theme-like styling for AFS
ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.18f);
ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, state.line_weight);
ImPlot::PushStyleVar(ImPlotStyleVar_MarkerSize, state.show_plot_markers ? 5.5f : 0.0f);
ImPlot::PushStyleVar(ImPlotStyleVar_MarkerWeight, 1.4f);
ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(14, 14));
ImPlot::PushStyleVar(ImPlotStyleVar_LabelPadding, ImVec2(6, 4));
// Standard grid line color (subtle)
ImPlot::PushStyleColor(ImPlotCol_AxisGrid, ImVec4(1, 1, 1, 0.12f));
ImPlot::PushStyleColor(ImPlotCol_Line, GetThemeColor(ImGuiCol_PlotLines, state.current_theme));
}
void RenderChartHeader(PlotKind kind, const char* title, const char* desc, AppState& state) {
ImGui::PushStyleColor(ImGuiCol_Text, GetThemeColor(ImGuiCol_PlotLines, state.current_theme));
ImGui::Text("%s", title);
ImGui::PopStyleColor();
if (desc && desc[0] != '\0') {
ImGui::SameLine();
HelpMarker(desc);
}
if (kind != PlotKind::None) {
float button_size = ImGui::GetFrameHeight();
ImGui::SameLine();
float right_edge = ImGui::GetWindowContentRegionMax().x - button_size;
ImGui::SetCursorPosX(right_edge);
ImGui::PushID(static_cast<int>(kind));
const char* icon = state.is_rendering_expanded_plot ? ICON_MD_CLOSE_FULLSCREEN
: ICON_MD_OPEN_IN_FULL;
if (ImGui::Button(icon, ImVec2(button_size, button_size))) {
if (state.is_rendering_expanded_plot) {
state.expanded_plot = PlotKind::None;
} else {
state.expanded_plot = kind;
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(state.is_rendering_expanded_plot ? "Exit full screen"
: "Expand to full screen");
}
// New: Pop-out button (GIMP style)
ImGui::SameLine();
ImGui::SetCursorPosX(right_edge - button_size - 4);
if (ImGui::Button(ICON_MD_OPEN_IN_NEW, ImVec2(button_size, button_size))) {
bool found = false;
for (auto f : state.active_floaters) {
if (f == kind) { found = true; break; }
}
if (!found) state.active_floaters.push_back(kind);
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Pop out to standalone window");
// Update inspector context if clicked
if (ImGui::IsItemClicked()) {
state.inspector_context = kind;
}
ImGui::PopID();
}
ImGui::Dummy(ImVec2(0.0f, 4.0f));
}
void HandlePlotContextMenu(PlotKind kind, AppState& state) {
if (kind == PlotKind::None) return;
ImGui::PushID(static_cast<int>(kind));
if (ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
ImGui::OpenPopup("PlotContext");
}
if (ImGui::BeginPopup("PlotContext")) {
if (state.expanded_plot == kind) {
if (ImGui::MenuItem(ICON_MD_FULLSCREEN_EXIT " Exit Full Screen")) {
state.expanded_plot = PlotKind::None;
}
} else if (kind != PlotKind::None) {
if (ImGui::MenuItem(ICON_MD_FULLSCREEN " Expand to Full Screen")) {
state.expanded_plot = kind;
}
}
if (state.focus_chart == kind) {
if (ImGui::MenuItem(ICON_MD_REMOVE_CIRCLE_OUTLINE " Remove from Focus")) {
state.focus_chart = PlotKind::None;
}
} else if (kind != PlotKind::None) {
if (ImGui::MenuItem(ICON_MD_CENTER_FOCUS_STRONG " Focus on Dashboard")) {
state.focus_chart = kind;
state.current_workspace = Workspace::Dashboard;
}
}
if (ImGui::MenuItem(ICON_MD_OPEN_IN_NEW " Pop out to Window")) {
bool already_open = false;
for (auto k : state.active_floaters) { if (k == kind) { already_open = true; break; } }
if (!already_open) state.active_floaters.push_back(kind);
}
if (ImGui::MenuItem("Reset View")) {
ImPlot::SetNextAxesToFit();
}
ImGui::Separator();
if (ImGui::BeginMenu("Settings")) {
ImGui::Checkbox("Legends", &state.show_plot_legends);
ImGui::Checkbox("Markers", &state.show_plot_markers);
ImGui::Checkbox("Interaction", &state.enable_plot_interaction);
ImGui::EndMenu();
}
ImGui::EndPopup();
}
ImGui::PopID();
}
ImVec4 GetThemeColor(ImGuiCol col, ThemeProfile theme) {
switch (theme) {
case ThemeProfile::Amber:
if (col == ImGuiCol_Text) return ImVec4(1.0f, 0.7f, 0.0f, 1.0f);
if (col == ImGuiCol_Header) return ImVec4(1.0f, 0.6f, 0.0f, 0.4f);
if (col == ImGuiCol_PlotLines) return ImVec4(1.0f, 0.8f, 0.0f, 1.0f);
break;
case ThemeProfile::Emerald:
if (col == ImGuiCol_Text) return ImVec4(0.0f, 1.0f, 0.4f, 1.0f);
if (col == ImGuiCol_Header) return ImVec4(0.0f, 0.8f, 0.3f, 0.4f);
if (col == ImGuiCol_PlotLines) return ImVec4(0.2f, 1.0f, 0.5f, 1.0f);
break;
case ThemeProfile::Cyberpunk:
if (col == ImGuiCol_Text) return ImVec4(1.0f, 0.0f, 0.5f, 1.0f);
if (col == ImGuiCol_Header) return ImVec4(0.1f, 0.0f, 0.2f, 0.4f);
if (col == ImGuiCol_PlotLines) return ImVec4(0.0f, 1.0f, 1.0f, 1.0f);
break;
case ThemeProfile::Monochrome:
if (col == ImGuiCol_Text) return ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
if (col == ImGuiCol_Header) return ImVec4(0.2f, 0.2f, 0.2f, 0.4f);
if (col == ImGuiCol_PlotLines) return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
break;
case ThemeProfile::Solarized:
if (col == ImGuiCol_Text) return ImVec4(0.52f, 0.60f, 0.00f, 1.0f);
if (col == ImGuiCol_Header) return ImVec4(0.03f, 0.21f, 0.26f, 0.4f);
if (col == ImGuiCol_PlotLines) return ImVec4(0.15f, 0.45f, 0.55f, 1.0f);
break;
case ThemeProfile::Nord:
if (col == ImGuiCol_Text) return ImVec4(0.56f, 0.80f, 0.71f, 1.0f);
if (col == ImGuiCol_Header) return ImVec4(0.18f, 0.20f, 0.25f, 0.4f);
if (col == ImGuiCol_PlotLines) return ImVec4(0.53f, 0.75f, 0.82f, 1.0f);
break;
case ThemeProfile::Dracula:
if (col == ImGuiCol_Text) return ImVec4(1.00f, 0.47f, 0.77f, 1.0f);
if (col == ImGuiCol_Header) return ImVec4(0.16f, 0.17f, 0.24f, 0.4f);
if (col == ImGuiCol_PlotLines) return ImVec4(0.74f, 0.57f, 0.97f, 1.0f);
break;
default: // Cobalt
if (col == ImGuiCol_PlotLines) return ImVec4(0.4f, 0.8f, 1.0f, 1.0f);
break;
}
return ImGui::GetStyleColorVec4(col);
}
ImVec4 GetSeriesColor(int index) {
static const ImVec4 palette[] = {
ImVec4(0.20f, 0.65f, 0.96f, 1.0f),
ImVec4(0.96f, 0.54f, 0.24f, 1.0f),
ImVec4(0.90f, 0.28f, 0.46f, 1.0f),
ImVec4(0.38f, 0.88f, 0.46f, 1.0f),
ImVec4(0.86f, 0.80f, 0.26f, 1.0f),
ImVec4(0.62f, 0.38f, 0.96f, 1.0f),
ImVec4(0.20f, 0.86f, 0.82f, 1.0f),
ImVec4(0.92f, 0.34f, 0.22f, 1.0f),
ImVec4(0.64f, 0.72f, 0.98f, 1.0f),
ImVec4(0.75f, 0.56f, 0.36f, 1.0f),
ImVec4(0.95f, 0.78f, 0.36f, 1.0f),
ImVec4(0.42f, 0.74f, 0.90f, 1.0f),
};
const int count = static_cast<int>(sizeof(palette) / sizeof(palette[0]));
int safe_index = index % count;
if (safe_index < 0) safe_index += count;
return palette[safe_index];
}
ImVec4 GetStepColor(float step, AppState& state) {
ImVec4 base = GetThemeColor(ImGuiCol_PlotLines, state.current_theme);
float alpha = 0.2f + 0.8f * step;
return ImVec4(base.x, base.y, base.z, alpha);
}
int GetPlotAxisFlags(const AppState& state) {
int flags = ImPlotAxisFlags_None;
if (!state.enable_plot_interaction) {
flags |= ImPlotAxisFlags_Lock;
}
return flags;
}
ImPlotFlags BasePlotFlags(const AppState& state, bool allow_legend) {
ImPlotFlags flags = ImPlotFlags_NoMenus;
if (!allow_legend || !state.show_plot_legends) {
flags |= ImPlotFlags_NoLegend;
}
return flags;
}
const std::vector<PlotOption>& PlotOptions() {
static const std::vector<PlotOption> options = {
{PlotKind::QualityTrends, "Quality Trends"},
{PlotKind::GeneratorEfficiency, "Generator Efficiency"},
{PlotKind::CoverageDensity, "Coverage Density"},
{PlotKind::TrainingLoss, "Training Loss (Top Runs)"},
{PlotKind::LossVsSamples, "Loss vs Samples"},
{PlotKind::DomainCoverage, "Domain Coverage"},
{PlotKind::EmbeddingQuality, "Embedding Quality"},
{PlotKind::AgentThroughput, "Agent Throughput"},
{PlotKind::MissionQueue, "Mission Queue"},
{PlotKind::QualityDirection, "Quality Direction"},
{PlotKind::GeneratorMix, "Generator Mix"},
{PlotKind::EmbeddingDensity, "Embedding Density"},
{PlotKind::AgentUtilization, "Agent Utilization"},
{PlotKind::MissionProgress, "Mission Progress"},
{PlotKind::EvalMetrics, "Eval Metrics"},
{PlotKind::Rejections, "Rejection Reasons"},
{PlotKind::KnowledgeGraph, "Knowledge Graph"},
{PlotKind::LatentSpace, "Latent Space"},
{PlotKind::Effectiveness, "Domain Effectiveness"},
{PlotKind::Thresholds, "Threshold Sensitivity"},
{PlotKind::MountsStatus, "Local Mounts Status"},
};
return options;
}
int PlotKindToIndex(PlotKind kind) {
const auto& options = PlotOptions();
for (size_t i = 0; i < options.size(); ++i) {
if (options[i].kind == kind) return static_cast<int>(i);
}
return 0;
}
PlotKind IndexToPlotKind(int index) {
const auto& options = PlotOptions();
if (index < 0 || static_cast<size_t>(index) >= options.size()) {
return options.front().kind;
}
return options[index].kind;
}
float Clamp01(float value) {
return std::max(0.0f, std::min(1.0f, value));
}
void AppendLog(AppState& state, const std::string& agent, const std::string& message, const std::string& kind) {
if (message.empty()) return;
constexpr size_t kMaxLogs = 300;
state.logs.push_back(LogEntry{agent, message, kind});
if (state.logs.size() > kMaxLogs) state.logs.pop_front();
}
AgentState* FindAgentByName(std::vector<AgentState>& agents, const std::string& name) {
for (auto& agent : agents) {
if (agent.name == name) return &agent;
}
return nullptr;
}
} // namespace ui
} // namespace viz
} // namespace afs

41
apps/studio/src/ui/core.h Normal file
View File

@@ -0,0 +1,41 @@
#pragma once
#include <imgui.h>
#include <implot.h>
#include "../models/state.h"
namespace afs {
namespace viz {
namespace ui {
// Utility Functions
void HelpMarker(const char* desc);
void ApplyPremiumPlotStyles(const char* plot_id, AppState& state);
void RenderChartHeader(PlotKind kind, const char* title, const char* desc, AppState& state);
void HandlePlotContextMenu(PlotKind kind, AppState& state);
float Clamp01(float value);
void AppendLog(AppState& state, const std::string& agent, const std::string& message, const std::string& kind);
AgentState* FindAgentByName(std::vector<AgentState>& agents, const std::string& name);
// Theme Helpers
ImVec4 GetThemeColor(ImGuiCol col, ThemeProfile theme);
ImVec4 GetSeriesColor(int index);
ImVec4 GetStepColor(float step, AppState& state);
ImVec4 GetStepColor(float step, AppState& state);
int GetPlotAxisFlags(const AppState& state);
ImPlotFlags BasePlotFlags(const AppState& state, bool allow_legend);
// Grid Helpers
struct PlotOption {
PlotKind kind;
const char* label;
};
const std::vector<PlotOption>& PlotOptions();
int PlotKindToIndex(PlotKind kind);
PlotKind IndexToPlotKind(int index);
} // namespace ui
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,234 @@
#include "chat_panel.h"
#include "../core.h"
#include "../../icons.h"
#include <imgui.h>
#include <vector>
#include <mutex>
namespace afs::viz::ui {
namespace {
// Streaming response state (shared between callbacks and render)
std::mutex streaming_mutex;
std::string streaming_response;
bool is_streaming = false;
}
void RenderChatPanel(AppState& state,
LlamaClient& llama_client,
std::function<void(const std::string&, const std::string&, const std::string&)> log_callback) {
// Toolbar
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4));
// Status indicator
bool is_ready = llama_client.IsReady();
bool is_generating = llama_client.IsGenerating();
if (is_generating) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_PENDING " Generating...");
} else if (is_ready) {
ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), ICON_MD_CHECK_CIRCLE " Ready");
} else {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR " Not Connected");
}
ImGui::SameLine();
if (ImGui::Button(ICON_MD_REFRESH " Check")) {
llama_client.CheckHealth();
}
ImGui::SameLine();
if (ImGui::Button(ICON_MD_DELETE " Clear")) {
state.logs.clear();
llama_client.ClearHistory();
std::lock_guard<std::mutex> lock(streaming_mutex);
streaming_response.clear();
is_streaming = false;
}
ImGui::SameLine();
if (is_generating && ImGui::Button(ICON_MD_STOP " Stop")) {
llama_client.StopGeneration();
}
// Config section (collapsible)
ImGui::SameLine();
if (ImGui::CollapsingHeader(ICON_MD_SETTINGS " Config", ImGuiTreeNodeFlags_None)) {
LlamaConfig config = llama_client.GetConfig();
bool config_changed = false;
ImGui::SetNextItemWidth(300);
static char model_path[512] = "";
if (model_path[0] == '\0' && !config.model_path.empty()) {
strncpy(model_path, config.model_path.c_str(), sizeof(model_path) - 1);
}
if (ImGui::InputText("Model Path", model_path, sizeof(model_path))) {
config.model_path = model_path;
config_changed = true;
}
ImGui::SetNextItemWidth(200);
static char rpc_servers[256] = "100.104.53.21:50052";
if (ImGui::InputText("RPC Servers", rpc_servers, sizeof(rpc_servers))) {
config.rpc_servers = rpc_servers;
config_changed = true;
}
if (ImGui::Checkbox("Use RPC", &config.use_rpc)) {
config_changed = true;
}
ImGui::SetNextItemWidth(100);
if (ImGui::SliderFloat("Temperature", &config.temperature, 0.0f, 2.0f)) {
config_changed = true;
}
ImGui::SetNextItemWidth(100);
if (ImGui::SliderInt("Max Tokens", &config.n_predict, 32, 2048)) {
config_changed = true;
}
if (config_changed) {
llama_client.SetConfig(config);
}
}
ImGui::PopStyleVar();
ImGui::Separator();
// Chat Area
float footer_height = 50.0f;
ImVec2 chat_size = ImVec2(0, ImGui::GetContentRegionAvail().y - footer_height);
ImGui::BeginChild("ChatHistory", chat_size, true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
// Display history from LlamaClient
const auto& history = llama_client.GetHistory();
for (const auto& msg : history) {
ImVec4 color;
const char* icon;
if (msg.role == "user") {
color = ImVec4(0.4f, 0.8f, 1.0f, 1.0f);
icon = ICON_MD_PERSON;
} else if (msg.role == "assistant") {
color = ImVec4(0.4f, 1.0f, 0.6f, 1.0f);
icon = ICON_MD_SMART_TOY;
} else {
color = ImVec4(1.0f, 0.6f, 0.2f, 1.0f);
icon = ICON_MD_INFO;
}
ImGui::TextColored(color, "%s %s", icon, msg.role.c_str());
ImGui::Indent();
ImGui::PushTextWrapPos(ImGui::GetContentRegionAvail().x);
ImGui::TextUnformatted(msg.content.c_str());
ImGui::PopTextWrapPos();
ImGui::Unindent();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
}
// Show streaming response
{
std::lock_guard<std::mutex> lock(streaming_mutex);
if (is_streaming && !streaming_response.empty()) {
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.6f, 1.0f), ICON_MD_SMART_TOY " assistant");
ImGui::Indent();
ImGui::PushTextWrapPos(ImGui::GetContentRegionAvail().x);
ImGui::TextUnformatted(streaming_response.c_str());
ImGui::PopTextWrapPos();
// Blinking cursor
static float blink_timer = 0.0f;
blink_timer += ImGui::GetIO().DeltaTime;
if (fmod(blink_timer, 1.0f) < 0.5f) {
ImGui::SameLine(0, 0);
ImGui::Text("_");
}
ImGui::Unindent();
}
}
// Auto-scroll
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 50) {
ImGui::SetScrollHereY(1.0f);
}
ImGui::EndChild();
// Input Area
ImGui::Separator();
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 8));
float send_button_width = 80.0f;
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - send_button_width - 10);
bool send_triggered = false;
if (ImGui::InputTextWithHint("##ChatInput", "Type a message...",
state.chat_input.data(), state.chat_input.size(),
ImGuiInputTextFlags_EnterReturnsTrue)) {
send_triggered = true;
}
ImGui::SameLine();
bool can_send = is_ready && !is_generating && state.chat_input[0] != '\0';
if (!can_send) {
ImGui::BeginDisabled();
}
if (ImGui::Button(ICON_MD_SEND " Send", ImVec2(send_button_width, 0)) || send_triggered) {
if (can_send) {
std::string message(state.chat_input.data());
// Log user message
if (log_callback) {
log_callback("user", message, "user");
}
// Clear streaming state
{
std::lock_guard<std::mutex> lock(streaming_mutex);
streaming_response.clear();
is_streaming = true;
}
// Send to LlamaClient
llama_client.SendMessage(
message,
// Token callback - accumulate streaming response
[](const std::string& token) {
std::lock_guard<std::mutex> lock(streaming_mutex);
streaming_response += token;
},
// Completion callback
[log_callback](bool success, const std::string& error) {
std::lock_guard<std::mutex> lock(streaming_mutex);
is_streaming = false;
if (!success && log_callback) {
log_callback("system", "Error: " + error, "system");
}
}
);
// Clear input
state.chat_input[0] = '\0';
ImGui::SetKeyboardFocusHere(-1);
}
}
if (!can_send) {
ImGui::EndDisabled();
}
ImGui::PopStyleVar();
// Status bar
ImGui::TextDisabled("%s", llama_client.GetStatusMessage().c_str());
}
} // namespace afs::viz::ui

View File

@@ -0,0 +1,15 @@
#pragma once
#include "../../models/state.h"
#include "../../core/llama_client.h"
#include <functional>
#include <string>
namespace afs::viz::ui {
// A clean, log-based chat interface with llama.cpp integration.
void RenderChatPanel(AppState& state,
LlamaClient& llama_client,
std::function<void(const std::string&, const std::string&, const std::string&)> log_callback);
} // namespace afs::viz::ui

View File

@@ -0,0 +1,625 @@
#include "shortcuts.h"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
namespace afs {
namespace viz {
namespace ui {
namespace {
const std::array<ActionDefinition, kActionCount>& ActionDefinitions() {
static const std::array<ActionDefinition, kActionCount> definitions = {
ActionDefinition{ActionId::Refresh, "refresh", "Refresh Data",
"Reload data from all configured sources.",
Shortcut{ImGuiKey_F5, false, false, false, false}},
ActionDefinition{ActionId::Quit, "quit", "Quit",
"Close the visualization application.",
Shortcut{ImGuiKey_Q, true, false, false, false}},
ActionDefinition{ActionId::ToggleInspector, "toggle_inspector",
"Inspector Panel",
"Show or hide the Inspector panel.",
Shortcut{ImGuiKey_I, true, false, false, false}},
ActionDefinition{ActionId::ToggleDatasetPanel, "toggle_dataset",
"Dataset Panel",
"Show or hide the Dataset panel.",
Shortcut{ImGuiKey_D, true, true, false, false}},
ActionDefinition{ActionId::ToggleSystemsPanel, "toggle_systems",
"Systems Panel",
"Show or hide the Systems panel.",
Shortcut{ImGuiKey_S, true, true, false, false}},
ActionDefinition{ActionId::ToggleStatusBar, "toggle_status",
"Status Strip",
"Show or hide the status strip.",
Shortcut{ImGuiKey_B, true, true, false, false}},
ActionDefinition{ActionId::ToggleControls, "toggle_controls",
"Sidebar Controls",
"Show or hide the sidebar controls.",
Shortcut{ImGuiKey_C, true, true, false, false}},
ActionDefinition{ActionId::ToggleAutoRefresh, "toggle_auto_refresh",
"Auto Refresh",
"Enable or disable auto refresh of data.",
Shortcut{ImGuiKey_A, true, true, false, false}},
ActionDefinition{ActionId::ToggleSimulation, "toggle_simulation",
"Simulate Activity",
"Enable or disable simulated activity streams.",
Shortcut{ImGuiKey_Y, true, true, false, false}},
ActionDefinition{ActionId::ToggleCompactUI, "toggle_compact_ui",
"Compact Charts",
"Toggle compact chart layout.",
Shortcut{ImGuiKey_U, true, true, false, false}},
ActionDefinition{ActionId::ToggleLockLayout, "toggle_lock_layout",
"Lock Layout",
"Lock or unlock the docking layout.",
Shortcut{ImGuiKey_L, true, true, false, false}},
ActionDefinition{ActionId::ResetLayout, "reset_layout", "Reset Layout",
"Reset the docking layout to defaults.",
Shortcut{ImGuiKey_R, true, true, true, false}},
ActionDefinition{ActionId::ToggleSampleReview, "toggle_sample_review",
"Sample Review",
"Open the sample review window.",
Shortcut{ImGuiKey_R, true, true, false, false}},
ActionDefinition{ActionId::ToggleShortcutsWindow, "toggle_shortcuts",
"Keyboard Shortcuts",
"Open the shortcuts editor.",
Shortcut{ImGuiKey_Slash, true, false, false, false}},
ActionDefinition{ActionId::ToggleDemoWindow, "toggle_demo",
"ImGui Demo",
"Toggle the ImGui demo window.",
Shortcut{ImGuiKey_M, true, true, false, false}},
ActionDefinition{ActionId::WorkspaceDashboard, "workspace_dashboard",
"Workspace: Dashboard",
"Switch to the Dashboard workspace.",
Shortcut{ImGuiKey_1, true, false, false, false}},
ActionDefinition{ActionId::WorkspaceAnalysis, "workspace_analysis",
"Workspace: Analysis",
"Switch to the Analysis workspace.",
Shortcut{ImGuiKey_2, true, false, false, false}},
ActionDefinition{ActionId::WorkspaceOptimization,
"workspace_optimization",
"Workspace: Optimization",
"Switch to the Optimization workspace.",
Shortcut{ImGuiKey_3, true, false, false, false}},
ActionDefinition{ActionId::WorkspaceSystems, "workspace_systems",
"Workspace: Systems",
"Switch to the Systems workspace.",
Shortcut{ImGuiKey_4, true, false, false, false}},
ActionDefinition{ActionId::WorkspaceCustom, "workspace_custom",
"Workspace: Custom Grid",
"Switch to the Custom Grid workspace.",
Shortcut{ImGuiKey_5, true, false, false, false}},
ActionDefinition{ActionId::WorkspaceChat, "workspace_chat",
"Workspace: Chat",
"Switch to the Chat workspace.",
Shortcut{ImGuiKey_6, true, false, false, false}},
ActionDefinition{ActionId::WorkspaceTraining, "workspace_training",
"Workspace: Training Hub",
"Switch to the Training Hub workspace.",
Shortcut{ImGuiKey_7, true, false, false, false}},
ActionDefinition{ActionId::WorkspaceContext, "workspace_context",
"Workspace: Context Broker",
"Switch to the Context Broker workspace.",
Shortcut{ImGuiKey_8, true, false, false, false}},
// Graph View Navigation
ActionDefinition{ActionId::ToggleGraphBrowser, "toggle_graph_browser",
"Toggle Graph Browser", "Show/hide graph browser sidebar",
Shortcut{ImGuiKey_B, true, false, false, false}},
ActionDefinition{ActionId::ToggleCompanionPanels, "toggle_companion_panels",
"Toggle Companion Panels", "Show/hide companion panels sidebar",
Shortcut{ImGuiKey_Backslash, true, false, false, false}},
ActionDefinition{ActionId::NavigateBack, "navigate_back",
"Navigate Back", "Go to previous graph in history",
Shortcut{ImGuiKey_LeftArrow, false, false, true, false}},
ActionDefinition{ActionId::NavigateForward, "navigate_forward",
"Navigate Forward", "Go to next graph in history",
Shortcut{ImGuiKey_RightArrow, false, false, true, false}},
ActionDefinition{ActionId::BookmarkGraph, "bookmark_graph",
"Bookmark Graph", "Add/remove current graph from bookmarks",
Shortcut{ImGuiKey_D, true, false, false, false}},
ActionDefinition{ActionId::OpenGraphPalette, "open_graph_palette",
"Open Graph Palette", "Quick graph switcher",
Shortcut{ImGuiKey_K, true, false, false, false}},
};
return definitions;
}
bool ContainsInsensitive(const std::string& text, const std::string& pattern) {
if (pattern.empty()) return true;
auto it = std::search(text.begin(), text.end(), pattern.begin(),
pattern.end(), [](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
});
return it != text.end();
}
} // namespace
ShortcutManager::ShortcutManager() {
config_path_ = ResolveDefaultPath();
const auto& defs = ActionDefinitions();
for (const auto& def : defs) {
bindings_[ToIndex(def.id)] = def.default_shortcut;
}
}
const std::array<ActionDefinition, kActionCount>& ShortcutManager::Actions() const {
return ActionDefinitions();
}
const ActionDefinition& ShortcutManager::GetDefinition(ActionId id) const {
return ActionDefinitions()[ToIndex(id)];
}
const Shortcut& ShortcutManager::GetShortcut(ActionId id) const {
return bindings_[ToIndex(id)];
}
void ShortcutManager::SetShortcut(ActionId id, const Shortcut& shortcut) {
Shortcut& slot = bindings_[ToIndex(id)];
if (slot.key == shortcut.key && slot.ctrl == shortcut.ctrl &&
slot.shift == shortcut.shift && slot.alt == shortcut.alt &&
slot.super == shortcut.super) {
return;
}
slot = shortcut;
dirty_ = true;
}
void ShortcutManager::ResetShortcut(ActionId id) {
SetShortcut(id, GetDefinition(id).default_shortcut);
}
void ShortcutManager::ResetAll() {
const auto& defs = ActionDefinitions();
for (const auto& def : defs) {
bindings_[ToIndex(def.id)] = def.default_shortcut;
}
dirty_ = true;
}
void ShortcutManager::SetConfigPath(std::string path) {
if (path.empty()) {
config_path_ = ResolveDefaultPath();
return;
}
config_path_ = std::move(path);
}
bool ShortcutManager::LoadFromDisk(std::string* error) {
last_error_.clear();
if (config_path_.empty()) {
config_path_ = ResolveDefaultPath();
}
std::filesystem::path path(config_path_);
if (!std::filesystem::exists(path)) {
if (error) *error = "";
return false;
}
std::ifstream file(path);
if (!file.is_open()) {
last_error_ = "Failed to open shortcut config";
if (error) *error = last_error_;
return false;
}
std::string line;
bool ok = true;
while (std::getline(file, line)) {
std::string trimmed = Trim(line);
if (trimmed.empty()) continue;
if (trimmed[0] == '#' || trimmed[0] == ';') continue;
size_t pos = trimmed.find('=');
if (pos == std::string::npos) {
ok = false;
continue;
}
std::string key = Trim(trimmed.substr(0, pos));
std::string value = Trim(trimmed.substr(pos + 1));
const ActionDefinition* def = FindDefinition(key);
if (!def) {
ok = false;
continue;
}
Shortcut shortcut;
std::string parse_error;
if (!ParseShortcutString(value, &shortcut, &parse_error)) {
ok = false;
continue;
}
bindings_[ToIndex(def->id)] = shortcut;
}
dirty_ = false;
if (!ok) {
last_error_ = "Some shortcuts could not be parsed";
if (error) *error = last_error_;
}
return ok;
}
bool ShortcutManager::SaveToDisk(std::string* error) {
last_error_.clear();
if (config_path_.empty()) {
last_error_ = "Missing config path";
if (error) *error = last_error_;
return false;
}
std::filesystem::path path(config_path_);
std::filesystem::path parent = path.parent_path();
if (!parent.empty()) {
std::error_code ec;
std::filesystem::create_directories(parent, ec);
if (ec) {
last_error_ = "Failed to create shortcut config directory";
if (error) *error = last_error_;
return false;
}
}
std::ofstream file(path, std::ios::trunc);
if (!file.is_open()) {
last_error_ = "Failed to write shortcut config";
if (error) *error = last_error_;
return false;
}
file << "# AFS Viz shortcuts\n";
file << "# Format: action_key = Ctrl+Shift+Key\n";
const ImGuiIO& io = ImGui::GetIO();
for (const auto& def : Actions()) {
std::string value = FormatShortcut(GetShortcut(def.id), io);
file << def.config_key << " = " << value << "\n";
}
if (!file) {
last_error_ = "Failed to write shortcut config";
if (error) *error = last_error_;
return false;
}
dirty_ = false;
return true;
}
bool ShortcutManager::SaveIfDirty(std::string* error) {
if (!dirty_) return false;
std::string save_error;
if (!SaveToDisk(&save_error)) {
last_error_ = save_error;
if (error) *error = last_error_;
return false;
}
dirty_ = false;
return true;
}
void ShortcutManager::BeginCapture(ActionId id) {
capturing_action_ = id;
capturing_ = true;
}
void ShortcutManager::CancelCapture() {
capturing_action_ = ActionId::Count;
capturing_ = false;
}
bool ShortcutManager::HandleCapture(const ImGuiIO& io) {
if (!capturing_) return false;
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
CancelCapture();
return true;
}
for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; ++key) {
ImGuiKey key_value = static_cast<ImGuiKey>(key);
if (!IsValidShortcutKey(key_value)) continue;
if (ImGui::IsKeyPressed(key_value, false)) {
Shortcut shortcut;
shortcut.key = key_value;
shortcut.ctrl = io.KeyCtrl;
shortcut.shift = io.KeyShift;
shortcut.alt = io.KeyAlt;
shortcut.super = io.KeySuper;
SetShortcut(capturing_action_, shortcut);
CancelCapture();
return true;
}
}
return false;
}
bool ShortcutManager::ShouldProcessShortcuts(const ImGuiIO& io) const {
if (capturing_) return false;
if (io.WantTextInput) return false;
return true;
}
bool ShortcutManager::IsTriggered(ActionId id, const ImGuiIO& io) const {
if (!ShouldProcessShortcuts(io)) return false;
const Shortcut& shortcut = GetShortcut(id);
if (shortcut.key == ImGuiKey_None) return false;
if (!ImGui::IsKeyPressed(shortcut.key, false)) return false;
if (shortcut.ctrl != io.KeyCtrl) return false;
if (shortcut.shift != io.KeyShift) return false;
if (shortcut.alt != io.KeyAlt) return false;
if (shortcut.super != io.KeySuper) return false;
return true;
}
std::string ShortcutManager::FormatShortcut(ActionId id, const ImGuiIO& io) const {
return FormatShortcut(GetShortcut(id), io);
}
std::string ShortcutManager::FormatShortcut(const Shortcut& shortcut,
const ImGuiIO& io) {
static_cast<void>(io);
if (shortcut.key == ImGuiKey_None) return "";
std::string label;
auto append = [&](const char* part) {
if (!label.empty()) label += "+";
label += part;
};
if (shortcut.ctrl) append("Ctrl");
if (shortcut.shift) append("Shift");
if (shortcut.alt) append("Alt");
if (shortcut.super) append("Super");
const char* key_name = ImGui::GetKeyName(shortcut.key);
append(key_name && key_name[0] != '\0' ? key_name : "?");
return label;
}
size_t ShortcutManager::ToIndex(ActionId id) {
return static_cast<size_t>(id);
}
const ActionDefinition* ShortcutManager::FindDefinition(
const std::string& key) const {
std::string needle = ToLower(key);
for (const auto& def : Actions()) {
if (ToLower(def.config_key) == needle) return &def;
if (ToLower(def.label) == needle) return &def;
}
return nullptr;
}
std::string ShortcutManager::Trim(const std::string& text) {
size_t start = 0;
while (start < text.size() &&
std::isspace(static_cast<unsigned char>(text[start]))) {
++start;
}
size_t end = text.size();
while (end > start &&
std::isspace(static_cast<unsigned char>(text[end - 1]))) {
--end;
}
return text.substr(start, end - start);
}
std::string ShortcutManager::ToLower(const std::string& text) {
std::string out = text;
std::transform(out.begin(), out.end(), out.begin(),
[](unsigned char ch) { return std::tolower(ch); });
return out;
}
ImGuiKey ShortcutManager::FindKeyByName(const std::string& name) {
std::string target = ToLower(name);
for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; ++key) {
ImGuiKey key_value = static_cast<ImGuiKey>(key);
const char* key_name = ImGui::GetKeyName(key_value);
if (!key_name || key_name[0] == '\0') continue;
if (ToLower(key_name) == target) return key_value;
}
return ImGuiKey_None;
}
bool ShortcutManager::ParseShortcutString(const std::string& text,
Shortcut* shortcut,
std::string* error) {
if (!shortcut) return false;
Shortcut result;
std::string value = Trim(text);
if (value.empty() || ToLower(value) == "none" ||
ToLower(value) == "unbound") {
*shortcut = result;
return true;
}
std::stringstream stream(value);
std::string token;
bool has_key = false;
while (std::getline(stream, token, '+')) {
std::string part = Trim(token);
if (part.empty()) continue;
std::string lower = ToLower(part);
if (lower == "ctrl" || lower == "control") {
result.ctrl = true;
continue;
}
if (lower == "shift") {
result.shift = true;
continue;
}
if (lower == "alt" || lower == "option") {
result.alt = true;
continue;
}
if (lower == "super" || lower == "cmd" || lower == "command" ||
lower == "meta") {
result.super = true;
continue;
}
if (has_key) {
if (error) *error = "Multiple keys specified";
return false;
}
ImGuiKey key_value = FindKeyByName(part);
if (key_value == ImGuiKey_None) {
if (error) *error = "Unknown key";
return false;
}
result.key = key_value;
has_key = true;
}
if (!has_key) {
if (error) *error = "Missing key";
return false;
}
*shortcut = result;
return true;
}
std::string ShortcutManager::ResolveDefaultPath() {
const char* override_path = std::getenv("AFS_VIZ_SHORTCUTS_PATH");
if (override_path && override_path[0] != '\0') {
return override_path;
}
const char* xdg = std::getenv("XDG_CONFIG_HOME");
const char* home = std::getenv("HOME");
std::filesystem::path base;
if (xdg && xdg[0] != '\0') {
base = xdg;
} else if (home && home[0] != '\0') {
base = std::filesystem::path(home) / ".config";
} else {
base = std::filesystem::current_path();
}
return (base / "afs" / "viz_shortcuts.conf").string();
}
bool ShortcutManager::IsValidShortcutKey(ImGuiKey key) {
// Exclude modifier keys from being bound as main shortcut keys
switch (key) {
case ImGuiKey_None:
case ImGuiKey_LeftCtrl:
case ImGuiKey_RightCtrl:
case ImGuiKey_LeftShift:
case ImGuiKey_RightShift:
case ImGuiKey_LeftAlt:
case ImGuiKey_RightAlt:
case ImGuiKey_LeftSuper:
case ImGuiKey_RightSuper:
return false;
default:
return true;
}
}
void RenderShortcutsWindow(ShortcutManager& shortcuts, bool* open) {
if (!open || !*open) return;
if (!ImGui::Begin("Keyboard Shortcuts", open)) {
ImGui::End();
return;
}
ImGui::Text("Customize keyboard shortcuts.");
ImGui::TextDisabled("Click Edit, then press the desired key combo.");
if (ImGui::Button("Save")) {
shortcuts.SaveToDisk();
}
ImGui::SameLine();
if (ImGui::Button("Reload")) {
shortcuts.LoadFromDisk();
}
ImGui::SameLine();
if (ImGui::Button("Reset All")) {
shortcuts.ResetAll();
}
if (shortcuts.IsCapturing()) {
ImGui::SameLine();
ImGui::TextDisabled("Press keys... (Esc to cancel)");
}
ImGui::TextDisabled("Config: %s", shortcuts.GetConfigPath().c_str());
if (!shortcuts.GetLastError().empty()) {
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f),
"Shortcut error: %s", shortcuts.GetLastError().c_str());
}
static std::array<char, 64> filter{};
ImGui::InputTextWithHint("##ShortcutFilter", "Filter actions",
filter.data(), filter.size());
if (ImGui::BeginTable("ShortcutTable", 4,
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Shortcut", ImGuiTableColumnFlags_WidthFixed, 150.0f);
ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Edit", ImGuiTableColumnFlags_WidthFixed, 140.0f);
ImGui::TableHeadersRow();
const std::string filter_text(filter.data());
const ImGuiIO& io = ImGui::GetIO();
for (const auto& action : shortcuts.Actions()) {
if (!ContainsInsensitive(action.label, filter_text) &&
!ContainsInsensitive(action.description, filter_text)) {
continue;
}
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("%s", action.label);
ImGui::TableSetColumnIndex(1);
std::string label = shortcuts.FormatShortcut(action.id, io);
ImGui::TextDisabled("%s", label.empty() ? "Unbound" : label.c_str());
ImGui::TableSetColumnIndex(2);
ImGui::TextWrapped("%s", action.description);
ImGui::TableSetColumnIndex(3);
ImGui::PushID(static_cast<int>(action.id));
bool is_capturing = shortcuts.IsCapturing() &&
shortcuts.CapturingAction() == action.id;
if (is_capturing) {
if (ImGui::Button("Cancel")) {
shortcuts.CancelCapture();
}
} else if (ImGui::Button("Edit")) {
shortcuts.BeginCapture(action.id);
}
ImGui::SameLine();
if (ImGui::Button("Clear")) {
shortcuts.SetShortcut(action.id, Shortcut{});
}
ImGui::SameLine();
if (ImGui::Button("Reset")) {
shortcuts.ResetShortcut(action.id);
}
ImGui::PopID();
}
ImGui::EndTable();
}
ImGui::End();
}
} // namespace ui
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,118 @@
#pragma once
#include <array>
#include <string>
#include <imgui.h>
namespace afs {
namespace viz {
namespace ui {
enum class ActionId {
Refresh,
Quit,
ToggleInspector,
ToggleDatasetPanel,
ToggleSystemsPanel,
ToggleStatusBar,
ToggleControls,
ToggleAutoRefresh,
ToggleSimulation,
ToggleCompactUI,
ToggleLockLayout,
ResetLayout,
ToggleSampleReview,
ToggleShortcutsWindow,
ToggleDemoWindow,
WorkspaceDashboard,
WorkspaceAnalysis,
WorkspaceOptimization,
WorkspaceSystems,
WorkspaceCustom,
WorkspaceChat,
WorkspaceTraining,
WorkspaceContext,
// Graph View Navigation
ToggleGraphBrowser,
ToggleCompanionPanels,
NavigateBack,
NavigateForward,
BookmarkGraph,
OpenGraphPalette,
Count,
};
struct Shortcut {
ImGuiKey key = ImGuiKey_None;
bool ctrl = false;
bool shift = false;
bool alt = false;
bool super = false;
};
struct ActionDefinition {
ActionId id;
const char* config_key;
const char* label;
const char* description;
Shortcut default_shortcut;
};
constexpr int kActionCount = static_cast<int>(ActionId::Count);
class ShortcutManager {
public:
ShortcutManager();
const std::array<ActionDefinition, kActionCount>& Actions() const;
const ActionDefinition& GetDefinition(ActionId id) const;
const Shortcut& GetShortcut(ActionId id) const;
const std::string& GetConfigPath() const { return config_path_; }
const std::string& GetLastError() const { return last_error_; }
void SetShortcut(ActionId id, const Shortcut& shortcut);
void ResetShortcut(ActionId id);
void ResetAll();
void SetConfigPath(std::string path);
bool LoadFromDisk(std::string* error = nullptr);
bool SaveToDisk(std::string* error = nullptr);
bool SaveIfDirty(std::string* error = nullptr);
bool IsCapturing() const { return capturing_; }
ActionId CapturingAction() const { return capturing_action_; }
void BeginCapture(ActionId id);
void CancelCapture();
bool HandleCapture(const ImGuiIO& io);
bool ShouldProcessShortcuts(const ImGuiIO& io) const;
bool IsTriggered(ActionId id, const ImGuiIO& io) const;
std::string FormatShortcut(ActionId id, const ImGuiIO& io) const;
static std::string FormatShortcut(const Shortcut& shortcut, const ImGuiIO& io);
private:
static size_t ToIndex(ActionId id);
static bool IsValidShortcutKey(ImGuiKey key);
const ActionDefinition* FindDefinition(const std::string& key) const;
static std::string Trim(const std::string& text);
static std::string ToLower(const std::string& text);
static ImGuiKey FindKeyByName(const std::string& name);
static bool ParseShortcutString(const std::string& text,
Shortcut* shortcut,
std::string* error);
static std::string ResolveDefaultPath();
std::array<Shortcut, kActionCount> bindings_{};
ActionId capturing_action_ = ActionId::Count;
bool capturing_ = false;
bool dirty_ = false;
std::string config_path_;
std::string last_error_;
};
void RenderShortcutsWindow(ShortcutManager& shortcuts, bool* open);
} // namespace ui
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,861 @@
#pragma once
#include <stdint.h> // uint8_t, etc.
#include <stdio.h> // sprintf, scanf
#include "icons.h"
#ifdef _MSC_VER
#define _PRISizeT "I"
#define ImSnprintf _snprintf
#else
#define _PRISizeT "z"
#define ImSnprintf snprintf
#endif
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4996) // warning C4996: 'sprintf': This function or \
// variable may be unsafe.
#endif
namespace afs {
namespace viz {
struct MemoryEditorWidget {
enum DataFormat {
DataFormat_Bin = 0,
DataFormat_Dec = 1,
DataFormat_Hex = 2,
DataFormat_COUNT
};
// Settings
bool Open; // = true // set to false when DrawWindow() was closed. ignore
// if not using DrawWindow().
bool ReadOnly; // = false // disable any editing.
int Cols; // = 16 // number of columns to display.
bool OptShowOptions; // = true // display options button/context menu. when
// disabled, options will be locked unless you provide
// your own UI for them.
bool OptShowDataPreview; // = false // display a footer previewing the
// decimal/binary/hex/float representation of the
// currently selected bytes.
bool OptShowHexII; // = false // display values in HexII representation
// instead of regular hexadecimal: hide null/zero bytes,
// ascii values as ".X".
bool OptShowAscii; // = true // display ASCII representation on the right
// side.
bool OptGreyOutZeroes; // = true // display null/zero bytes using the
// TextDisabled color.
bool OptUpperCaseHex; // = true // display hexadecimal values as "FF"
// instead of "ff".
int OptMidColsCount; // = 8 // set to 0 to disable extra spacing between
// every mid-cols.
int OptAddrDigitsCount; // = 0 // number of addr digits to display
// (default calculated based on maximum displayed
// addr).
float OptFooterExtraHeight; // = 0 // space to reserve at the bottom of
// the widget to add custom widgets
ImU32 HighlightColor; // // background color of highlighted bytes.
ImU8 (*ReadFn)(const ImU8* data,
size_t off); // = 0 // optional handler to read bytes.
void (*WriteFn)(ImU8* data, size_t off,
ImU8 d); // = 0 // optional handler to write bytes.
bool (*HighlightFn)(
const ImU8* data,
size_t off); //= 0 // optional handler to return Highlight property
//(to support non-contiguous highlighting).
// [Internal State]
bool ContentsWidthChanged;
size_t DataPreviewAddr;
size_t DataEditingAddr;
bool DataEditingTakeFocus;
char DataInputBuf[32];
char AddrInputBuf[32];
size_t GotoAddr;
size_t HighlightMin, HighlightMax;
int PreviewEndianess;
ImGuiDataType PreviewDataType;
// Expanded
ImU8* ComparisonData;
void SetComparisonData(void* data) { ComparisonData = (ImU8*)data; }
MemoryEditorWidget() {
// Settings
Open = true;
ReadOnly = false;
Cols = 16;
OptShowOptions = true;
OptShowDataPreview = false;
OptShowHexII = false;
OptShowAscii = true;
OptGreyOutZeroes = true;
OptUpperCaseHex = true;
OptMidColsCount = 8;
OptAddrDigitsCount = 0;
OptFooterExtraHeight = 0.0f;
HighlightColor = IM_COL32(255, 255, 255, 50);
ReadFn = nullptr;
WriteFn = nullptr;
HighlightFn = nullptr;
// State/Internals
ContentsWidthChanged = false;
DataPreviewAddr = DataEditingAddr = (size_t)-1;
DataEditingTakeFocus = false;
memset(DataInputBuf, 0, sizeof(DataInputBuf));
memset(AddrInputBuf, 0, sizeof(AddrInputBuf));
GotoAddr = (size_t)-1;
HighlightMin = HighlightMax = (size_t)-1;
PreviewEndianess = 0;
PreviewDataType = ImGuiDataType_S32;
}
void GotoAddrAndHighlight(size_t addr_min, size_t addr_max) {
GotoAddr = addr_min;
HighlightMin = addr_min;
HighlightMax = addr_max;
}
struct Sizes {
int AddrDigitsCount;
float LineHeight;
float GlyphWidth;
float HexCellWidth;
float SpacingBetweenMidCols;
float PosHexStart;
float PosHexEnd;
float PosAsciiStart;
float PosAsciiEnd;
float WindowWidth;
Sizes() { memset(this, 0, sizeof(*this)); }
};
void CalcSizes(Sizes& s, size_t mem_size, size_t base_display_addr) {
ImGuiStyle& style = ImGui::GetStyle();
s.AddrDigitsCount = OptAddrDigitsCount;
if (s.AddrDigitsCount == 0)
for (size_t n = base_display_addr + mem_size - 1; n > 0; n >>= 4)
s.AddrDigitsCount++;
s.LineHeight = ImGui::GetTextLineHeight();
s.GlyphWidth =
ImGui::CalcTextSize("F").x + 1; // We assume the font is mono-space
s.HexCellWidth =
(float)(int)(s.GlyphWidth *
2.5f); // "FF " we include trailing space in the width to
// easily catch clicks everywhere
s.SpacingBetweenMidCols =
(float)(int)(s.HexCellWidth * 0.25f); // Every OptMidColsCount columns
// we add a bit of extra spacing
s.PosHexStart = (s.AddrDigitsCount + 2) * s.GlyphWidth;
s.PosHexEnd = s.PosHexStart + (s.HexCellWidth * Cols);
s.PosAsciiStart = s.PosAsciiEnd = s.PosHexEnd;
if (OptShowAscii) {
s.PosAsciiStart = s.PosHexEnd + s.GlyphWidth * 1;
if (OptMidColsCount > 0)
s.PosAsciiStart +=
(float)((Cols + OptMidColsCount - 1) / OptMidColsCount) *
s.SpacingBetweenMidCols;
s.PosAsciiEnd = s.PosAsciiStart + Cols * s.GlyphWidth;
}
s.WindowWidth = s.PosAsciiEnd + style.ScrollbarSize +
style.WindowPadding.x * 2 + s.GlyphWidth;
}
// Standalone Memory Editor window
void DrawWindow(const char* title, void* mem_data, size_t mem_size,
size_t base_display_addr = 0x0000) {
Sizes s;
CalcSizes(s, mem_size, base_display_addr);
ImGui::SetNextWindowSize(ImVec2(s.WindowWidth, s.WindowWidth * 0.60f),
ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, 0.0f),
ImVec2(s.WindowWidth, FLT_MAX));
Open = true;
if (ImGui::Begin(title, &Open, ImGuiWindowFlags_NoScrollbar)) {
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows) &&
ImGui::IsMouseReleased(ImGuiMouseButton_Right))
ImGui::OpenPopup("context");
DrawContents(mem_data, mem_size, base_display_addr);
if (ContentsWidthChanged) {
CalcSizes(s, mem_size, base_display_addr);
ImGui::SetWindowSize(ImVec2(s.WindowWidth, ImGui::GetWindowSize().y));
}
}
ImGui::End();
}
// Memory Editor contents only
void DrawContents(void* mem_data_void, size_t mem_size,
size_t base_display_addr = 0x0000) {
if (Cols < 1)
Cols = 1;
ImU8* mem_data = (ImU8*)mem_data_void;
Sizes s;
CalcSizes(s, mem_size, base_display_addr);
ImGuiStyle& style = ImGui::GetStyle();
// We begin into our scrolling region with the 'ImGuiWindowFlags_NoMove' in
// order to prevent click from moving the window. This is used as a facility
// since our main click detection code doesn't assign an ActiveId so the
// click would normally be caught as a window-move.
const float height_separator = style.ItemSpacing.y;
float footer_height = OptFooterExtraHeight;
if (OptShowOptions)
footer_height +=
height_separator + ImGui::GetFrameHeightWithSpacing() * 1;
if (OptShowDataPreview)
footer_height += height_separator +
ImGui::GetFrameHeightWithSpacing() * 1 +
ImGui::GetTextLineHeightWithSpacing() * 3;
ImGui::BeginChild("##scrolling", ImVec2(0, -footer_height), false,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
// We are not really using the clipper API correctly here, because we rely
// on visible_start_addr/visible_end_addr for our scrolling function.
const int line_total_count = (int)((mem_size + Cols - 1) / Cols);
ImGuiListClipper clipper;
clipper.Begin(line_total_count, s.LineHeight);
bool data_next = false;
if (ReadOnly || DataEditingAddr >= mem_size)
DataEditingAddr = (size_t)-1;
if (DataPreviewAddr >= mem_size)
DataPreviewAddr = (size_t)-1;
size_t preview_data_type_size =
OptShowDataPreview ? DataTypeGetSize(PreviewDataType) : 0;
size_t data_editing_addr_next = (size_t)-1;
if (DataEditingAddr != (size_t)-1) {
// Move cursor but only apply on next frame so scrolling with be
// synchronized (because currently we can't change the scrolling while the
// window is being rendered)
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) &&
(ptrdiff_t)DataEditingAddr >= (ptrdiff_t)Cols) {
data_editing_addr_next = DataEditingAddr - Cols;
} else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) &&
(ptrdiff_t)DataEditingAddr < (ptrdiff_t)mem_size - Cols) {
data_editing_addr_next = DataEditingAddr + Cols;
} else if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) &&
(ptrdiff_t)DataEditingAddr > (ptrdiff_t)0) {
data_editing_addr_next = DataEditingAddr - 1;
} else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) &&
(ptrdiff_t)DataEditingAddr < (ptrdiff_t)mem_size - 1) {
data_editing_addr_next = DataEditingAddr + 1;
}
}
// Draw vertical separator
ImVec2 window_pos = ImGui::GetWindowPos();
if (OptShowAscii)
draw_list->AddLine(
ImVec2(window_pos.x + s.PosAsciiStart - s.GlyphWidth, window_pos.y),
ImVec2(window_pos.x + s.PosAsciiStart - s.GlyphWidth,
window_pos.y + 9999),
ImGui::GetColorU32(ImGuiCol_Border));
const ImU32 color_text = ImGui::GetColorU32(ImGuiCol_Text);
const ImU32 color_disabled = OptGreyOutZeroes
? ImGui::GetColorU32(ImGuiCol_TextDisabled)
: color_text;
const char* format_address =
OptUpperCaseHex ? "%0*" _PRISizeT "X: " : "%0*" _PRISizeT "x: ";
const char* format_data =
OptUpperCaseHex ? "%0*" _PRISizeT "X" : "%0*" _PRISizeT "x";
const char* format_byte = OptUpperCaseHex ? "%02X" : "%02x";
const char* format_byte_space = OptUpperCaseHex ? "%02X " : "%02x ";
while (clipper.Step())
for (int line_i = clipper.DisplayStart; line_i < clipper.DisplayEnd;
line_i++) // display only visible lines
{
size_t addr = (size_t)(line_i * Cols);
ImGui::Text(format_address, s.AddrDigitsCount,
base_display_addr + addr);
// Draw Hexadecimal
for (int n = 0; n < Cols && addr < mem_size; n++, addr++) {
float byte_pos_x = s.PosHexStart + s.HexCellWidth * n;
if (OptMidColsCount > 0)
byte_pos_x +=
(float)(n / OptMidColsCount) * s.SpacingBetweenMidCols;
ImGui::SameLine(byte_pos_x);
// Draw highlight
bool is_highlight_from_user_range =
(addr >= HighlightMin && addr < HighlightMax);
bool is_highlight_from_user_func =
(HighlightFn && HighlightFn(mem_data, addr));
bool is_highlight_from_preview =
(addr >= DataPreviewAddr &&
addr < DataPreviewAddr + preview_data_type_size);
if (is_highlight_from_user_range || is_highlight_from_user_func ||
is_highlight_from_preview) {
ImVec2 pos = ImGui::GetCursorScreenPos();
float highlight_width = s.GlyphWidth * 2;
bool is_next_byte_highlighted =
(addr + 1 < mem_size) &&
((HighlightMax != (size_t)-1 && addr + 1 < HighlightMax) ||
(HighlightFn && HighlightFn(mem_data, addr + 1)));
if (is_next_byte_highlighted || (n + 1 == Cols)) {
highlight_width = s.HexCellWidth;
if (OptMidColsCount > 0 && n > 0 && (n + 1) < Cols &&
((n + 1) % OptMidColsCount) == 0)
highlight_width += s.SpacingBetweenMidCols;
}
draw_list->AddRectFilled(
pos, ImVec2(pos.x + highlight_width, pos.y + s.LineHeight),
HighlightColor);
}
bool comparison_rom_diff = false;
if (ComparisonData != nullptr) {
int a = mem_data[addr];
int b = ComparisonData[addr];
if (a != b) {
ImVec2 pos = ImGui::GetCursorScreenPos();
float highlight_width = s.GlyphWidth * 2;
bool is_next_byte_highlighted =
(addr + 1 < mem_size) &&
((HighlightMax != (size_t)-1 && addr + 1 < HighlightMax) ||
(HighlightFn && HighlightFn(mem_data, addr + 1)));
if (is_next_byte_highlighted || (n + 1 == Cols)) {
highlight_width = s.HexCellWidth;
if (OptMidColsCount > 0 && n > 0 && (n + 1) < Cols &&
((n + 1) % OptMidColsCount) == 0)
highlight_width += s.SpacingBetweenMidCols;
}
draw_list->AddRectFilled(
pos, ImVec2(pos.x + highlight_width, pos.y + s.LineHeight),
IM_COL32(255, 0, 0, 50));
}
}
if (DataEditingAddr == addr) {
// Display text input on current byte
bool data_write = false;
ImGui::PushID((void*)addr);
if (DataEditingTakeFocus) {
ImGui::SetKeyboardFocusHere(0);
snprintf(AddrInputBuf, sizeof(AddrInputBuf), format_data, s.AddrDigitsCount,
base_display_addr + addr);
snprintf(DataInputBuf, sizeof(DataInputBuf), format_byte,
ReadFn ? ReadFn(mem_data, addr) : mem_data[addr]);
}
struct UserData {
// FIXME: We should have a way to retrieve the text edit cursor
// position more easily in the API, this is rather tedious. This
// is such a ugly mess we may be better off not using InputText()
// at all here.
static int Callback(ImGuiInputTextCallbackData* data) {
UserData* user_data = (UserData*)data->UserData;
if (!data->HasSelection())
user_data->CursorPos = data->CursorPos;
if (data->SelectionStart == 0 &&
data->SelectionEnd == data->BufTextLen) {
// When not editing a byte, always refresh its InputText
// content pulled from underlying memory data (this is a bit
// tricky, since InputText technically "owns" the master copy
// of the buffer we edit it in there)
data->DeleteChars(0, data->BufTextLen);
data->InsertChars(0, user_data->CurrentBufOverwrite);
data->SelectionStart = 0;
data->SelectionEnd = 2;
data->CursorPos = 0;
}
return 0;
}
char CurrentBufOverwrite[3]; // Input
int CursorPos; // Output
};
UserData user_data;
user_data.CursorPos = -1;
snprintf(user_data.CurrentBufOverwrite, sizeof(user_data.CurrentBufOverwrite), format_byte,
ReadFn ? ReadFn(mem_data, addr) : mem_data[addr]);
ImGuiInputTextFlags flags = ImGuiInputTextFlags_CharsHexadecimal |
ImGuiInputTextFlags_EnterReturnsTrue |
ImGuiInputTextFlags_AutoSelectAll |
ImGuiInputTextFlags_NoHorizontalScroll |
ImGuiInputTextFlags_CallbackAlways;
#if IMGUI_VERSION_NUM >= 18104
flags |= ImGuiInputTextFlags_AlwaysOverwrite;
#else
flags |= ImGuiInputTextFlags_AlwaysInsertMode;
#endif
ImGui::SetNextItemWidth(s.GlyphWidth * 2);
if (ImGui::InputText("##data", DataInputBuf,
IM_ARRAYSIZE(DataInputBuf), flags,
UserData::Callback, &user_data))
data_write = data_next = true;
else if (!DataEditingTakeFocus && !ImGui::IsItemActive())
DataEditingAddr = data_editing_addr_next = (size_t)-1;
DataEditingTakeFocus = false;
if (user_data.CursorPos >= 2)
data_write = data_next = true;
if (data_editing_addr_next != (size_t)-1)
data_write = data_next = false;
unsigned int data_input_value = 0;
if (data_write &&
sscanf(DataInputBuf, "%X", &data_input_value) == 1) {
if (WriteFn)
WriteFn(mem_data, addr, (ImU8)data_input_value);
else
mem_data[addr] = (ImU8)data_input_value;
}
ImGui::PopID();
} else {
// NB: The trailing space is not visible but ensure there's no gap
// that the mouse cannot click on.
ImU8 b = ReadFn ? ReadFn(mem_data, addr) : mem_data[addr];
if (OptShowHexII) {
if ((b >= 32 && b < 128))
ImGui::Text(".%c ", b);
else if (b == 0xFF && OptGreyOutZeroes)
ImGui::TextDisabled("## ");
else if (b == 0x00)
ImGui::Text(" ");
else
ImGui::Text(format_byte_space, b);
} else {
if (b == 0 && OptGreyOutZeroes)
ImGui::TextDisabled("00 ");
else
ImGui::Text(format_byte_space, b);
}
if (!ReadOnly && ImGui::IsItemHovered() &&
ImGui::IsMouseClicked(0)) {
DataEditingTakeFocus = true;
data_editing_addr_next = addr;
}
}
}
if (OptShowAscii) {
// Draw ASCII values
ImGui::SameLine(s.PosAsciiStart);
ImVec2 pos = ImGui::GetCursorScreenPos();
addr = line_i * Cols;
ImGui::PushID(line_i);
if (ImGui::InvisibleButton(
"ascii",
ImVec2(s.PosAsciiEnd - s.PosAsciiStart, s.LineHeight))) {
DataEditingAddr = DataPreviewAddr =
addr +
(size_t)((ImGui::GetIO().MousePos.x - pos.x) / s.GlyphWidth);
DataEditingTakeFocus = true;
}
ImGui::PopID();
for (int n = 0; n < Cols && addr < mem_size; n++, addr++) {
if (addr == DataEditingAddr) {
draw_list->AddRectFilled(
pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight),
ImGui::GetColorU32(ImGuiCol_FrameBg));
draw_list->AddRectFilled(
pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight),
ImGui::GetColorU32(ImGuiCol_TextSelectedBg));
}
unsigned char c = ReadFn ? ReadFn(mem_data, addr) : mem_data[addr];
char display_c = (c < 32 || c >= 128) ? '.' : c;
draw_list->AddText(pos,
(display_c == c) ? color_text : color_disabled,
&display_c, &display_c + 1);
pos.x += s.GlyphWidth;
}
}
}
ImGui::PopStyleVar(2);
ImGui::EndChild();
// Notify the main window of our ideal child content size (FIXME: we are
// missing an API to get the contents size from the child)
ImGui::SetCursorPosX(s.WindowWidth);
if (data_next && DataEditingAddr + 1 < mem_size) {
DataEditingAddr = DataPreviewAddr = DataEditingAddr + 1;
DataEditingTakeFocus = true;
} else if (data_editing_addr_next != (size_t)-1) {
DataEditingAddr = DataPreviewAddr = data_editing_addr_next;
DataEditingTakeFocus = true;
}
const bool lock_show_data_preview = OptShowDataPreview;
if (OptShowOptions) {
ImGui::Separator();
DrawOptionsLine(s, mem_data, mem_size, base_display_addr);
}
if (lock_show_data_preview) {
ImGui::Separator();
DrawPreviewLine(s, mem_data, mem_size, base_display_addr);
}
}
void DrawOptionsLine(const Sizes& s, void* mem_data, size_t mem_size,
size_t base_display_addr) {
IM_UNUSED(mem_data);
ImGuiStyle& style = ImGui::GetStyle();
const char* format_range =
OptUpperCaseHex ? "Range %0*" _PRISizeT "X..%0*" _PRISizeT "X"
: "Range %0*" _PRISizeT "x..%0*" _PRISizeT "x";
// Options menu
if (ImGui::Button(ICON_MD_SETTINGS " Options"))
ImGui::OpenPopup("context");
if (ImGui::BeginPopup("context")) {
ImGui::SetNextItemWidth(s.GlyphWidth * 7 + style.FramePadding.x * 2.0f);
if (ImGui::DragInt(ICON_MD_VIEW_COLUMN " ##cols", &Cols, 0.2f, 4, 32, "%d cols")) {
ContentsWidthChanged = true;
if (Cols < 1)
Cols = 1;
}
ImGui::Checkbox(ICON_MD_PREVIEW " Show Data Preview", &OptShowDataPreview);
ImGui::Checkbox(ICON_MD_HEXAGON " Show HexII", &OptShowHexII);
if (ImGui::Checkbox(ICON_MD_ABC " Show Ascii", &OptShowAscii)) {
ContentsWidthChanged = true;
}
ImGui::Checkbox(ICON_MD_FORMAT_COLOR_RESET " Grey out zeroes", &OptGreyOutZeroes);
ImGui::Checkbox(ICON_MD_KEYBOARD_CAPSLOCK " Uppercase Hex", &OptUpperCaseHex);
ImGui::EndPopup();
}
ImGui::SameLine();
ImGui::Text(ICON_MD_ZOOM_OUT_MAP);
ImGui::SameLine();
ImGui::Text(format_range, s.AddrDigitsCount, base_display_addr,
s.AddrDigitsCount, base_display_addr + mem_size - 1);
ImGui::SameLine();
ImGui::SetNextItemWidth((s.AddrDigitsCount + 1) * s.GlyphWidth +
style.FramePadding.x * 2.0f);
if (ImGui::InputText("##addr", AddrInputBuf, IM_ARRAYSIZE(AddrInputBuf),
ImGuiInputTextFlags_CharsHexadecimal |
ImGuiInputTextFlags_EnterReturnsTrue)) {
size_t goto_addr;
if (sscanf(AddrInputBuf, "%" _PRISizeT "X", &goto_addr) == 1) {
GotoAddr = goto_addr - base_display_addr;
HighlightMin = HighlightMax = (size_t)-1;
}
}
ImGui::SameLine();
ImGui::Text(ICON_MD_LOCATION_ON);
if (GotoAddr != (size_t)-1) {
if (GotoAddr < mem_size) {
ImGui::BeginChild("##scrolling");
ImGui::SetScrollFromPosY(ImGui::GetCursorStartPos().y +
(GotoAddr / Cols) *
ImGui::GetTextLineHeight());
ImGui::EndChild();
DataEditingAddr = DataPreviewAddr = GotoAddr;
DataEditingTakeFocus = true;
}
GotoAddr = (size_t)-1;
}
}
void DrawPreviewLine(const Sizes& s, void* mem_data_void, size_t mem_size,
size_t base_display_addr) {
IM_UNUSED(base_display_addr);
ImU8* mem_data = (ImU8*)mem_data_void;
ImGuiStyle& style = ImGui::GetStyle();
ImGui::AlignTextToFramePadding();
ImGui::Text(ICON_MD_SEARCH " Preview as:");
ImGui::SameLine();
ImGui::SetNextItemWidth((s.GlyphWidth * 10.0f) +
style.FramePadding.x * 2.0f +
style.ItemInnerSpacing.x);
if (ImGui::BeginCombo("##combo_type", DataTypeGetDesc(PreviewDataType),
ImGuiComboFlags_HeightLargest)) {
for (int n = 0; n < ImGuiDataType_COUNT; n++)
if (ImGui::Selectable(DataTypeGetDesc((ImGuiDataType)n),
PreviewDataType == n))
PreviewDataType = (ImGuiDataType)n;
ImGui::EndCombo();
}
ImGui::SameLine();
ImGui::SetNextItemWidth((s.GlyphWidth * 6.0f) +
style.FramePadding.x * 2.0f +
style.ItemInnerSpacing.x);
ImGui::Combo("##combo_endianess", &PreviewEndianess, "LE\0BE\0\0");
char buf[128] = "";
float x = s.GlyphWidth * 6.0f;
bool has_value = DataPreviewAddr != (size_t)-1;
if (has_value)
DrawPreviewData(DataPreviewAddr, mem_data, mem_size, PreviewDataType,
DataFormat_Dec, buf, (size_t)IM_ARRAYSIZE(buf));
ImGui::Text(ICON_MD_NUMBERS " Dec");
ImGui::SameLine(x);
ImGui::TextUnformatted(has_value ? buf : "N/A");
if (has_value)
DrawPreviewData(DataPreviewAddr, mem_data, mem_size, PreviewDataType,
DataFormat_Hex, buf, (size_t)IM_ARRAYSIZE(buf));
ImGui::Text(ICON_MD_HEXAGON " Hex");
ImGui::SameLine(x);
ImGui::TextUnformatted(has_value ? buf : "N/A");
if (has_value)
DrawPreviewData(DataPreviewAddr, mem_data, mem_size, PreviewDataType,
DataFormat_Bin, buf, (size_t)IM_ARRAYSIZE(buf));
buf[IM_ARRAYSIZE(buf) - 1] = 0;
ImGui::Text(ICON_MD_DATA_ARRAY " Bin");
ImGui::SameLine(x);
ImGui::TextUnformatted(has_value ? buf : "N/A");
}
// Utilities for Data Preview
const char* DataTypeGetDesc(ImGuiDataType data_type) const {
const char* descs[] = {"Int8", "Uint8", "Int16", "Uint16", "Int32",
"Uint32", "Int64", "Uint64", "Float", "Double"};
IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
return descs[data_type];
}
size_t DataTypeGetSize(ImGuiDataType data_type) const {
const size_t sizes[] = {
1, 1, 2, 2, 4, 4, 8, 8, sizeof(float), sizeof(double)};
IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
return sizes[data_type];
}
const char* DataFormatGetDesc(DataFormat data_format) const {
const char* descs[] = {"Bin", "Dec", "Hex"};
IM_ASSERT(data_format >= 0 && data_format < DataFormat_COUNT);
return descs[data_format];
}
bool IsBigEndian() const {
uint16_t x = 1;
char c[2];
memcpy(c, &x, 2);
return c[0] != 0;
}
static void* EndianessCopyBigEndian(void* _dst, void* _src, size_t s,
int is_little_endian) {
if (is_little_endian) {
uint8_t* dst = (uint8_t*)_dst;
uint8_t* src = (uint8_t*)_src + s - 1;
for (int i = 0, n = (int)s; i < n; ++i)
memcpy(dst++, src--, 1);
return _dst;
} else {
return memcpy(_dst, _src, s);
}
}
static void* EndianessCopyLittleEndian(void* _dst, void* _src, size_t s,
int is_little_endian) {
if (is_little_endian) {
return memcpy(_dst, _src, s);
} else {
uint8_t* dst = (uint8_t*)_dst;
uint8_t* src = (uint8_t*)_src + s - 1;
for (int i = 0, n = (int)s; i < n; ++i)
memcpy(dst++, src--, 1);
return _dst;
}
}
void* EndianessCopy(void* dst, void* src, size_t size) const {
static void* (*fp)(void*, void*, size_t, int) = nullptr;
if (fp == nullptr)
fp = IsBigEndian() ? EndianessCopyBigEndian : EndianessCopyLittleEndian;
return fp(dst, src, size, PreviewEndianess);
}
const char* FormatBinary(const uint8_t* buf, int width) const {
IM_ASSERT(width <= 64);
size_t out_n = 0;
static char out_buf[64 + 8 + 1];
int n = width / 8;
for (int j = n - 1; j >= 0; --j) {
for (int i = 0; i < 8; ++i)
out_buf[out_n++] = (buf[j] & (1 << (7 - i))) ? '1' : '0';
out_buf[out_n++] = ' ';
}
IM_ASSERT(out_n < IM_ARRAYSIZE(out_buf));
out_buf[out_n] = 0;
return out_buf;
}
// [Internal]
void DrawPreviewData(size_t addr, const ImU8* mem_data, size_t mem_size,
ImGuiDataType data_type, DataFormat data_format,
char* out_buf, size_t out_buf_size) const {
uint8_t buf[8];
size_t elem_size = DataTypeGetSize(data_type);
size_t size = addr + elem_size > mem_size ? mem_size - addr : elem_size;
if (ReadFn)
for (int i = 0, n = (int)size; i < n; ++i)
buf[i] = ReadFn(mem_data, addr + i);
else
memcpy(buf, mem_data + addr, size);
if (data_format == DataFormat_Bin) {
uint8_t binbuf[8];
EndianessCopy(binbuf, buf, size);
ImSnprintf(out_buf, out_buf_size, "%s",
FormatBinary(binbuf, (int)size * 8));
return;
}
out_buf[0] = 0;
switch (data_type) {
case ImGuiDataType_S8: {
int8_t int8 = 0;
EndianessCopy(&int8, buf, size);
if (data_format == DataFormat_Dec) {
ImSnprintf(out_buf, out_buf_size, "%hhd", int8);
return;
}
if (data_format == DataFormat_Hex) {
ImSnprintf(out_buf, out_buf_size, "0x%02x", int8 & 0xFF);
return;
}
break;
}
case ImGuiDataType_U8: {
uint8_t uint8 = 0;
EndianessCopy(&uint8, buf, size);
if (data_format == DataFormat_Dec) {
ImSnprintf(out_buf, out_buf_size, "%hhu", uint8);
return;
}
if (data_format == DataFormat_Hex) {
ImSnprintf(out_buf, out_buf_size, "0x%02x", uint8 & 0xFF);
return;
}
break;
}
case ImGuiDataType_S16: {
int16_t int16 = 0;
EndianessCopy(&int16, buf, size);
if (data_format == DataFormat_Dec) {
ImSnprintf(out_buf, out_buf_size, "%hd", int16);
return;
}
if (data_format == DataFormat_Hex) {
ImSnprintf(out_buf, out_buf_size, "0x%04x", int16 & 0xFFFF);
return;
}
break;
}
case ImGuiDataType_U16: {
uint16_t uint16 = 0;
EndianessCopy(&uint16, buf, size);
if (data_format == DataFormat_Dec) {
ImSnprintf(out_buf, out_buf_size, "%hu", uint16);
return;
}
if (data_format == DataFormat_Hex) {
ImSnprintf(out_buf, out_buf_size, "0x%04x", uint16 & 0xFFFF);
return;
}
break;
}
case ImGuiDataType_S32: {
int32_t int32 = 0;
EndianessCopy(&int32, buf, size);
if (data_format == DataFormat_Dec) {
ImSnprintf(out_buf, out_buf_size, "%d", int32);
return;
}
if (data_format == DataFormat_Hex) {
ImSnprintf(out_buf, out_buf_size, "0x%08x", int32);
return;
}
break;
}
case ImGuiDataType_U32: {
uint32_t uint32 = 0;
EndianessCopy(&uint32, buf, size);
if (data_format == DataFormat_Dec) {
ImSnprintf(out_buf, out_buf_size, "%u", uint32);
return;
}
if (data_format == DataFormat_Hex) {
ImSnprintf(out_buf, out_buf_size, "0x%08x", uint32);
return;
}
break;
}
case ImGuiDataType_S64: {
int64_t int64 = 0;
EndianessCopy(&int64, buf, size);
if (data_format == DataFormat_Dec) {
ImSnprintf(out_buf, out_buf_size, "%lld", (long long)int64);
return;
}
if (data_format == DataFormat_Hex) {
ImSnprintf(out_buf, out_buf_size, "0x%016llx", (long long)int64);
return;
}
break;
}
case ImGuiDataType_U64: {
uint64_t uint64 = 0;
EndianessCopy(&uint64, buf, size);
if (data_format == DataFormat_Dec) {
ImSnprintf(out_buf, out_buf_size, "%llu", (unsigned long long)uint64);
return;
}
if (data_format == DataFormat_Hex) {
ImSnprintf(out_buf, out_buf_size, "0x%016llx",
(unsigned long long)uint64);
return;
}
break;
}
case ImGuiDataType_Float: {
float float32 = 0.0f;
EndianessCopy(&float32, buf, size);
if (data_format == DataFormat_Dec) {
ImSnprintf(out_buf, out_buf_size, "%f", float32);
return;
}
if (data_format == DataFormat_Hex) {
ImSnprintf(out_buf, out_buf_size, "%a", float32);
return;
}
break;
}
case ImGuiDataType_Double: {
double float64 = 0.0;
EndianessCopy(&float64, buf, size);
if (data_format == DataFormat_Dec) {
ImSnprintf(out_buf, out_buf_size, "%f", float64);
return;
}
if (data_format == DataFormat_Hex) {
ImSnprintf(out_buf, out_buf_size, "%a", float64);
return;
}
break;
}
case ImGuiDataType_COUNT:
break;
} // Switch
}
};
} // namespace viz
} // namespace afs
#ifdef _MSC_VER
#pragma warning(pop)
#endif

View File

@@ -0,0 +1,385 @@
#include "sample_review.h"
#include <imgui.h>
#include <algorithm>
#include <fstream>
#include <sstream>
#include <filesystem>
#include "../core/filesystem.h"
#include "../core/logger.h"
namespace afs {
namespace viz {
namespace {
TrainingSample ParseSampleLine(const std::string& line, bool is_rejected = false) {
TrainingSample sample;
sample.is_rejected = is_rejected;
try {
auto j = nlohmann::json::parse(line);
sample.instruction = j.value("instruction", "");
sample.input = j.value("input", "");
sample.output = j.value("output", "");
sample.domain = j.value("domain", "unknown");
sample.source = j.value("source", "");
if (is_rejected) {
if (j.contains("rejection_reason")) {
sample.rejection_reason = j["rejection_reason"];
}
if (j.contains("quality_score")) {
sample.quality_score = j["quality_score"];
}
if (j.contains("rejection_details")) {
sample.rejection_details = j["rejection_details"];
}
}
} catch (...) {
// Skip malformed lines
}
return sample;
}
} // namespace
SampleReviewWidget::SampleReviewWidget() {}
bool SampleReviewWidget::LoadDataset(const std::filesystem::path& dataset_dir) {
dataset_dir_ = dataset_dir;
accepted_samples_.clear();
rejected_samples_.clear();
// Load rejected samples
auto rejected_file = dataset_dir / "rejected.jsonl";
if (studio::core::FileSystem::Exists(rejected_file)) {
std::ifstream f(rejected_file);
if (f.is_open()) {
std::string line;
while (std::getline(f, line)) {
if (!line.empty()) {
rejected_samples_.push_back(ParseSampleLine(line, true));
}
}
}
}
// Load accepted samples
auto train_file = dataset_dir / "train.jsonl";
if (studio::core::FileSystem::Exists(train_file)) {
std::ifstream f(train_file);
if (f.is_open()) {
std::string line;
while (std::getline(f, line)) {
if (!line.empty()) {
accepted_samples_.push_back(ParseSampleLine(line, false));
}
}
}
}
return !accepted_samples_.empty() || !rejected_samples_.empty();
}
void SampleReviewWidget::Render(bool* p_open) {
ImGui::SetNextWindowSize(ImVec2(1400, 800), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Sample Review & Annotation", p_open)) {
ImGui::End();
return;
}
// Header stats
ImGui::Text("Dataset: %s", dataset_dir_.filename().c_str());
ImGui::SameLine(0, 20);
ImGui::Text("Accepted: %zu", accepted_samples_.size());
ImGui::SameLine(0, 20);
ImGui::Text("Rejected: %zu", rejected_samples_.size());
ImGui::Separator();
// Main layout: 3 columns
ImGui::Columns(3, "review_layout", true);
// Left: Sample Browser
RenderSampleBrowser();
ImGui::NextColumn();
// Middle: Sample Viewer
RenderSampleViewer();
ImGui::NextColumn();
// Right: Context Reference + Feedback
RenderContextReference();
ImGui::Spacing();
RenderFeedbackPanel();
ImGui::Columns(1);
ImGui::End();
}
void SampleReviewWidget::RenderSampleBrowser() {
ImGui::Text("Sample Browser");
ImGui::Separator();
// Filters
ImGui::Checkbox("Show Rejected", &show_rejected_);
ImGui::SameLine();
ImGui::Checkbox("Show Accepted", &show_accepted_);
ImGui::InputText("Domain", domain_filter_, sizeof(domain_filter_));
ImGui::InputText("Reason", reason_filter_, sizeof(reason_filter_));
ImGui::SliderFloat("Min Score", &min_quality_score_, 0.0f, 1.0f);
ImGui::Separator();
// Sample list
ImGui::BeginChild("SampleList", ImVec2(0, -30), true);
auto render_samples = [&](const std::vector<TrainingSample>& samples, const char* label) {
if (ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen)) {
for (size_t i = 0; i < samples.size(); ++i) {
const auto& sample = samples[i];
// Apply filters
if (domain_filter_[0] != '\0' && sample.domain.find(domain_filter_) == std::string::npos) continue;
if (sample.quality_score && *sample.quality_score < min_quality_score_) continue;
char label_buf[256];
snprintf(label_buf, sizeof(label_buf), "[%s] %s##%zu",
sample.domain.c_str(),
sample.instruction.substr(0, 40).c_str(),
i);
if (ImGui::Selectable(label_buf, (int)i == current_sample_idx_)) {
current_sample_idx_ = i;
}
// Show quality score if available
if (sample.quality_score) {
ImGui::SameLine();
ImGui::TextDisabled("%.3f", *sample.quality_score);
}
}
ImGui::TreePop();
}
};
if (show_rejected_) {
render_samples(rejected_samples_, "Rejected Samples");
}
if (show_accepted_) {
render_samples(accepted_samples_, "Accepted Samples");
}
ImGui::EndChild();
// Navigation
if (ImGui::Button("< Prev")) {
current_sample_idx_ = std::max(0, current_sample_idx_ - 1);
}
ImGui::SameLine();
if (ImGui::Button("Next >")) {
current_sample_idx_++;
}
}
void SampleReviewWidget::RenderSampleViewer() {
ImGui::Text("Sample Details");
ImGui::Separator();
if (rejected_samples_.empty() && accepted_samples_.empty()) {
ImGui::TextDisabled("No samples loaded");
return;
}
// Get current sample
const TrainingSample* sample = nullptr;
if (current_sample_idx_ < (int)rejected_samples_.size()) {
sample = &rejected_samples_[current_sample_idx_];
} else if (current_sample_idx_ < (int)(rejected_samples_.size() + accepted_samples_.size())) {
sample = &accepted_samples_[current_sample_idx_ - rejected_samples_.size()];
}
if (!sample) {
ImGui::TextDisabled("No sample selected");
return;
}
// Metadata
ImGui::Text("Domain: %s", sample->domain.c_str());
ImGui::SameLine(0, 20);
ImGui::Text("Source: %s", sample->source.c_str());
if (sample->is_rejected) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "REJECTED");
if (sample->rejection_reason) {
ImGui::SameLine();
ImGui::Text("- %s", sample->rejection_reason->c_str());
}
if (sample->quality_score) {
ImGui::SameLine();
ImGui::Text("(score: %.3f)", *sample->quality_score);
}
} else {
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "ACCEPTED");
}
ImGui::Separator();
// Content
ImGui::BeginChild("SampleContent", ImVec2(0, 0), true);
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Instruction:");
ImGui::TextWrapped("%s", sample->instruction.c_str());
ImGui::Spacing();
if (!sample->input.empty()) {
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.6f, 1.0f), "Input:");
ImGui::TextWrapped("%s", sample->input.c_str());
ImGui::Spacing();
}
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "Output:");
ImGui::TextWrapped("%s", sample->output.c_str());
// Rejection details
if (sample->rejection_details) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Rejection Details:");
auto& details = *sample->rejection_details;
if (details.contains("threshold")) {
ImGui::Text("Threshold: %.3f", details["threshold"].get<float>());
}
if (details.contains("diversity")) {
ImGui::Text("Diversity: %.3f", details["diversity"].get<float>());
}
if (details.contains("kg_consistency")) {
ImGui::Text("KG Consistency: %.3f", details["kg_consistency"].get<float>());
}
if (details.contains("hallucination_risk")) {
ImGui::Text("Hallucination Risk: %.3f", details["hallucination_risk"].get<float>());
}
if (details.contains("coherence")) {
ImGui::Text("Coherence: %.3f", details["coherence"].get<float>());
}
}
ImGui::EndChild();
}
void SampleReviewWidget::RenderContextReference() {
ImGui::Text("Reference Context");
ImGui::Separator();
ImGui::InputText("Search ASM", context_search_, sizeof(context_search_));
if (ImGui::Button("Load My ASM Files")) {
LoadContextFiles();
}
ImGui::BeginChild("ContextFiles", ImVec2(0, 200), true);
for (const auto& file : context_files_) {
if (ImGui::Selectable(file.filename().c_str())) {
std::ifstream f(file);
std::stringstream ss;
ss << f.rdbuf();
selected_context_content_ = ss.str();
}
}
ImGui::EndChild();
if (!selected_context_content_.empty()) {
ImGui::Text("Context Preview:");
ImGui::BeginChild("ContextPreview", ImVec2(0, 150), true);
ImGui::TextWrapped("%s", selected_context_content_.substr(0, 500).c_str());
ImGui::EndChild();
}
}
void SampleReviewWidget::RenderFeedbackPanel() {
ImGui::Text("User Feedback");
ImGui::Separator();
ImGui::InputTextMultiline("##feedback", feedback_buffer_, sizeof(feedback_buffer_),
ImVec2(-1, 100));
if (ImGui::Button("✓ Approve (Use as Golden Example)", ImVec2(-1, 0))) {
ApproveCurrentSample();
}
if (ImGui::Button("✗ Reject (Bad Sample)", ImVec2(-1, 0))) {
RejectCurrentSample(feedback_buffer_);
}
ImGui::Spacing();
if (ImGui::Button("Save All Annotations", ImVec2(-1, 0))) {
SaveAnnotations();
}
}
void SampleReviewWidget::LoadContextFiles() {
context_files_.clear();
// Load user's ASM files from alttp disassembly
auto alttp_path = std::filesystem::path(std::getenv("HOME")) / ".context" / "knowledge" / "alttp";
if (studio::core::FileSystem::Exists(alttp_path)) {
try {
std::error_code ec;
for (const auto& entry : std::filesystem::recursive_directory_iterator(alttp_path, ec)) {
std::error_code entry_ec;
if (!ec && entry.is_regular_file(entry_ec) && entry.path().extension() == ".asm") {
context_files_.push_back(entry.path());
}
}
} catch (...) {
LOG_WARN("Error during recursive ASM search in " + alttp_path.string());
}
}
}
void SampleReviewWidget::ApproveCurrentSample() {
// Mark current sample as user-approved golden example
// This will be saved to a separate file
}
void SampleReviewWidget::RejectCurrentSample(const std::string& reason) {
// Mark current sample as user-rejected with feedback
}
void SampleReviewWidget::SaveAnnotations() {
auto annotations_file = dataset_dir_ / "user_annotations.json";
nlohmann::json annotations = nlohmann::json::array();
// Save approved/rejected samples with user feedback
for (const auto& sample : rejected_samples_) {
if (sample.user_approved || !sample.user_feedback.empty()) {
annotations.push_back({
{"instruction", sample.instruction},
{"domain", sample.domain},
{"user_approved", sample.user_approved},
{"user_feedback", sample.user_feedback}
});
}
}
std::ofstream f(annotations_file);
f << annotations.dump(2);
}
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,75 @@
#pragma once
#include <string>
#include <vector>
#include <optional>
#include <filesystem>
#include <nlohmann/json.hpp>
namespace afs {
namespace viz {
struct TrainingSample {
std::string instruction;
std::string input;
std::string output;
std::string domain;
std::string source;
// For rejected samples
std::optional<std::string> rejection_reason;
std::optional<float> quality_score;
std::optional<nlohmann::json> rejection_details;
bool is_rejected = false;
bool user_approved = false;
std::string user_feedback;
};
class SampleReviewWidget {
public:
SampleReviewWidget();
/// Load samples from dataset directory
bool LoadDataset(const std::filesystem::path& dataset_dir);
/// Render the review window
void Render(bool* p_open = nullptr);
/// Save user annotations/approvals
void SaveAnnotations();
private:
void RenderSampleBrowser();
void RenderSampleViewer();
void RenderContextReference();
void RenderFeedbackPanel();
void LoadContextFiles();
void ApproveCurrentSample();
void RejectCurrentSample(const std::string& reason);
// Data
std::vector<TrainingSample> accepted_samples_;
std::vector<TrainingSample> rejected_samples_;
std::filesystem::path dataset_dir_;
// UI State
int current_sample_idx_ = 0;
bool show_rejected_ = true;
bool show_accepted_ = true;
char feedback_buffer_[1024] = {0};
char context_search_[256] = {0};
// Filters
char domain_filter_[128] = {0};
char reason_filter_[128] = {0};
float min_quality_score_ = 0.0f;
// Context files (user's ASM for reference)
std::vector<std::filesystem::path> context_files_;
std::string selected_context_content_;
};
} // namespace viz
} // namespace afs

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,391 @@
#ifndef YAZE_APP_GUI_MODULES_TEXT_EDITOR_H
#define YAZE_APP_GUI_MODULES_TEXT_EDITOR_H
// Originally from ImGuiColorTextEdit/TextEditor.h
#include <array>
#include <map>
#include <memory>
#include <regex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "imgui.h"
class TextEditor {
public:
enum class PaletteIndex {
Default,
Keyword,
Number,
String,
CharLiteral,
Punctuation,
Preprocessor,
Identifier,
KnownIdentifier,
PreprocIdentifier,
Comment,
MultiLineComment,
Background,
Cursor,
Selection,
ErrorMarker,
Breakpoint,
LineNumber,
CurrentLineFill,
CurrentLineFillInactive,
CurrentLineEdge,
Max
};
enum class SelectionMode { Normal, Word, Line };
struct Breakpoint {
int mLine;
bool mEnabled;
std::string mCondition;
Breakpoint() : mLine(-1), mEnabled(false) {}
};
// Represents a character coordinate from the user's point of view,
// i. e. consider an uniform grid (assuming fixed-width font) on the
// screen as it is rendered, and each cell has its own coordinate, starting
// from 0. Tabs are counted as [1..mTabSize] count empty spaces, depending on
// how many space is necessary to reach the next tab stop.
// For example, coordinate (1, 5) represents the character 'B' in a line
// "\tABC", when mTabSize = 4, because it is rendered as " ABC" on the
// screen.
struct Coordinates {
int mLine, mColumn;
Coordinates() : mLine(0), mColumn(0) {}
Coordinates(int aLine, int aColumn) : mLine(aLine), mColumn(aColumn) {
assert(aLine >= 0);
assert(aColumn >= 0);
}
static Coordinates Invalid() {
static Coordinates invalid(-1, -1);
return invalid;
}
bool operator==(const Coordinates& o) const {
return mLine == o.mLine && mColumn == o.mColumn;
}
bool operator!=(const Coordinates& o) const {
return mLine != o.mLine || mColumn != o.mColumn;
}
bool operator<(const Coordinates& o) const {
if (mLine != o.mLine)
return mLine < o.mLine;
return mColumn < o.mColumn;
}
bool operator>(const Coordinates& o) const {
if (mLine != o.mLine)
return mLine > o.mLine;
return mColumn > o.mColumn;
}
bool operator<=(const Coordinates& o) const {
if (mLine != o.mLine)
return mLine < o.mLine;
return mColumn <= o.mColumn;
}
bool operator>=(const Coordinates& o) const {
if (mLine != o.mLine)
return mLine > o.mLine;
return mColumn >= o.mColumn;
}
};
struct Identifier {
Coordinates mLocation;
std::string mDeclaration;
};
typedef std::string String;
typedef std::unordered_map<std::string, Identifier> Identifiers;
typedef std::unordered_set<std::string> Keywords;
typedef std::map<int, std::string> ErrorMarkers;
typedef std::unordered_set<int> Breakpoints;
typedef std::array<ImU32, (unsigned)PaletteIndex::Max> Palette;
typedef uint8_t Char;
struct Glyph {
Char mChar;
PaletteIndex mColorIndex = PaletteIndex::Default;
bool mComment : 1;
bool mMultiLineComment : 1;
bool mPreprocessor : 1;
Glyph(Char aChar, PaletteIndex aColorIndex)
: mChar(aChar),
mColorIndex(aColorIndex),
mComment(false),
mMultiLineComment(false),
mPreprocessor(false) {}
};
typedef std::vector<Glyph> Line;
typedef std::vector<Line> Lines;
struct LanguageDefinition {
typedef std::pair<std::string, PaletteIndex> TokenRegexString;
typedef std::vector<TokenRegexString> TokenRegexStrings;
typedef bool (*TokenizeCallback)(const char* in_begin, const char* in_end,
const char*& out_begin,
const char*& out_end,
PaletteIndex& paletteIndex);
std::string mName;
Keywords mKeywords;
Identifiers mIdentifiers;
Identifiers mPreprocIdentifiers;
std::string mCommentStart, mCommentEnd, mSingleLineComment;
char mPreprocChar;
bool mAutoIndentation;
TokenizeCallback mTokenize;
TokenRegexStrings mTokenRegexStrings;
bool mCaseSensitive;
LanguageDefinition()
: mPreprocChar('#'),
mAutoIndentation(true),
mTokenize(nullptr),
mCaseSensitive(true) {}
static const LanguageDefinition& CPlusPlus();
static const LanguageDefinition& HLSL();
static const LanguageDefinition& GLSL();
static const LanguageDefinition& C();
static const LanguageDefinition& SQL();
static const LanguageDefinition& AngelScript();
static const LanguageDefinition& Lua();
};
TextEditor();
~TextEditor();
void SetLanguageDefinition(const LanguageDefinition& aLanguageDef);
const LanguageDefinition& GetLanguageDefinition() const {
return mLanguageDefinition;
}
const Palette& GetPalette() const { return mPaletteBase; }
void SetPalette(const Palette& aValue);
void SetErrorMarkers(const ErrorMarkers& aMarkers) {
mErrorMarkers = aMarkers;
}
void SetBreakpoints(const Breakpoints& aMarkers) { mBreakpoints = aMarkers; }
void Render(const char* aTitle, const ImVec2& aSize = ImVec2(),
bool aBorder = false);
void SetText(const std::string& aText);
std::string GetText() const;
void SetTextLines(const std::vector<std::string>& aLines);
std::vector<std::string> GetTextLines() const;
std::string GetSelectedText() const;
std::string GetCurrentLineText() const;
int GetTotalLines() const { return (int)mLines.size(); }
bool IsOverwrite() const { return mOverwrite; }
void SetReadOnly(bool aValue);
bool IsReadOnly() const { return mReadOnly; }
bool IsTextChanged() const { return mTextChanged; }
bool IsCursorPositionChanged() const { return mCursorPositionChanged; }
bool IsColorizerEnabled() const { return mColorizerEnabled; }
void SetColorizerEnable(bool aValue);
Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); }
void SetCursorPosition(const Coordinates& aPosition);
inline void SetHandleMouseInputs(bool aValue) { mHandleMouseInputs = aValue; }
inline bool IsHandleMouseInputsEnabled() const {
return mHandleKeyboardInputs;
}
inline void SetHandleKeyboardInputs(bool aValue) {
mHandleKeyboardInputs = aValue;
}
inline bool IsHandleKeyboardInputsEnabled() const {
return mHandleKeyboardInputs;
}
inline void SetImGuiChildIgnored(bool aValue) { mIgnoreImGuiChild = aValue; }
inline bool IsImGuiChildIgnored() const { return mIgnoreImGuiChild; }
inline void SetShowWhitespaces(bool aValue) { mShowWhitespaces = aValue; }
inline bool IsShowingWhitespaces() const { return mShowWhitespaces; }
void SetTabSize(int aValue);
inline int GetTabSize() const { return mTabSize; }
void InsertText(const std::string& aValue);
void InsertText(const char* aValue);
void MoveUp(int aAmount = 1, bool aSelect = false);
void MoveDown(int aAmount = 1, bool aSelect = false);
void MoveLeft(int aAmount = 1, bool aSelect = false, bool aWordMode = false);
void MoveRight(int aAmount = 1, bool aSelect = false, bool aWordMode = false);
void MoveTop(bool aSelect = false);
void MoveBottom(bool aSelect = false);
void MoveHome(bool aSelect = false);
void MoveEnd(bool aSelect = false);
void SetSelectionStart(const Coordinates& aPosition);
void SetSelectionEnd(const Coordinates& aPosition);
void SetSelection(const Coordinates& aStart, const Coordinates& aEnd,
SelectionMode aMode = SelectionMode::Normal);
void SelectWordUnderCursor();
void SelectAll();
bool HasSelection() const;
void Copy();
void Cut();
void Paste();
void Delete();
bool CanUndo() const;
bool CanRedo() const;
void Undo(int aSteps = 1);
void Redo(int aSteps = 1);
static const Palette& GetDarkPalette();
static const Palette& GetLightPalette();
static const Palette& GetRetroBluePalette();
private:
typedef std::vector<std::pair<std::regex, PaletteIndex>> RegexList;
struct EditorState {
Coordinates mSelectionStart;
Coordinates mSelectionEnd;
Coordinates mCursorPosition;
};
class UndoRecord {
public:
UndoRecord() {}
~UndoRecord() {}
UndoRecord(const std::string& aAdded,
const TextEditor::Coordinates aAddedStart,
const TextEditor::Coordinates aAddedEnd,
const std::string& aRemoved,
const TextEditor::Coordinates aRemovedStart,
const TextEditor::Coordinates aRemovedEnd,
TextEditor::EditorState& aBefore,
TextEditor::EditorState& aAfter);
void Undo(TextEditor* aEditor);
void Redo(TextEditor* aEditor);
std::string mAdded;
Coordinates mAddedStart;
Coordinates mAddedEnd;
std::string mRemoved;
Coordinates mRemovedStart;
Coordinates mRemovedEnd;
EditorState mBefore;
EditorState mAfter;
};
typedef std::vector<UndoRecord> UndoBuffer;
void ProcessInputs();
void Colorize(int aFromLine = 0, int aCount = -1);
void ColorizeRange(int aFromLine = 0, int aToLine = 0);
void ColorizeInternal();
float TextDistanceToLineStart(const Coordinates& aFrom) const;
void EnsureCursorVisible();
int GetPageSize() const;
std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const;
Coordinates GetActualCursorCoordinates() const;
Coordinates SanitizeCoordinates(const Coordinates& aValue) const;
void Advance(Coordinates& aCoordinates) const;
void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd);
int InsertTextAt(Coordinates& aWhere, const char* aValue);
void AddUndo(UndoRecord& aValue);
Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const;
Coordinates FindWordStart(const Coordinates& aFrom) const;
Coordinates FindWordEnd(const Coordinates& aFrom) const;
Coordinates FindNextWord(const Coordinates& aFrom) const;
int GetCharacterIndex(const Coordinates& aCoordinates) const;
int GetCharacterColumn(int aLine, int aIndex) const;
int GetLineCharacterCount(int aLine) const;
int GetLineMaxColumn(int aLine) const;
bool IsOnWordBoundary(const Coordinates& aAt) const;
void RemoveLine(int aStart, int aEnd);
void RemoveLine(int aIndex);
Line& InsertLine(int aIndex);
void EnterCharacter(ImWchar aChar, bool aShift);
void Backspace();
void DeleteSelection();
std::string GetWordUnderCursor() const;
std::string GetWordAt(const Coordinates& aCoords) const;
ImU32 GetGlyphColor(const Glyph& aGlyph) const;
void HandleKeyboardInputs();
void HandleMouseInputs();
void Render();
float mLineSpacing;
Lines mLines;
EditorState mState;
UndoBuffer mUndoBuffer;
int mUndoIndex;
int mTabSize;
bool mOverwrite;
bool mReadOnly;
bool mWithinRender;
bool mScrollToCursor;
bool mScrollToTop;
bool mTextChanged;
bool mColorizerEnabled;
float mTextStart; // position (in pixels) where a code line starts relative
// to the left of the TextEditor.
int mLeftMargin;
bool mCursorPositionChanged;
int mColorRangeMin, mColorRangeMax;
SelectionMode mSelectionMode;
bool mHandleKeyboardInputs;
bool mHandleMouseInputs;
bool mIgnoreImGuiChild;
bool mShowWhitespaces;
Palette mPaletteBase;
Palette mPalette;
LanguageDefinition mLanguageDefinition;
RegexList mRegexList;
bool mCheckComments;
Breakpoints mBreakpoints;
ErrorMarkers mErrorMarkers;
ImVec2 mCharAdvance;
Coordinates mInteractiveStart, mInteractiveEnd;
std::string mLineBuffer;
uint64_t mStartTime;
float mLastClick;
};
#endif // YAZE_APP_GUI_MODULES_TEXT_EDITOR_H

View File

@@ -0,0 +1,352 @@
/**
* @file training_status.cpp
* @brief Implementation of training campaign status monitoring widget
*/
#include "training_status.h"
#include <cstdlib>
#include <sstream>
#include <fstream>
#include <iostream>
#include <iomanip>
#include <filesystem>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace afs {
namespace viz {
TrainingStatusWidget::TrainingStatusWidget()
: last_update_(std::chrono::steady_clock::now()) {
// Initial update
Update();
}
void TrainingStatusWidget::Render(bool* p_open) {
if (!p_open || !*p_open) {
return;
}
// Auto-update check
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - last_update_).count();
if (auto_update_ && elapsed >= static_cast<int>(update_interval_)) {
Update();
}
// Main window
ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Training Campaign Status", p_open)) {
ImGui::End();
return;
}
// Header with timestamp
ImGui::Text("Last Update: %s", FormatTimestamp(health_.campaign ? health_.campaign->last_update : "").c_str());
ImGui::SameLine(ImGui::GetWindowWidth() - 150);
if (ImGui::Button("Refresh Now")) {
Update();
}
ImGui::SameLine();
ImGui::Checkbox("Auto", &auto_update_);
ImGui::Separator();
// Error message if any
if (!error_message_.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::TextWrapped("Error: %s", error_message_.c_str());
ImGui::PopStyleColor();
ImGui::Separator();
}
// Sections in columns
ImGui::BeginChild("StatusSections", ImVec2(0, 0), false);
// Campaign section
RenderCampaignSection();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// System resources section
RenderSystemResourcesSection();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Services section
RenderServicesSection();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Issues section
RenderIssuesSection();
ImGui::EndChild();
ImGui::End();
}
void TrainingStatusWidget::Update() {
if (FetchHealthData()) {
last_update_ = std::chrono::steady_clock::now();
error_message_.clear();
}
}
bool TrainingStatusWidget::FetchHealthData() {
// Execute Python health check script
const char* env_root = std::getenv("AFS_ROOT");
std::string root;
if (env_root && env_root[0] != '\0') {
root = env_root;
} else {
const char* home = std::getenv("HOME");
if (home && home[0] != '\0') {
root = std::string(home) + "/src/trunk/scawful/research/afs";
}
}
if (root.empty()) {
std::error_code ec;
root = std::filesystem::current_path(ec).string();
}
if (root.empty()) {
error_message_ = "Unable to resolve AFS root path";
return false;
}
std::string cmd = "cd \"" + root + "\" && PYTHONPATH=src .venv/bin/python -m agents.training.health_check --json 2>/dev/null";
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) {
error_message_ = "Failed to execute health check command";
return false;
}
// Read output
std::stringstream buffer;
char line[256];
while (fgets(line, sizeof(line), pipe)) {
buffer << line;
}
int status = pclose(pipe);
if (status != 0) {
error_message_ = "Health check command failed with status " + std::to_string(status);
return false;
}
// Parse JSON
std::string json_str = buffer.str();
return ParseHealthJSON(json_str);
}
bool TrainingStatusWidget::ParseHealthJSON(const std::string& json_str) {
try {
auto j = json::parse(json_str);
// Parse campaign status
if (j.contains("campaign") && !j["campaign"].is_null()) {
CampaignStatus campaign;
auto c = j["campaign"];
if (c.contains("pid") && !c["pid"].is_null()) {
campaign.pid = c["pid"];
}
if (c.contains("log_file") && !c["log_file"].is_null()) {
campaign.log_file = c["log_file"];
}
campaign.running = c.value("running", false);
campaign.samples_generated = c.value("samples_generated", 0);
campaign.target_samples = c.value("target_samples", 0);
campaign.progress_percent = c.value("progress_percent", 0.0f);
campaign.current_domain = c.value("current_domain", "unknown");
campaign.samples_per_min = c.value("samples_per_min", 0.0f);
campaign.eta_hours = c.value("eta_hours", 0.0f);
campaign.quality_pass_rate = c.value("quality_pass_rate", 0.0f);
campaign.last_update = c.value("last_update", "");
health_.campaign = campaign;
} else {
health_.campaign = std::nullopt;
}
// Parse system health
health_.embedding_service_running = j.value("embedding_service_running", false);
health_.knowledge_bases_loaded = j.value("knowledge_bases_loaded", 0);
health_.cpu_percent = j.value("cpu_percent", 0.0f);
health_.memory_percent = j.value("memory_percent", 0.0f);
health_.disk_free_gb = j.value("disk_free_gb", 0.0f);
// Parse issues
health_.issues.clear();
if (j.contains("issues")) {
for (const auto& issue : j["issues"]) {
health_.issues.push_back(issue);
}
}
if (j.contains("last_checkpoint") && !j["last_checkpoint"].is_null()) {
health_.last_checkpoint = j["last_checkpoint"];
}
return true;
} catch (const json::exception& e) {
error_message_ = std::string("JSON parse error: ") + e.what();
return false;
}
}
void TrainingStatusWidget::RenderCampaignSection() {
ImGui::Text("Campaign Status");
ImGui::Spacing();
if (!health_.campaign.has_value()) {
ImGui::TextColored(ImVec4(0.5f, 0.5f, 1.0f, 1.0f), "🔵 No active campaign");
return;
}
const auto& c = health_.campaign.value();
// Status indicator
ImVec4 status_color = c.running ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
const char* status_icon = c.running ? "🟢" : "🔴";
ImGui::TextColored(status_color, "%s %s", status_icon, c.running ? "Running" : "Stopped");
if (c.pid > 0) {
ImGui::Text("PID: %d", c.pid);
}
// Progress bar
ImGui::Text("Progress:");
ImGui::ProgressBar(c.progress_percent / 100.0f, ImVec2(-1, 0), "");
ImGui::SameLine();
ImGui::Text("%d / %d (%.1f%%)", c.samples_generated, c.target_samples, c.progress_percent);
// Metrics
ImGui::Text("Domain: %s", c.current_domain.c_str());
ImGui::Text("Generation Rate: %.1f samples/min", c.samples_per_min);
ImGui::Text("Quality Pass Rate: %.1f%%", c.quality_pass_rate * 100.0f);
if (c.eta_hours > 0) {
ImGui::Text("ETA: %s", FormatDuration(c.eta_hours).c_str());
}
if (!c.log_file.empty()) {
ImGui::Text("Log: %s", c.log_file.c_str());
ImGui::SameLine();
if (ImGui::SmallButton("Open")) {
std::string cmd = "open -a Terminal " + c.log_file;
system(cmd.c_str());
}
}
}
void TrainingStatusWidget::RenderSystemResourcesSection() {
ImGui::Text("System Resources");
ImGui::Spacing();
// CPU
const char* cpu_icon = GetStatusIcon(health_.cpu_percent, 70.0f, 90.0f);
ImVec4 cpu_color = GetStatusColor(health_.cpu_percent, 70.0f, 90.0f);
ImGui::TextColored(cpu_color, "%s CPU: %.1f%%", cpu_icon, health_.cpu_percent);
// Memory
const char* mem_icon = GetStatusIcon(health_.memory_percent, 70.0f, 90.0f);
ImVec4 mem_color = GetStatusColor(health_.memory_percent, 70.0f, 90.0f);
ImGui::TextColored(mem_color, "%s Memory: %.1f%%", mem_icon, health_.memory_percent);
// Disk (inverted thresholds - low disk is bad)
const char* disk_icon = health_.disk_free_gb > 50 ? "🟢" :
health_.disk_free_gb > 10 ? "🟡" : "🔴";
ImVec4 disk_color = health_.disk_free_gb > 50 ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) :
health_.disk_free_gb > 10 ? ImVec4(1.0f, 1.0f, 0.0f, 1.0f) :
ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
ImGui::TextColored(disk_color, "%s Disk Free: %.1f GB", disk_icon, health_.disk_free_gb);
}
void TrainingStatusWidget::RenderServicesSection() {
ImGui::Text("Services");
ImGui::Spacing();
// Embedding service
const char* emb_icon = health_.embedding_service_running ? "🟢" : "🔴";
ImVec4 emb_color = health_.embedding_service_running ?
ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
ImGui::TextColored(emb_color, "%s Embedding Service: %s", emb_icon,
health_.embedding_service_running ? "Running" : "Stopped");
ImGui::Text("Knowledge Bases: %d loaded", health_.knowledge_bases_loaded);
}
void TrainingStatusWidget::RenderIssuesSection() {
if (health_.issues.empty()) {
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "✅ No issues detected");
return;
}
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "⚠️ Issues (%zu)", health_.issues.size());
ImGui::Spacing();
for (const auto& issue : health_.issues) {
ImGui::BulletText("%s", issue.c_str());
}
}
ImVec4 TrainingStatusWidget::GetStatusColor(float value, float warn_threshold, float critical_threshold) {
if (value < warn_threshold) {
return ImVec4(0.0f, 1.0f, 0.0f, 1.0f); // Green
} else if (value < critical_threshold) {
return ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Yellow
} else {
return ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red
}
}
const char* TrainingStatusWidget::GetStatusIcon(float value, float warn_threshold, float critical_threshold) {
if (value < warn_threshold) {
return "🟢";
} else if (value < critical_threshold) {
return "🟡";
} else {
return "🔴";
}
}
std::string TrainingStatusWidget::FormatDuration(float hours) {
int h = static_cast<int>(hours);
int m = static_cast<int>((hours - h) * 60);
std::ostringstream oss;
if (h > 0) {
oss << h << "h ";
}
oss << m << "m";
return oss.str();
}
std::string TrainingStatusWidget::FormatTimestamp(const std::string& iso_timestamp) {
if (iso_timestamp.empty()) {
return "Never";
}
// Parse ISO timestamp (simplified - just show time part)
size_t time_pos = iso_timestamp.find('T');
if (time_pos != std::string::npos && time_pos + 8 < iso_timestamp.size()) {
return iso_timestamp.substr(time_pos + 1, 8); // HH:MM:SS
}
return iso_timestamp;
}
} // namespace viz
} // namespace afs

View File

@@ -0,0 +1,93 @@
/**
* @file training_status.h
* @brief Training campaign status monitoring widget for ImGui
*
* Displays real-time status of training data generation campaigns:
* - Campaign progress (samples generated, target, percentage)
* - Generation rate (samples/min, ETA)
* - Quality metrics (pass rate, domain breakdown)
* - System resources (CPU, memory, disk)
* - Issues and alerts
*
* Data source: Python health_check.py via JSON output
*/
#pragma once
#include <string>
#include <vector>
#include <chrono>
#include <optional>
#include "imgui.h"
namespace afs {
namespace viz {
struct CampaignStatus {
int pid = 0;
std::string log_file;
bool running = false;
int samples_generated = 0;
int target_samples = 0;
float progress_percent = 0.0f;
std::string current_domain;
float samples_per_min = 0.0f;
float eta_hours = 0.0f;
float quality_pass_rate = 0.0f;
std::string last_update;
};
struct SystemHealth {
std::optional<CampaignStatus> campaign;
bool embedding_service_running = false;
int knowledge_bases_loaded = 0;
float cpu_percent = 0.0f;
float memory_percent = 0.0f;
float disk_free_gb = 0.0f;
std::vector<std::string> issues;
std::string last_checkpoint;
};
class TrainingStatusWidget {
public:
TrainingStatusWidget();
~TrainingStatusWidget() = default;
// Render the training status widget
void Render(bool* p_open = nullptr);
// Update health data (call periodically)
void Update();
// Set update interval (default: 30 seconds)
void SetUpdateInterval(float seconds) { update_interval_ = seconds; }
private:
// Fetch health data from Python health_check script
bool FetchHealthData();
// Parse JSON health data
bool ParseHealthJSON(const std::string& json);
// Render sections
void RenderCampaignSection();
void RenderSystemResourcesSection();
void RenderServicesSection();
void RenderIssuesSection();
// Helper functions
ImVec4 GetStatusColor(float value, float warn_threshold, float critical_threshold);
const char* GetStatusIcon(float value, float warn_threshold, float critical_threshold);
std::string FormatDuration(float hours);
std::string FormatTimestamp(const std::string& iso_timestamp);
// Data
SystemHealth health_;
std::chrono::steady_clock::time_point last_update_;
float update_interval_ = 30.0f; // seconds
bool auto_update_ = true;
std::string error_message_;
};
} // namespace viz
} // namespace afs