diff --git a/data.h b/data.h index abb9a72..90c0e48 100644 --- a/data.h +++ b/data.h @@ -93,6 +93,43 @@ struct ProjectRow { int vaults_count = 0; }; +// Test unitario asociado a una funcion (1:N — una funcion puede tener varios). +struct UnitTestRow { + std::string id; + std::string function_id; + std::string name; + std::string lang; + std::string file_path; + std::string code; + std::string created_at; +}; + +// Detalle completo de una funcion (codigo + documentacion). +// Cargado on-demand cuando el usuario selecciona una funcion en Explorer. +struct FunctionDetail { + std::string id; + std::string name; + std::string lang; + std::string domain; + std::string kind; + std::string purity; + std::string version; + std::string signature; + std::string description; + std::string code; + std::string documentation; + std::string notes; + std::string example; + std::string params_schema; + std::string uses_functions; + std::string uses_types; + std::string returns; + std::string error_type; + std::string file_path; + std::string created_at; + bool tested = false; +}; + struct ProjectDetail { std::string id; // "" si no hay seleccion; "orphans" para huerfanas std::string name; diff --git a/data_http.cpp b/data_http.cpp index 5df87c6..bee7e13 100644 --- a/data_http.cpp +++ b/data_http.cpp @@ -308,6 +308,130 @@ bool load_project_detail_http(const std::string& api_url, return true; } +// --------------------------------------------------------------------------- +// Function detail endpoints (Explorer) +// --------------------------------------------------------------------------- + +bool load_function_detail_http(const std::string& api_url, + const std::string& id, + FunctionDetail& out) { + std::string host; int port; + if (!parse_url(api_url, host, port)) return false; + + HttpClient cli(host, port); + + // /api/databases//query solo acepta { "sql": ... } sin args + // parametrizados (ver handleQuery en sqlite_api). Escapamos comilla + // simple para que un id con apostrofe no rompa la query — los IDs del + // registry son [a-z0-9_]+ asi que el escape es defensivo. + std::string escaped; + escaped.reserve(id.size()); + for (char c : id) { + if (c == '\'') escaped += "''"; + else escaped.push_back(c); + } + std::string sql = + "SELECT id, name, lang, domain, kind, purity, version, signature, " + "description, code, documentation, notes, example, params_schema, " + "uses_functions, uses_types, returns, error_type, file_path, " + "created_at, tested FROM functions WHERE id = '" + escaped + "'"; + + auto j = api_query(cli, sql.c_str()); + if (j.is_null() || !j.contains("rows") || j["rows"].empty()) return false; + + auto& row = j["rows"][0]; + out = FunctionDetail{}; + out.id = extract_str(row, 0); + out.name = extract_str(row, 1); + out.lang = extract_str(row, 2); + out.domain = extract_str(row, 3); + out.kind = extract_str(row, 4); + out.purity = extract_str(row, 5); + out.version = extract_str(row, 6); + out.signature = extract_str(row, 7); + out.description = extract_str(row, 8); + out.code = extract_str(row, 9); + out.documentation = extract_str(row, 10); + out.notes = extract_str(row, 11); + out.example = extract_str(row, 12); + out.params_schema = extract_str(row, 13); + out.uses_functions= extract_str(row, 14); + out.uses_types = extract_str(row, 15); + out.returns = extract_str(row, 16); + out.error_type = extract_str(row, 17); + out.file_path = extract_str(row, 18); + out.created_at = extract_str(row, 19); + out.tested = extract_row_int(row, 20) != 0; + return true; +} + +bool load_all_functions_http(const std::string& api_url, + std::vector& out) { + std::string host; int port; + if (!parse_url(api_url, host, port)) return false; + + HttpClient cli(host, port); + auto j = api_query(cli, + "SELECT id, name, lang, domain, kind, purity, description, " + "created_at, tested FROM functions ORDER BY name"); + if (j.is_null() || !j.contains("rows")) return false; + + out.clear(); + out.reserve(j["rows"].size()); + for (auto& row : j["rows"]) { + FunctionRow r; + r.id = extract_str(row, 0); + r.name = extract_str(row, 1); + r.lang = extract_str(row, 2); + r.domain = extract_str(row, 3); + r.kind = extract_str(row, 4); + r.purity = extract_str(row, 5); + r.description = extract_str(row, 6); + r.created_at = extract_str(row, 7); + r.tested = extract_row_int(row, 8) != 0; + out.push_back(std::move(r)); + } + return true; +} + +bool load_unit_tests_http(const std::string& api_url, + const std::string& function_id, + std::vector& out) { + std::string host; int port; + if (!parse_url(api_url, host, port)) return false; + + HttpClient cli(host, port); + + // Mismo escape defensivo que en load_function_detail_http — los IDs son + // [a-z0-9_]+ pero por consistencia escapamos comilla simple. + std::string escaped; + escaped.reserve(function_id.size()); + for (char c : function_id) { + if (c == '\'') escaped += "''"; + else escaped.push_back(c); + } + std::string sql = + "SELECT id, function_id, name, lang, file_path, code, created_at " + "FROM unit_tests WHERE function_id = '" + escaped + "' ORDER BY name"; + + auto j = api_query(cli, sql.c_str()); + if (j.is_null() || !j.contains("rows")) return false; + + out.clear(); + for (auto& row : j["rows"]) { + UnitTestRow r; + r.id = extract_str(row, 0); + r.function_id = extract_str(row, 1); + r.name = extract_str(row, 2); + r.lang = extract_str(row, 3); + r.file_path = extract_str(row, 4); + r.code = extract_str(row, 5); + r.created_at = extract_str(row, 6); + out.push_back(std::move(r)); + } + return true; +} + // --------------------------------------------------------------------------- // Mutation endpoints // --------------------------------------------------------------------------- diff --git a/data_http.h b/data_http.h index a96043d..2ed0b8f 100644 --- a/data_http.h +++ b/data_http.h @@ -18,6 +18,23 @@ bool load_project_detail_http(const std::string& api_url, const std::string& id, ProjectDetail& out); +// Load detalle completo de una funcion (codigo + documentacion + metadata). +// Usado por la pestana Explorer. +bool load_function_detail_http(const std::string& api_url, + const std::string& id, + FunctionDetail& out); + +// Load lista plana de funciones para el Explorer (id, name, lang, domain, +// kind, purity, description, tested) — todas las funciones, no solo recientes. +bool load_all_functions_http(const std::string& api_url, + std::vector& out); + +// Load tests unitarios asociados a una funcion (tabla unit_tests con FK +// function_id). Devuelve la lista vacia si la funcion no tiene tests. +bool load_unit_tests_http(const std::string& api_url, + const std::string& function_id, + std::vector& out); + // Operaciones de mutacion (thread-safe porque http_post ya lo es). // Devuelven el body de respuesta en `out_body`. true si HTTP status 2xx. bool http_post_reindex(const std::string& api_url, std::string& out_body); diff --git a/views.cpp b/views.cpp index ed278b3..6eb307f 100644 --- a/views.cpp +++ b/views.cpp @@ -26,8 +26,11 @@ #include "core/toast.h" #include "core/process_runner.h" #include "core/tree_view.h" +#include "core/selectable_text.h" #include +#include +#include #include #include @@ -60,8 +63,22 @@ static std::vector to_cstr(const std::vector& v) { return out; } +// Cache del Explorer: forward declarado aqui para que trigger_reload pueda +// invalidarlo (la lista completa de funciones se cachea hasta que el usuario +// dispara Reload o Reindex). +static std::vector g_explorer_funcs; +static bool g_explorer_loaded = false; +static std::string g_explorer_selected_id; +static FunctionDetail g_explorer_detail; +static std::vector g_explorer_tests; +static int g_explorer_test_idx = 0; +static char g_explorer_filter[128] = {}; +static int g_explorer_lang_idx = 0; +static int g_explorer_domain_idx = 0; + static void trigger_reload() { ImGui::GetIO().UserData = reinterpret_cast(1); + g_explorer_loaded = false; } // --------------------------------------------------------------------------- @@ -170,7 +187,7 @@ void draw_charts(RegistryData& data, float height) { const char* labels[] = {"Pure", "Impure"}; float values[] = {static_cast(data.stats.pure_functions), static_cast(data.stats.impure_functions)}; - pie_chart("##purity", labels, values, 2); + pie_chart("##purity", labels, values, 2, 0.0f, plot_h); chart_panel_end(); } ImGui::TableSetColumnIndex(3); @@ -180,7 +197,7 @@ void draw_charts(RegistryData& data, float height) { auto labels = to_cstr(data.kind_labels); if (!labels.empty()) pie_chart("##kind", labels.data(), data.kind_values.data(), - static_cast(labels.size())); + static_cast(labels.size()), 0.0f, plot_h); chart_panel_end(); } ImGui::EndTable(); @@ -590,6 +607,263 @@ static void draw_actions_bar() { last_reindex_state = now; } +// --------------------------------------------------------------------------- +// Functions Explorer +// --------------------------------------------------------------------------- +// Pestana con dos columnas: a la izquierda lista filtrable de funciones del +// registry; a la derecha codigo + documentacion + metadata de la funcion +// seleccionada. La lista se carga en el primer render (lazy) y se cachea — +// el boton "Reload" del toolbar fuerza recarga al disparar trigger_reload +// (que pone g_explorer_loaded = false). + +static const char* kExplorerLangs[] = {"all", "go", "py", "ts", "sh", "cpp"}; +static const char* kExplorerDomains[] = {"all", "core", "infra", "finance", + "datascience", "cybersecurity", + "shell", "tui", "pipelines", + "browser", "viz", "gfx", "notebook"}; + +static bool str_contains_ci(const std::string& haystack, const char* needle) { + if (!needle || !*needle) return true; + std::string h = haystack; + std::string n = needle; + for (auto& c : h) c = static_cast(std::tolower(static_cast(c))); + for (auto& c : n) c = static_cast(std::tolower(static_cast(c))); + return h.find(n) != std::string::npos; +} + +static void explorer_select(const std::string& id) { + g_explorer_selected_id = id; + g_explorer_detail = FunctionDetail{}; + g_explorer_tests.clear(); + g_explorer_test_idx = 0; + if (!g_api_url.empty() && !id.empty()) { + load_function_detail_http(g_api_url, id, g_explorer_detail); + load_unit_tests_http(g_api_url, id, g_explorer_tests); + } +} + +static void explorer_reload_list() { + g_explorer_funcs.clear(); + g_explorer_loaded = false; + if (g_api_url.empty()) return; + if (load_all_functions_http(g_api_url, g_explorer_funcs)) { + g_explorer_loaded = true; + } +} + +void draw_functions_explorer() { + if (!g_explorer_loaded) { + explorer_reload_list(); + } + + if (g_explorer_funcs.empty()) { + empty_state("( no data )", "No functions to explore", + "Run 'fn index' or check the API connection"); + return; + } + + const ImGuiTableFlags flags = ImGuiTableFlags_Resizable + | ImGuiTableFlags_SizingStretchProp; + if (!ImGui::BeginTable("##explorer_layout", 2, flags)) return; + + ImGui::TableSetupColumn("list", ImGuiTableColumnFlags_WidthStretch, 1.0f); + ImGui::TableSetupColumn("detail", ImGuiTableColumnFlags_WidthStretch, 2.4f); + ImGui::TableNextRow(); + + // --- Left: filter + list --- + ImGui::TableSetColumnIndex(0); + ImGui::BeginChild("##explorer_left", ImVec2(0, 0), ImGuiChildFlags_Borders); + + fn_ui::text_input("Search", g_explorer_filter, sizeof(g_explorer_filter), + "name or description"); + + ImGui::PushItemWidth(120.0f); + ImGui::Combo("##lang", &g_explorer_lang_idx, kExplorerLangs, + IM_ARRAYSIZE(kExplorerLangs)); + ImGui::SameLine(); + ImGui::Combo("##domain", &g_explorer_domain_idx, kExplorerDomains, + IM_ARRAYSIZE(kExplorerDomains)); + ImGui::PopItemWidth(); + ImGui::Spacing(); + + // Conteo de resultados visibles + lista + int visible = 0; + ImGui::BeginChild("##explorer_list", ImVec2(0, 0)); + for (const auto& f : g_explorer_funcs) { + if (g_explorer_lang_idx > 0 + && f.lang != kExplorerLangs[g_explorer_lang_idx]) continue; + if (g_explorer_domain_idx > 0 + && f.domain != kExplorerDomains[g_explorer_domain_idx]) continue; + if (g_explorer_filter[0] != '\0' + && !str_contains_ci(f.name, g_explorer_filter) + && !str_contains_ci(f.description, g_explorer_filter)) continue; + + visible++; + bool sel = (g_explorer_selected_id == f.id); + char label[256]; + std::snprintf(label, sizeof(label), "%s [%s/%s]", + f.name.c_str(), f.lang.c_str(), f.domain.c_str()); + if (ImGui::Selectable(label, sel, ImGuiSelectableFlags_AllowOverlap)) { + explorer_select(f.id); + } + } + ImGui::EndChild(); + (void)visible; + + ImGui::EndChild(); + + // --- Right: detail --- + ImGui::TableSetColumnIndex(1); + ImGui::BeginChild("##explorer_right", ImVec2(0, 0), ImGuiChildFlags_Borders); + + if (g_explorer_selected_id.empty()) { + empty_state("\xe2\x86\x90", "Select a function", + "Click a function on the left to see its code and documentation"); + } else { + const auto& d = g_explorer_detail; + + // Header: nombre + signature + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text); + ImGui::TextUnformatted(d.name.empty() + ? g_explorer_selected_id.c_str() : d.name.c_str()); + ImGui::PopStyleColor(); + + // Badges meta (lang/domain/kind/purity/tested) + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); + ImGui::Text("%s · %s · %s · %s%s", + d.lang.c_str(), d.domain.c_str(), + d.kind.c_str(), d.purity.c_str(), + d.tested ? " · tested" : ""); + ImGui::PopStyleColor(); + + if (!d.description.empty()) { + ImGui::Spacing(); + fn_ui::selectable_text_wrapped(d.description.c_str()); + } + + if (!d.signature.empty()) { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); + ImGui::TextUnformatted("Signature:"); + ImGui::PopStyleColor(); + fn_ui::selectable_text_wrapped(d.signature.c_str()); + } + + ImGui::Separator(); + + // Tabs Code / Documentation / Notes / Example + if (ImGui::BeginTabBar("##fn_detail_tabs")) { + if (ImGui::BeginTabItem("Code")) { + if (d.code.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim); + ImGui::TextUnformatted("(no code stored)"); + ImGui::PopStyleColor(); + } else { + // InputTextMultiline read-only — permite seleccion y copia, + // y maneja scroll horizontal/vertical para codigo largo. + ImVec2 sz = ImGui::GetContentRegionAvail(); + ImGui::InputTextMultiline("##code", + const_cast(d.code.c_str()), + d.code.size() + 1, sz, + ImGuiInputTextFlags_ReadOnly); + } + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Documentation")) { + if (d.documentation.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim); + ImGui::TextUnformatted("(no documentation)"); + ImGui::PopStyleColor(); + } else { + ImGui::BeginChild("##doc_scroll", ImVec2(0, 0)); + fn_ui::selectable_text_wrapped(d.documentation.c_str()); + ImGui::EndChild(); + } + ImGui::EndTabItem(); + } + // Tests: caja con dropdown si hay varios + visor del codigo del + // test seleccionado. Nombre del tab incluye el conteo para que se + // vea de un vistazo si la funcion tiene tests o no. + char tests_tab_label[32]; + std::snprintf(tests_tab_label, sizeof(tests_tab_label), + "Tests (%zu)", g_explorer_tests.size()); + if (ImGui::BeginTabItem(tests_tab_label)) { + if (g_explorer_tests.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim); + ImGui::TextUnformatted("(no unit tests for this function)"); + ImGui::PopStyleColor(); + } else { + // Combo para elegir entre los varios tests (la mayoria + // de funciones solo tienen 1, pero algunas tienen varios + // ficheros de tests). + if (g_explorer_test_idx >= (int)g_explorer_tests.size()) + g_explorer_test_idx = 0; + std::vector names; + names.reserve(g_explorer_tests.size()); + for (auto& t : g_explorer_tests) names.push_back(t.name); + std::vector name_cstr; + for (auto& n : names) name_cstr.push_back(n.c_str()); + + ImGui::PushItemWidth(-FLT_MIN); + ImGui::Combo("##test_select", &g_explorer_test_idx, + name_cstr.data(), + static_cast(name_cstr.size())); + ImGui::PopItemWidth(); + + const auto& t = g_explorer_tests[g_explorer_test_idx]; + + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); + ImGui::Text("%s · %s", t.lang.c_str(), + t.file_path.empty() ? "(no path)" : t.file_path.c_str()); + ImGui::PopStyleColor(); + ImGui::Separator(); + + if (t.code.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim); + ImGui::TextUnformatted("(no code stored for this test)"); + ImGui::PopStyleColor(); + } else { + ImVec2 sz = ImGui::GetContentRegionAvail(); + ImGui::InputTextMultiline("##test_code", + const_cast(t.code.c_str()), + t.code.size() + 1, sz, + ImGuiInputTextFlags_ReadOnly); + } + } + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Metadata")) { + ImGui::BeginChild("##meta_scroll", ImVec2(0, 0)); + auto kv = [](const char* k, const std::string& v) { + if (v.empty()) return; + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); + ImGui::TextUnformatted(k); + ImGui::PopStyleColor(); + fn_ui::selectable_text_wrapped(v.c_str()); + ImGui::Spacing(); + }; + kv("ID:", d.id); + kv("Version:", d.version); + kv("File path:", d.file_path); + kv("Created:", d.created_at); + kv("Returns:", d.returns); + kv("Error type:", d.error_type); + kv("Uses functions:", d.uses_functions); + kv("Uses types:", d.uses_types); + kv("Params schema:", d.params_schema); + kv("Example:", d.example); + kv("Notes:", d.notes); + ImGui::EndChild(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + } + ImGui::EndChild(); + + ImGui::EndTable(); +} + // --------------------------------------------------------------------------- // Main draw // --------------------------------------------------------------------------- @@ -613,26 +887,37 @@ void draw_dashboard(RegistryData& data) { draw_actions_bar(); ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm)); - draw_kpi_row(data); - ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md)); + // Navegacion top-level: cada tab ocupa toda la zona de contenido. El + // primero ("Dashboard") incluye los KPIs + charts + tabla de recientes; + // los demas son vistas dedicadas a su entidad. + if (ImGui::BeginTabBar("##main_tabs", ImGuiTabBarFlags_FittingPolicyScroll)) { + if (ImGui::BeginTabItem("Dashboard")) { + draw_kpi_row(data); + ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md)); - // Altura del bloque de charts proporcional al espacio disponible: ~28% del - // espacio restante despues de KPIs/header/tabs (clamped a [200, 360] px - // para que ni se aplaste ni domine en pantallas grandes). - const float remaining_h = ImGui::GetContentRegionAvail().y; - float chart_h = remaining_h * 0.32f; - if (chart_h < 200.0f) chart_h = 200.0f; - if (chart_h > 360.0f) chart_h = 360.0f; - draw_charts(data, chart_h); - ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md)); + // Altura del bloque de charts proporcional al espacio disponible: + // ~32% del espacio restante despues de KPIs (clamped a [200, 360]). + const float remaining_h = ImGui::GetContentRegionAvail().y; + float chart_h = remaining_h * 0.40f; + if (chart_h < 200.0f) chart_h = 200.0f; + if (chart_h > 360.0f) chart_h = 360.0f; + draw_charts(data, chart_h); + ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md)); - if (ImGui::BeginTabBar("##tables")) { - if (ImGui::BeginTabItem("Projects")) { - draw_projects_list(data); + // Tabla de funciones recientes para que el Dashboard tenga algo + // accionable abajo sin tener que cambiar de pestana. + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); + ImGui::TextUnformatted("Recent Functions"); + ImGui::PopStyleColor(); + draw_recent_functions(data.recent_funcs); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Recent Functions")) { - draw_recent_functions(data.recent_funcs); + if (ImGui::BeginTabItem("Explorer")) { + draw_functions_explorer(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Projects")) { + draw_projects_list(data); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Apps")) { diff --git a/views.h b/views.h index 125bd95..7d12994 100644 --- a/views.h +++ b/views.h @@ -18,3 +18,7 @@ void draw_apps_list(const std::vector& apps); void draw_analysis_list(const std::vector& analyses); void draw_types_list(const std::vector& types); void draw_projects_list(RegistryData& data); + +// Explorer: lista navegable de funciones + visor de codigo y documentacion. +// Carga la lista completa al primer render via /api/databases/registry/query. +void draw_functions_explorer();