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:
2026-05-13 00:50:35 +02:00
parent d782d463cb
commit 100aeaa1fc
12 changed files with 3040 additions and 291 deletions
+425 -25
View File
@@ -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)