Files
fn_registry/cpp/functions/viz/data_table_ai_panel.cpp
egutierrez b9716a7cd6 chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)
Snapshot de WIP acumulado de sesiones previas antes de merge wave 1
del flow 0008 (kanban_cpp + agent_runner_api + DoD schema).

Incluye:
- dev/flows/0008-kanban-cpp-and-agent-workflows.md
- dev/issues/0112-0119*.md (7 sub-issues)
- WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:17:08 +02:00

189 lines
7.3 KiB
C++

// data_table_ai_panel — modal "Ask AI" de la tabla TQL.
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
//
// Rangos de lineas del fuente original (antes de la extraccion):
// - Stub llm_anthropic (no-op) : lineas 82-112
// - Modal "Ask AI" completo : lineas 4209-4328
// - Boton trigger en viz selector: linea 1588
//
// NOTA DEUDA TECNICA: llm_anthropic no esta en el registry todavia (Wave 4 TODO).
// Pendiente promover a cpp/functions/infra/llm_anthropic (.h + .cpp + .md).
#include "viz/data_table_ai_panel.h"
#include "core/data_table_types.h"
#include "core/tql_apply.h"
#include "imgui.h"
// llm_anthropic: usar header real si disponible, stub si no.
#ifdef FN_LLM_ANTHROPIC
# include "core/llm_anthropic.h"
#else
// Stub no-op (inline en este TU hasta que llm_anthropic se promueva al registry).
// TODO: Wave 4 — promover a cpp/functions/infra/llm_anthropic.cpp + .h + .md
namespace llm_anthropic {
enum class OutputMode { TQL, SQL };
struct AskInput {
std::string question;
std::string tql_current;
std::vector<std::string> col_names;
std::vector<data_table::ColumnType> col_types;
std::vector<std::string> joinable_names;
OutputMode mode = OutputMode::TQL;
std::string model;
int max_tokens = 8192;
};
struct AskResult {
std::string code;
std::string raw;
std::string error;
int tokens_in = 0;
int tokens_out = 0;
};
inline AskResult ask(const AskInput&, const std::string& = "") {
AskResult r;
r.error = "llm_anthropic not available (stub). Build with FN_LLM_ANTHROPIC=1.";
return r;
}
} // namespace llm_anthropic
#endif
#include <string>
#include <vector>
#include <cstdio>
namespace data_table {
// ---------------------------------------------------------------------------
// draw_ask_ai_modal
// Dibuja el modal "Ask AI". Debe llamarse cada frame (patron ImGui).
// Abre el popup si ask_ai.open == true.
// ---------------------------------------------------------------------------
void draw_ask_ai_modal(AskAiState& ask_ai,
State& st,
const std::vector<std::string>& active_headers,
const std::vector<ColumnType>& active_types,
int orig_cols)
{
if (ask_ai.open) ImGui::OpenPopup("Ask AI");
ImGui::SetNextWindowSize(ImVec2(820, 560), ImGuiCond_Appearing);
if (ImGui::BeginPopupModal("Ask AI", &ask_ai.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 (ask_ai.mode == 1) ask_ai.mode = 0;
#endif
ImGui::Combo("Output##askmode", &ask_ai.mode, modes, IM_ARRAYSIZE(modes));
#ifndef FN_TQL_DUCKDB
if (ask_ai.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", ask_ai.question, sizeof(ask_ai.question),
ImVec2(-1, 80));
ImGui::BeginDisabled(ask_ai.busy);
if (ImGui::Button("Send")) {
ask_ai.busy = true;
ask_ai.status = "Sending...";
ask_ai.error.clear();
ask_ai.response_code.clear();
ask_ai.response_raw.clear();
// Build AskInput desde el state actual.
llm_anthropic::AskInput in;
in.question = ask_ai.question;
in.tql_current = ask_ai.current_tql;
in.col_names = active_headers;
in.col_types = active_types;
in.mode = (ask_ai.mode == 1)
? llm_anthropic::OutputMode::SQL
: llm_anthropic::OutputMode::TQL;
// Llamada blocking (UI freeze breve durante red).
auto r = llm_anthropic::ask(in);
ask_ai.busy = false;
if (!r.error.empty()) {
ask_ai.error = r.error;
ask_ai.status = "Error";
} else {
ask_ai.response_raw = r.raw;
ask_ai.response_code = r.code;
ask_ai.status = "Got response.";
// Llenar edit buffer
std::snprintf(ask_ai.edit_buf, sizeof(ask_ai.edit_buf),
"%s", r.code.c_str());
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (!ask_ai.status.empty()) {
ImGui::TextDisabled("%s", ask_ai.status.c_str());
}
if (!ask_ai.error.empty()) {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", ask_ai.error.c_str());
}
ImGui::Separator();
ImGui::Columns(2, "ask_cols", true);
ImGui::TextUnformatted("Current");
ImGui::InputTextMultiline("##ask_cur",
const_cast<char*>(ask_ai.current_tql.c_str()),
ask_ai.current_tql.size() + 1,
ImVec2(-1, 240),
ImGuiInputTextFlags_ReadOnly);
ImGui::NextColumn();
ImGui::TextUnformatted("Proposed (editable before apply)");
ImGui::InputTextMultiline("##ask_new", ask_ai.edit_buf, sizeof(ask_ai.edit_buf),
ImVec2(-1, 240));
ImGui::Columns(1);
bool can_apply = !ask_ai.busy && ask_ai.edit_buf[0] != '\0';
ImGui::BeginDisabled(!can_apply);
if (ImGui::Button("Apply")) {
std::string err;
if (ask_ai.mode == 0) {
// TQL apply
bool ok = tql::apply(ask_ai.edit_buf, st,
active_headers,
active_types,
nullptr, 0,
orig_cols,
&err);
if (ok) {
ask_ai.status = "Applied OK.";
ask_ai.open = false;
} else {
ask_ai.error = "tql::apply error: " + err;
ask_ai.status = "Apply failed.";
}
} else {
#ifdef FN_TQL_DUCKDB
// SQL apply: ejecutar via tql_duckdb sobre TableInputs activas.
// Para tablas en memoria construimos un TableInput basico desde
// active_headers/types. v1 no recupera cells originales aqui;
// reportamos solo error si fallo. Caller real deberia pasar
// tables() del render scope. Sin esto, marcamos status info.
ask_ai.status = "SQL execute disponible (FN_TQL_DUCKDB ON). "
"Integracion full pendiente: usar tql_duckdb::execute desde caller.";
#else
ask_ai.status = "SQL execute requires FN_TQL_DUCKDB build flag.";
#endif
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Reject")) {
ask_ai.response_code.clear();
ask_ai.edit_buf[0] = '\0';
}
ImGui::SameLine();
if (ImGui::Button("Close")) {
ask_ai.open = false;
}
ImGui::EndPopup();
}
}
} // namespace data_table