apps: add studio sources
This commit is contained in:
160
apps/studio/CMakeLists.txt
Normal file
160
apps/studio/CMakeLists.txt
Normal 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
33
apps/studio/README.md
Normal 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
584
apps/studio/src/app.cc
Normal 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
98
apps/studio/src/app.h
Normal 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
|
||||
BIN
apps/studio/src/assets/font/Cousine-Regular.ttf
Normal file
BIN
apps/studio/src/assets/font/Cousine-Regular.ttf
Normal file
Binary file not shown.
BIN
apps/studio/src/assets/font/DroidSans.ttf
Normal file
BIN
apps/studio/src/assets/font/DroidSans.ttf
Normal file
Binary file not shown.
BIN
apps/studio/src/assets/font/IBMPlexSansJP-Bold.ttf
Normal file
BIN
apps/studio/src/assets/font/IBMPlexSansJP-Bold.ttf
Normal file
Binary file not shown.
BIN
apps/studio/src/assets/font/Karla-Regular.ttf
Normal file
BIN
apps/studio/src/assets/font/Karla-Regular.ttf
Normal file
Binary file not shown.
BIN
apps/studio/src/assets/font/MaterialIcons-Regular.ttf
Normal file
BIN
apps/studio/src/assets/font/MaterialIcons-Regular.ttf
Normal file
Binary file not shown.
BIN
apps/studio/src/assets/font/NotoSansJP.ttf
Normal file
BIN
apps/studio/src/assets/font/NotoSansJP.ttf
Normal file
Binary file not shown.
BIN
apps/studio/src/assets/font/Roboto-Medium.ttf
Normal file
BIN
apps/studio/src/assets/font/Roboto-Medium.ttf
Normal file
Binary file not shown.
68
apps/studio/src/core/assets.cc
Normal file
68
apps/studio/src/core/assets.cc
Normal 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
|
||||
28
apps/studio/src/core/assets.h
Normal file
28
apps/studio/src/core/assets.h
Normal 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
|
||||
94
apps/studio/src/core/context.cc
Normal file
94
apps/studio/src/core/context.cc
Normal 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
|
||||
37
apps/studio/src/core/context.h
Normal file
37
apps/studio/src/core/context.h
Normal 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
|
||||
207
apps/studio/src/core/deployment_actions.cc
Normal file
207
apps/studio/src/core/deployment_actions.cc
Normal 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
|
||||
101
apps/studio/src/core/deployment_actions.h
Normal file
101
apps/studio/src/core/deployment_actions.h
Normal 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
|
||||
95
apps/studio/src/core/filesystem.cc
Normal file
95
apps/studio/src/core/filesystem.cc
Normal 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
|
||||
35
apps/studio/src/core/filesystem.h
Normal file
35
apps/studio/src/core/filesystem.h
Normal 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
|
||||
378
apps/studio/src/core/llama_client.cc
Normal file
378
apps/studio/src/core/llama_client.cc
Normal 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
|
||||
81
apps/studio/src/core/llama_client.h
Normal file
81
apps/studio/src/core/llama_client.h
Normal 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
|
||||
53
apps/studio/src/core/logger.cc
Normal file
53
apps/studio/src/core/logger.cc
Normal 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
|
||||
63
apps/studio/src/core/logger.h
Normal file
63
apps/studio/src/core/logger.h
Normal 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
|
||||
258
apps/studio/src/core/registry_reader.cc
Normal file
258
apps/studio/src/core/registry_reader.cc
Normal 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
|
||||
136
apps/studio/src/core/registry_reader.h
Normal file
136
apps/studio/src/core/registry_reader.h
Normal 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
|
||||
264
apps/studio/src/core/training_monitor.cc
Normal file
264
apps/studio/src/core/training_monitor.cc
Normal 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
|
||||
148
apps/studio/src/core/training_monitor.h
Normal file
148
apps/studio/src/core/training_monitor.h
Normal 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
|
||||
614
apps/studio/src/data_loader.cc
Normal file
614
apps/studio/src/data_loader.cc
Normal 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
|
||||
231
apps/studio/src/data_loader.h
Normal file
231
apps/studio/src/data_loader.h
Normal 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
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
41
apps/studio/src/main.cc
Normal 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();
|
||||
}
|
||||
257
apps/studio/src/models/state.h
Normal file
257
apps/studio/src/models/state.h
Normal 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
|
||||
125
apps/studio/src/themes/afs_theme.h
Normal file
125
apps/studio/src/themes/afs_theme.h
Normal 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
|
||||
25
apps/studio/src/ui/charts/chart.h
Normal file
25
apps/studio/src/ui/charts/chart.h
Normal 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
|
||||
85
apps/studio/src/ui/charts/coverage_density.cc
Normal file
85
apps/studio/src/ui/charts/coverage_density.cc
Normal 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
|
||||
16
apps/studio/src/ui/charts/coverage_density.h
Normal file
16
apps/studio/src/ui/charts/coverage_density.h
Normal 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
|
||||
97
apps/studio/src/ui/charts/generator_efficiency.cc
Normal file
97
apps/studio/src/ui/charts/generator_efficiency.cc
Normal 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
|
||||
16
apps/studio/src/ui/charts/generator_efficiency.h
Normal file
16
apps/studio/src/ui/charts/generator_efficiency.h
Normal 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
|
||||
118
apps/studio/src/ui/charts/quality_trends.cc
Normal file
118
apps/studio/src/ui/charts/quality_trends.cc
Normal 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
|
||||
14
apps/studio/src/ui/charts/quality_trends.h
Normal file
14
apps/studio/src/ui/charts/quality_trends.h
Normal 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
|
||||
1211
apps/studio/src/ui/components/charts.cc
Normal file
1211
apps/studio/src/ui/components/charts.cc
Normal file
File diff suppressed because it is too large
Load Diff
36
apps/studio/src/ui/components/charts.h
Normal file
36
apps/studio/src/ui/components/charts.h
Normal 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
|
||||
282
apps/studio/src/ui/components/companion_panels.cc
Normal file
282
apps/studio/src/ui/components/companion_panels.cc
Normal 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
|
||||
33
apps/studio/src/ui/components/companion_panels.h
Normal file
33
apps/studio/src/ui/components/companion_panels.h
Normal 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
|
||||
73
apps/studio/src/ui/components/comparison_view.cc
Normal file
73
apps/studio/src/ui/components/comparison_view.cc
Normal 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
|
||||
367
apps/studio/src/ui/components/deployment_panel.cc
Normal file
367
apps/studio/src/ui/components/deployment_panel.cc
Normal 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
|
||||
55
apps/studio/src/ui/components/deployment_panel.h
Normal file
55
apps/studio/src/ui/components/deployment_panel.h
Normal 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
|
||||
256
apps/studio/src/ui/components/graph_browser.cc
Normal file
256
apps/studio/src/ui/components/graph_browser.cc
Normal 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
|
||||
48
apps/studio/src/ui/components/graph_browser.h
Normal file
48
apps/studio/src/ui/components/graph_browser.h
Normal 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
|
||||
153
apps/studio/src/ui/components/graph_navigator.cc
Normal file
153
apps/studio/src/ui/components/graph_navigator.cc
Normal 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
|
||||
32
apps/studio/src/ui/components/graph_navigator.h
Normal file
32
apps/studio/src/ui/components/graph_navigator.h
Normal 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
|
||||
185
apps/studio/src/ui/components/metrics.cc
Normal file
185
apps/studio/src/ui/components/metrics.cc
Normal 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
|
||||
18
apps/studio/src/ui/components/metrics.h
Normal file
18
apps/studio/src/ui/components/metrics.h
Normal 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
|
||||
418
apps/studio/src/ui/components/model_registry.cc
Normal file
418
apps/studio/src/ui/components/model_registry.cc
Normal 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
|
||||
69
apps/studio/src/ui/components/model_registry.h
Normal file
69
apps/studio/src/ui/components/model_registry.h
Normal 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
|
||||
|
||||
1500
apps/studio/src/ui/components/panels.cc
Normal file
1500
apps/studio/src/ui/components/panels.cc
Normal file
File diff suppressed because it is too large
Load Diff
28
apps/studio/src/ui/components/panels.h
Normal file
28
apps/studio/src/ui/components/panels.h
Normal 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
|
||||
741
apps/studio/src/ui/components/tabs.cc
Normal file
741
apps/studio/src/ui/components/tabs.cc
Normal 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", ¤t_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
|
||||
33
apps/studio/src/ui/components/tabs.h
Normal file
33
apps/studio/src/ui/components/tabs.h
Normal 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
|
||||
275
apps/studio/src/ui/components/training_dashboard.cc
Normal file
275
apps/studio/src/ui/components/training_dashboard.cc
Normal 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
|
||||
42
apps/studio/src/ui/components/training_dashboard.h
Normal file
42
apps/studio/src/ui/components/training_dashboard.h
Normal 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
291
apps/studio/src/ui/core.cc
Normal 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
41
apps/studio/src/ui/core.h
Normal 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
|
||||
234
apps/studio/src/ui/panels/chat_panel.cc
Normal file
234
apps/studio/src/ui/panels/chat_panel.cc
Normal 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
|
||||
15
apps/studio/src/ui/panels/chat_panel.h
Normal file
15
apps/studio/src/ui/panels/chat_panel.h
Normal 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
|
||||
625
apps/studio/src/ui/shortcuts.cc
Normal file
625
apps/studio/src/ui/shortcuts.cc
Normal 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
|
||||
118
apps/studio/src/ui/shortcuts.h
Normal file
118
apps/studio/src/ui/shortcuts.h
Normal 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
|
||||
861
apps/studio/src/widgets/imgui_memory_editor.h
Normal file
861
apps/studio/src/widgets/imgui_memory_editor.h
Normal 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
|
||||
385
apps/studio/src/widgets/sample_review.cc
Normal file
385
apps/studio/src/widgets/sample_review.cc
Normal 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
|
||||
75
apps/studio/src/widgets/sample_review.h
Normal file
75
apps/studio/src/widgets/sample_review.h
Normal 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
|
||||
3849
apps/studio/src/widgets/text_editor.cc
Normal file
3849
apps/studio/src/widgets/text_editor.cc
Normal file
File diff suppressed because it is too large
Load Diff
391
apps/studio/src/widgets/text_editor.h
Normal file
391
apps/studio/src/widgets/text_editor.h
Normal 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
|
||||
352
apps/studio/src/widgets/training_status.cpp
Normal file
352
apps/studio/src/widgets/training_status.cpp
Normal 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
|
||||
93
apps/studio/src/widgets/training_status.h
Normal file
93
apps/studio/src/widgets/training_status.h
Normal 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
|
||||
@@ -1,7 +1,7 @@
|
||||
# STATUS
|
||||
|
||||
Stage: Prototype
|
||||
Now: init/status/workspace commands; context init/list/mount/validate/discover/ensure-all; graph export; minimal config + plugin discovery.
|
||||
Now: init/status/workspace commands; context init/list/mount/validate/discover/ensure-all; graph export; minimal config + plugin discovery; studio sources in apps/studio.
|
||||
Not yet: service runtime; full configuration schema validation.
|
||||
Next: one small utility; smoke-test stub.
|
||||
Issues: no runtime yet.
|
||||
|
||||
Reference in New Issue
Block a user