chore: auto-commit (12 archivos)
- playground/tables/CMakeLists.txt - playground/tables/data_table.cpp - playground/tables/data_table_logic.cpp - playground/tables/data_table_logic.h - playground/tables/self_test.cpp - playground/tables/tql.cpp - playground/tables/viz.cpp - playground/tables/viz.h - playground/tables/llm_anthropic.cpp - playground/tables/llm_anthropic.h - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,33 @@
|
||||
#include "data_table.h"
|
||||
#include "app_base.h"
|
||||
#include "imgui.h"
|
||||
#include "llm_anthropic.h"
|
||||
#include "lua_engine.h"
|
||||
#include "tql.h"
|
||||
#include "tql_to_sql.h"
|
||||
#include "viz.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// UTC date today as ISO YYYY-MM-DD. Para preset filtros Last7/30/90d.
|
||||
static std::string today_iso() {
|
||||
std::time_t t = std::time(nullptr);
|
||||
std::tm tm = *std::gmtime(&t);
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
|
||||
return buf;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -122,10 +135,106 @@ struct UiState {
|
||||
|
||||
// Toggle Table <-> View: remember last non-table display.
|
||||
ViewMode last_non_table_main = ViewMode::Bar;
|
||||
|
||||
// Drill history (fase 10). Stacks per-app; no persistido en TQL.
|
||||
std::vector<DrillStep> drill_back;
|
||||
std::vector<DrillStep> drill_forward;
|
||||
|
||||
// Row inspector (fase 10). -1 cerrado, sino row idx en el output del stage activo.
|
||||
int inspect_row = -1;
|
||||
bool inspect_open = false;
|
||||
|
||||
// Ask AI modal (fase 11 — issue 0080).
|
||||
bool ask_open = false;
|
||||
bool ask_busy = false;
|
||||
int ask_mode = 0; // 0 = TQL, 1 = SQL
|
||||
char ask_question[2048] = {0};
|
||||
std::string ask_current_tql; // emit del state actual al abrir modal
|
||||
std::string ask_response_raw; // texto del modelo
|
||||
std::string ask_response_code; // bloque extraido (Lua o SQL)
|
||||
std::string ask_error;
|
||||
std::string ask_status; // "Sent. Waiting..." / "OK" / error
|
||||
char ask_edit_buf[8192] = {0}; // buffer editable de propuesta
|
||||
};
|
||||
|
||||
UiState& ui() { static UiState s; return s; }
|
||||
|
||||
// Row inspector modal (fase 10). Muestra todas cols + valores de la fila
|
||||
// inspect_row del output del stage activo. Read-only + Copy TSV + Filter
|
||||
// by this row (anade filters al stage previo si existe).
|
||||
static void draw_row_inspector_modal(State& st, int active,
|
||||
const char* const* cells, int rows, int cols,
|
||||
const std::vector<std::string>& headers,
|
||||
const std::vector<ColumnType>& types,
|
||||
const std::vector<std::string>& prev_input_headers) {
|
||||
auto& U = ui();
|
||||
if (!U.inspect_open) return;
|
||||
if (U.inspect_row < 0 || U.inspect_row >= rows) {
|
||||
U.inspect_open = false;
|
||||
return;
|
||||
}
|
||||
ImGui::OpenPopup("##row_inspector");
|
||||
ImGui::SetNextWindowSize(ImVec2(560, 400), ImGuiCond_Appearing);
|
||||
if (ImGui::BeginPopupModal("##row_inspector", &U.inspect_open,
|
||||
ImGuiWindowFlags_NoSavedSettings)) {
|
||||
ImGui::Text("Row %d", U.inspect_row);
|
||||
ImGui::SameLine(0, 20);
|
||||
if (ImGui::SmallButton("Copy TSV")) {
|
||||
std::string tsv = row_to_tsv(cells, rows, cols, U.inspect_row, headers);
|
||||
ImGui::SetClipboardText(tsv.c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
bool can_filter = (active > 0 && !prev_input_headers.empty());
|
||||
ImGui::BeginDisabled(!can_filter);
|
||||
if (ImGui::SmallButton("Filter prev stage by this row")) {
|
||||
int target = active - 1;
|
||||
for (int c = 0; c < cols; ++c) {
|
||||
const char* v = cells[U.inspect_row * cols + c];
|
||||
if (!v || !*v) continue;
|
||||
const std::string& h = headers[c];
|
||||
std::string h_clean;
|
||||
parse_breakout_granularity(h, h_clean);
|
||||
int ci = -1;
|
||||
for (size_t i = 0; i < prev_input_headers.size(); ++i) {
|
||||
if (prev_input_headers[i] == h_clean) { ci = (int)i; break; }
|
||||
}
|
||||
if (ci < 0) continue;
|
||||
DrillStep step;
|
||||
step.target_stage = target;
|
||||
step.filter_pos = (int)st.stages[target].filters.size();
|
||||
step.prev_active_stage = st.active_stage;
|
||||
step.added = make_drill_filter(ci, v);
|
||||
if (apply_drill_step(st, step)) {
|
||||
U.drill_back.push_back(step);
|
||||
}
|
||||
}
|
||||
U.drill_forward.clear();
|
||||
U.inspect_open = false;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Separator();
|
||||
ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg
|
||||
| ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable;
|
||||
if (ImGui::BeginTable("##inspector_tbl", 2, flags, ImVec2(-1, -1))) {
|
||||
ImGui::TableSetupColumn("col");
|
||||
ImGui::TableSetupColumn("value");
|
||||
ImGui::TableHeadersRow();
|
||||
for (int c = 0; c < cols; ++c) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ColumnType t = (c < (int)types.size()) ? types[c] : ColumnType::String;
|
||||
ImGui::Text("%s %s", column_type_icon(t),
|
||||
(c < (int)headers.size()) ? headers[c].c_str() : "?");
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
const char* v = cells[U.inspect_row * cols + c];
|
||||
ImGui::TextWrapped("%s", v ? v : "");
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
int autocomplete_cb(ImGuiInputTextCallbackData* data) {
|
||||
UiState* U = (UiState*)data->UserData;
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackAlways) {
|
||||
@@ -180,6 +289,47 @@ void ensure_init(State& st, int eff_cols) {
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_stage_breadcrumb(State& st) {
|
||||
st.ensure_stage0();
|
||||
|
||||
// Drill history back/forward (fase 10). Botones al inicio.
|
||||
auto& U = ui();
|
||||
{
|
||||
bool can_back = !U.drill_back.empty();
|
||||
ImGui::BeginDisabled(!can_back);
|
||||
if (ImGui::SmallButton("<##drill_back")) {
|
||||
DrillStep s = U.drill_back.back();
|
||||
U.drill_back.pop_back();
|
||||
if (undo_drill_step(st, s)) {
|
||||
U.drill_forward.push_back(s);
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (can_back && ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Drill back (%zu)", U.drill_back.size());
|
||||
ImGui::SameLine();
|
||||
bool can_fwd = !U.drill_forward.empty();
|
||||
ImGui::BeginDisabled(!can_fwd);
|
||||
if (ImGui::SmallButton(">##drill_fwd")) {
|
||||
DrillStep s = U.drill_forward.back();
|
||||
U.drill_forward.pop_back();
|
||||
if (apply_drill_step(st, s)) {
|
||||
U.drill_back.push_back(s);
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (can_fwd && ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Drill forward (%zu)", U.drill_forward.size());
|
||||
ImGui::SameLine();
|
||||
bool can_up = (st.active_stage > 0);
|
||||
ImGui::BeginDisabled(!can_up);
|
||||
if (ImGui::SmallButton("^##drill_up")) drill_up(st);
|
||||
ImGui::EndDisabled();
|
||||
if (can_up && ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Drill up (stage previo, sin perder filters)");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("|");
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
for (int si = 0; si < (int)st.stages.size(); ++si) {
|
||||
if (si > 0) { ImGui::SameLine(); ImGui::TextDisabled(">"); ImGui::SameLine(); }
|
||||
|
||||
@@ -610,6 +760,19 @@ void draw_viz_selector(State& st) {
|
||||
ImGui::OpenPopup("##viz_cfg_popup");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Ask AI##ask_open")) {
|
||||
auto& U2 = ui();
|
||||
U2.ask_open = true;
|
||||
U2.ask_busy = false;
|
||||
U2.ask_error.clear();
|
||||
U2.ask_status.clear();
|
||||
U2.ask_response_code.clear();
|
||||
U2.ask_response_raw.clear();
|
||||
U2.ask_current_tql = tql::emit(st,
|
||||
std::vector<std::string>(), // emit headers stage 0 (caller fill si necesario)
|
||||
std::vector<ColumnType>());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ Viz##viz_add")) {
|
||||
VizPanel p;
|
||||
p.display = ViewMode::Bar;
|
||||
@@ -737,7 +900,8 @@ void draw_joins_chips(State& st, const std::vector<TableInput>& joinables,
|
||||
// Filter chips para el stage activo. eff_headers/eff_cols son del INPUT del
|
||||
// stage activo (= orig+derived para stage 0; output del stage previo para 1+).
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols) {
|
||||
void draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols,
|
||||
const std::vector<ColumnType>& eff_types) {
|
||||
auto& U = ui();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(120, 60, 170, 220));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(150, 85, 200, 240));
|
||||
@@ -746,6 +910,50 @@ void draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols)
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::SameLine();
|
||||
|
||||
// Presets (fase 10): menu con Last7/30/90d (cols Date), ExcludeNulls (any),
|
||||
// NonZero (cols numericas). Apply append a stg.filters via build_preset_filters.
|
||||
if (ImGui::SmallButton("Presets##fpresets")) ImGui::OpenPopup("##presets_menu");
|
||||
if (ImGui::BeginPopup("##presets_menu")) {
|
||||
int first_date = -1, first_num = -1;
|
||||
for (int c = 0; c < eff_cols && c < (int)eff_types.size(); ++c) {
|
||||
if (first_date < 0 && eff_types[c] == ColumnType::Date) first_date = c;
|
||||
if (first_num < 0 && (eff_types[c] == ColumnType::Int ||
|
||||
eff_types[c] == ColumnType::Float)) first_num = c;
|
||||
}
|
||||
auto apply_preset = [&](FilterPreset p, int col) {
|
||||
auto fs = build_preset_filters(p, col, today_iso());
|
||||
for (auto& f : fs) stg.filters.push_back(f);
|
||||
};
|
||||
if (first_date >= 0) {
|
||||
char l1[96], l2[96], l3[96];
|
||||
std::snprintf(l1, sizeof(l1), "Last 7 days on \"%s\"", eff_headers[first_date]);
|
||||
std::snprintf(l2, sizeof(l2), "Last 30 days on \"%s\"", eff_headers[first_date]);
|
||||
std::snprintf(l3, sizeof(l3), "Last 90 days on \"%s\"", eff_headers[first_date]);
|
||||
if (ImGui::MenuItem(l1)) apply_preset(FilterPreset::Last7d, first_date);
|
||||
if (ImGui::MenuItem(l2)) apply_preset(FilterPreset::Last30d, first_date);
|
||||
if (ImGui::MenuItem(l3)) apply_preset(FilterPreset::Last90d, first_date);
|
||||
ImGui::Separator();
|
||||
}
|
||||
if (ImGui::BeginMenu("Exclude nulls in...")) {
|
||||
for (int c = 0; c < eff_cols; ++c) {
|
||||
if (ImGui::MenuItem(eff_headers[c])) apply_preset(FilterPreset::ExcludeNulls, c);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (first_num >= 0) {
|
||||
if (ImGui::BeginMenu("Non-zero in...")) {
|
||||
for (int c = 0; c < eff_cols && c < (int)eff_types.size(); ++c) {
|
||||
if (eff_types[c] == ColumnType::Int || eff_types[c] == ColumnType::Float) {
|
||||
if (ImGui::MenuItem(eff_headers[c])) apply_preset(FilterPreset::NonZero, c);
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
if (stg.filters.empty()) {
|
||||
ImGui::TextDisabled("Sin filtros.");
|
||||
return;
|
||||
@@ -778,7 +986,8 @@ void draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols)
|
||||
}
|
||||
|
||||
// Chips de breakout (stage > 0).
|
||||
void draw_breakout_chips(Stage& stg, const char* const* in_headers, int in_cols) {
|
||||
void draw_breakout_chips(Stage& stg, const char* const* in_headers, int in_cols,
|
||||
const std::vector<ColumnType>& in_types) {
|
||||
auto& U = ui();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 60, 160, 170, 220));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 80, 190, 200, 240));
|
||||
@@ -792,6 +1001,17 @@ void draw_breakout_chips(Stage& stg, const char* const* in_headers, int in_cols)
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < stg.breakouts.size(); ) {
|
||||
std::string col_name;
|
||||
DateGranularity g = parse_breakout_granularity(stg.breakouts[i], col_name);
|
||||
|
||||
// Resolve col index para lookup de tipo.
|
||||
int col_idx = -1;
|
||||
for (int c = 0; c < in_cols; ++c) {
|
||||
if (std::strcmp(in_headers[c], col_name.c_str()) == 0) { col_idx = c; break; }
|
||||
}
|
||||
bool is_date_col = (col_idx >= 0 && col_idx < (int)in_types.size()
|
||||
&& in_types[col_idx] == ColumnType::Date);
|
||||
|
||||
char buf[256];
|
||||
std::snprintf(buf, sizeof(buf), "%s x##bk%zu", stg.breakouts[i].c_str(), i);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 60, 160, 170, 220));
|
||||
@@ -802,20 +1022,42 @@ void draw_breakout_chips(Stage& stg, const char* const* in_headers, int in_cols)
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||
U.edit_chip_kind = 2;
|
||||
U.edit_chip_idx = (int)i;
|
||||
// resolve current col name to index in in_headers
|
||||
U.edit_col_idx = 0;
|
||||
for (int c = 0; c < in_cols; ++c) {
|
||||
if (std::strcmp(in_headers[c], stg.breakouts[i].c_str()) == 0) {
|
||||
U.edit_col_idx = c; break;
|
||||
}
|
||||
}
|
||||
U.edit_col_idx = (col_idx >= 0) ? col_idx : 0;
|
||||
ImGui::OpenPopup("##edit_breakout");
|
||||
}
|
||||
if (clicked) { stg.breakouts.erase(stg.breakouts.begin() + i); continue; }
|
||||
|
||||
// Granularity combo inline cuando col Date (fase 10).
|
||||
if (is_date_col) {
|
||||
ImGui::SameLine();
|
||||
const char* preview = (g == DateGranularity::None)
|
||||
? "(raw)" : date_granularity_token(g);
|
||||
char combo_id[32];
|
||||
std::snprintf(combo_id, sizeof(combo_id), "##gran%zu", i);
|
||||
ImGui::SetNextItemWidth(72);
|
||||
if (ImGui::BeginCombo(combo_id, preview)) {
|
||||
DateGranularity opts[] = {
|
||||
DateGranularity::None,
|
||||
DateGranularity::Year,
|
||||
DateGranularity::Month,
|
||||
DateGranularity::Week,
|
||||
DateGranularity::Day,
|
||||
DateGranularity::Hour,
|
||||
};
|
||||
for (auto o : opts) {
|
||||
const char* lbl = (o == DateGranularity::None)
|
||||
? "(raw)" : date_granularity_token(o);
|
||||
if (ImGui::Selectable(lbl, o == g)) {
|
||||
stg.breakouts[i] = compose_breakout(col_name, o);
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
++i;
|
||||
}
|
||||
(void)in_headers; (void)in_cols;
|
||||
ImGui::NewLine();
|
||||
}
|
||||
|
||||
@@ -1220,7 +1462,8 @@ void draw_add_filter_popup(Stage& stg, const char* const* eff_headers_arr, int e
|
||||
}
|
||||
|
||||
void draw_add_breakout_popup(Stage& stg, const char* const* in_headers, int in_cols,
|
||||
const std::vector<ColumnType>& in_types) {
|
||||
const std::vector<ColumnType>& in_types,
|
||||
const char* const* in_cells, int in_rows) {
|
||||
auto& U = ui();
|
||||
if (!ImGui::BeginPopup("##addbreakout")) return;
|
||||
if (U.brk_picker_col < 0 || U.brk_picker_col >= in_cols) U.brk_picker_col = 0;
|
||||
@@ -1236,7 +1479,18 @@ void draw_add_breakout_popup(Stage& stg, const char* const* in_headers, int in_c
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::Button("Add##bk")) {
|
||||
stg.breakouts.emplace_back(in_headers[U.brk_picker_col]);
|
||||
int c = U.brk_picker_col;
|
||||
std::string col = in_headers[c];
|
||||
// Fase 10: si col es Date, auto-detect granularidad via rango lexical
|
||||
// (ISO YYYY-MM-DD ordena bien). Default Day si rango invalido.
|
||||
if (c >= 0 && c < (int)in_types.size() && in_types[c] == ColumnType::Date) {
|
||||
std::string lo, hi;
|
||||
column_min_max(in_cells, in_rows, in_cols, c, lo, hi);
|
||||
DateGranularity g = auto_date_granularity(lo, hi);
|
||||
stg.breakouts.emplace_back(compose_breakout(col, g));
|
||||
} else {
|
||||
stg.breakouts.emplace_back(col);
|
||||
}
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
@@ -1441,8 +1695,17 @@ void drill_into(State& st, int from_stage,
|
||||
if (prev_input_headers[i] == col_name) { ci = (int)i; break; }
|
||||
}
|
||||
if (ci < 0) return;
|
||||
st.stages[target].filters.push_back(make_drill_filter(ci, value));
|
||||
st.active_stage = target;
|
||||
|
||||
// Fase 10: graba step en drill_back, limpia forward (rama nueva).
|
||||
DrillStep step;
|
||||
step.target_stage = target;
|
||||
step.filter_pos = (int)st.stages[target].filters.size();
|
||||
step.prev_active_stage = st.active_stage;
|
||||
step.added = make_drill_filter(ci, value);
|
||||
apply_drill_step(st, step);
|
||||
auto& U = ui();
|
||||
U.drill_back.push_back(step);
|
||||
U.drill_forward.clear();
|
||||
}
|
||||
|
||||
} // anon namespace
|
||||
@@ -1659,7 +1922,7 @@ void render(const char* id,
|
||||
draw_joins_chips(st, *joinables, mh);
|
||||
}
|
||||
|
||||
draw_filter_chips(act, eff_headers.data(), eff_cols);
|
||||
draw_filter_chips(act, eff_headers.data(), eff_cols, eff_types);
|
||||
draw_add_filter_popup(act, eff_headers.data(), eff_cols, eff_types);
|
||||
draw_edit_filter_popup(act, eff_headers.data(), eff_cols, eff_types);
|
||||
|
||||
@@ -2290,12 +2553,13 @@ void render(const char* id,
|
||||
|
||||
if (chrome_visible) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
|
||||
draw_filter_chips(act, ih_ptrs.data(), in_cols_n);
|
||||
draw_filter_chips(act, ih_ptrs.data(), in_cols_n, input_types_active);
|
||||
draw_add_filter_popup(act, ih_ptrs.data(), in_cols_n, input_types_active);
|
||||
draw_edit_filter_popup(act, ih_ptrs.data(), in_cols_n, input_types_active);
|
||||
|
||||
draw_breakout_chips(act, ih_ptrs.data(), in_cols_n);
|
||||
draw_add_breakout_popup(act, ih_ptrs.data(), in_cols_n, input_types_active);
|
||||
draw_breakout_chips(act, ih_ptrs.data(), in_cols_n, input_types_active);
|
||||
draw_add_breakout_popup(act, ih_ptrs.data(), in_cols_n, input_types_active,
|
||||
cur_cells, cur_rows);
|
||||
draw_edit_breakout_popup(act, ih_ptrs.data(), in_cols_n);
|
||||
|
||||
draw_aggregation_chips(act, ih_ptrs.data(), in_cols_n);
|
||||
@@ -2524,7 +2788,22 @@ void render(const char* id,
|
||||
so_local.cells.push_back(cur_cells[i]);
|
||||
so_ptr = &so_local;
|
||||
}
|
||||
viz::render(*so_ptr, st.display, st.viz_config, ImVec2(-1, -1));
|
||||
int clicked_row = -1;
|
||||
viz::render(*so_ptr, st.display, st.viz_config, ImVec2(-1, -1), &clicked_row);
|
||||
// Fase 10: click sobre chart -> drill al stage previo usando
|
||||
// breakout col[0] como filtro Op::Eq sobre cells[clicked_row].
|
||||
if (clicked_row >= 0 && active > 0 &&
|
||||
so_ptr->cols > 0 && clicked_row < so_ptr->rows) {
|
||||
int n_brk = (int)st.stages[active].breakouts.size();
|
||||
if (n_brk > 0) {
|
||||
const char* v = so_ptr->cells[clicked_row * so_ptr->cols + 0];
|
||||
std::string col_clean;
|
||||
parse_breakout_granularity(so_ptr->headers[0], col_clean);
|
||||
drill_into(st, active, col_clean,
|
||||
v ? std::string(v) : "",
|
||||
input_headers_active);
|
||||
}
|
||||
}
|
||||
goto stage_n_table_end;
|
||||
}
|
||||
|
||||
@@ -2613,12 +2892,10 @@ void render(const char* id,
|
||||
ImGui::PushID(r * cur_cols_n + c);
|
||||
ImGui::Selectable(cell ? cell : "");
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||
// Drill-down solo si c es col de breakout (c < n_brk).
|
||||
if (c < n_brk) {
|
||||
U.pending_col = c;
|
||||
U.pending_value = cell ? cell : "";
|
||||
ImGui::OpenPopup("##drill_popup");
|
||||
}
|
||||
U.pending_col = c;
|
||||
U.pending_value = cell ? cell : "";
|
||||
U.inspect_row = r;
|
||||
ImGui::OpenPopup("##drill_popup");
|
||||
}
|
||||
if (ImGui::BeginPopup("##drill_popup")) {
|
||||
if (c < n_brk) {
|
||||
@@ -2631,6 +2908,12 @@ void render(const char* id,
|
||||
input_headers_active);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
if (ImGui::MenuItem("Inspect row...")) {
|
||||
U.inspect_row = r;
|
||||
U.inspect_open = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
@@ -2642,6 +2925,11 @@ void render(const char* id,
|
||||
}
|
||||
stage_n_table_end:;
|
||||
|
||||
// Row inspector modal (fase 10). Activado via right-click "Inspect row..."
|
||||
// sobre celdas del table del stage activo. `cur_cells` ya es row-major.
|
||||
draw_row_inspector_modal(st, active, cur_cells, cur_rows, cur_cols_n,
|
||||
cur_headers, cur_types, input_headers_active);
|
||||
|
||||
// Render extras (stage>0 path)
|
||||
if (!st.extra_panels.empty() && cur_cols_n > 0) {
|
||||
StageOutput so_local;
|
||||
@@ -2958,6 +3246,118 @@ void render(const char* id,
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// Ask AI modal (fase 11 — issue 0080).
|
||||
if (U.ask_open) ImGui::OpenPopup("Ask AI");
|
||||
ImGui::SetNextWindowSize(ImVec2(820, 560), ImGuiCond_Appearing);
|
||||
if (ImGui::BeginPopupModal("Ask AI", &U.ask_open,
|
||||
ImGuiWindowFlags_NoSavedSettings)) {
|
||||
ImGui::TextDisabled("Ask en lenguaje natural. Default TQL. SQL solo si DuckDB linkado.");
|
||||
const char* modes[] = {"TQL", "SQL (DuckDB)"};
|
||||
#ifndef FN_TQL_DUCKDB
|
||||
// SQL mode disabled visually pero el toggle existe (informativo)
|
||||
if (U.ask_mode == 1) U.ask_mode = 0;
|
||||
#endif
|
||||
ImGui::Combo("Output##askmode", &U.ask_mode, modes, IM_ARRAYSIZE(modes));
|
||||
#ifndef FN_TQL_DUCKDB
|
||||
if (U.ask_mode == 1) {
|
||||
ImGui::TextColored(ImVec4(1, 0.5f, 0.3f, 1),
|
||||
"SQL mode requires FN_TQL_DUCKDB=1 build flag.");
|
||||
}
|
||||
#endif
|
||||
ImGui::InputTextMultiline("##ask_q", U.ask_question, sizeof(U.ask_question),
|
||||
ImVec2(-1, 80));
|
||||
ImGui::BeginDisabled(U.ask_busy);
|
||||
if (ImGui::Button("Send")) {
|
||||
U.ask_busy = true;
|
||||
U.ask_status = "Sending...";
|
||||
U.ask_error.clear();
|
||||
U.ask_response_code.clear();
|
||||
U.ask_response_raw.clear();
|
||||
|
||||
// Build AskInput desde el state actual.
|
||||
llm_anthropic::AskInput in;
|
||||
in.question = U.ask_question;
|
||||
in.tql_current = U.ask_current_tql;
|
||||
in.col_names = U.active_headers;
|
||||
in.col_types = U.active_types;
|
||||
in.mode = (U.ask_mode == 1)
|
||||
? llm_anthropic::OutputMode::SQL
|
||||
: llm_anthropic::OutputMode::TQL;
|
||||
|
||||
// Llamada blocking (UI freeze breve durante red).
|
||||
auto r = llm_anthropic::ask(in);
|
||||
U.ask_busy = false;
|
||||
if (!r.error.empty()) {
|
||||
U.ask_error = r.error;
|
||||
U.ask_status = "Error";
|
||||
} else {
|
||||
U.ask_response_raw = r.raw;
|
||||
U.ask_response_code = r.code;
|
||||
U.ask_status = "Got response.";
|
||||
// Llenar edit buffer
|
||||
std::snprintf(U.ask_edit_buf, sizeof(U.ask_edit_buf),
|
||||
"%s", r.code.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (!U.ask_status.empty()) {
|
||||
ImGui::TextDisabled("%s", U.ask_status.c_str());
|
||||
}
|
||||
if (!U.ask_error.empty()) {
|
||||
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", U.ask_error.c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::Columns(2, "ask_cols", true);
|
||||
ImGui::TextUnformatted("Current");
|
||||
ImGui::InputTextMultiline("##ask_cur",
|
||||
const_cast<char*>(U.ask_current_tql.c_str()),
|
||||
U.ask_current_tql.size() + 1,
|
||||
ImVec2(-1, 240),
|
||||
ImGuiInputTextFlags_ReadOnly);
|
||||
ImGui::NextColumn();
|
||||
ImGui::TextUnformatted("Proposed (editable before apply)");
|
||||
ImGui::InputTextMultiline("##ask_new", U.ask_edit_buf, sizeof(U.ask_edit_buf),
|
||||
ImVec2(-1, 240));
|
||||
ImGui::Columns(1);
|
||||
|
||||
bool can_apply = !U.ask_busy && U.ask_edit_buf[0] != '\0';
|
||||
ImGui::BeginDisabled(!can_apply);
|
||||
if (ImGui::Button("Apply")) {
|
||||
std::string err;
|
||||
if (U.ask_mode == 0) {
|
||||
// TQL apply
|
||||
bool ok = tql::apply(U.ask_edit_buf, st,
|
||||
U.active_headers,
|
||||
U.active_types,
|
||||
nullptr, 0,
|
||||
(int)U.active_headers.size(),
|
||||
&err);
|
||||
if (ok) {
|
||||
U.ask_status = "Applied OK.";
|
||||
U.ask_open = false;
|
||||
} else {
|
||||
U.ask_error = "tql::apply error: " + err;
|
||||
U.ask_status = "Apply failed.";
|
||||
}
|
||||
} else {
|
||||
// SQL apply: requires DuckDB adapter (no v1).
|
||||
U.ask_status = "SQL execute requires FN_TQL_DUCKDB build flag.";
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reject")) {
|
||||
U.ask_response_code.clear();
|
||||
U.ask_edit_buf[0] = '\0';
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Close")) {
|
||||
U.ask_open = false;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (U.open_cell_popup) { ImGui::OpenPopup("##cell_op"); U.open_cell_popup = false; }
|
||||
if (ImGui::BeginPopup("##cell_op")) {
|
||||
ColumnType t = (U.pending_col >= 0 && U.pending_col < eff_cols)
|
||||
|
||||
Reference in New Issue
Block a user