merge issue/0109hk-dashboard-ghost-nodes
Dashboard + ghost-nodes mvp + promote a issue/flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -293,14 +293,44 @@ static void derive_status_eff(std::vector<Node>& nodes) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Draft (ghost-node) model -------------------------------------------
|
||||
|
||||
struct DraftNode {
|
||||
std::string id; // "tmp_<n>"
|
||||
std::string source_id; // id del nodo que la genero
|
||||
std::string title;
|
||||
std::string description;
|
||||
std::string proposed_type; // "issue" | "flow"
|
||||
std::string proposed_domain; // primer dominio sugerido
|
||||
std::string proposed_priority; // alta|media|baja
|
||||
std::vector<std::string> proposed_dod;
|
||||
|
||||
// posicion actual del ghost, animada desde source hasta target ring/sector.
|
||||
float src_x = 0, src_y = 0;
|
||||
float tgt_x = 0, tgt_y = 0;
|
||||
double spawn_t = 0; // anim start
|
||||
bool anim_done = false; // tras 1.4s deja de moverse
|
||||
};
|
||||
|
||||
// ---- UI state -----------------------------------------------------------
|
||||
|
||||
static fs::path g_root;
|
||||
static ScanResult g_scan;
|
||||
|
||||
// Drafts viven en memoria hasta promote o discard. NO se persisten.
|
||||
static std::vector<DraftNode> g_drafts;
|
||||
static int g_next_draft_n = 1;
|
||||
|
||||
// Tagged selection: Node (index in g_scan.nodes) o Draft (index in g_drafts).
|
||||
enum class SelKind { None, Node, Draft };
|
||||
static SelKind g_sel_kind = SelKind::None;
|
||||
static int g_sel_index = -1;
|
||||
|
||||
static bool g_show_tree = true;
|
||||
static bool g_show_inspector = true;
|
||||
static int g_selected = -1;
|
||||
static bool g_show_dashboard = true;
|
||||
static int g_hover = -1;
|
||||
static SelKind g_hover_kind = SelKind::None;
|
||||
static float g_cam_x = 0.0f;
|
||||
static float g_cam_y = 0.0f;
|
||||
static float g_cam_zoom = 1.0f;
|
||||
@@ -486,7 +516,8 @@ static void reload_scan() {
|
||||
g_scan = std::move(fresh);
|
||||
apply_layout(g_scan.nodes);
|
||||
recount(g_scan);
|
||||
g_selected = -1;
|
||||
g_sel_kind = SelKind::None;
|
||||
g_sel_index = -1;
|
||||
fn_log::log_info("skill_tree: reloaded %d nodes, %d parse errors",
|
||||
(int)g_scan.nodes.size(), g_scan.parse_errors);
|
||||
}
|
||||
@@ -618,9 +649,11 @@ static void draw_canvas() {
|
||||
|
||||
// Picking + node draw. Two passes: issues first (background), flows on top.
|
||||
g_hover = -1;
|
||||
g_hover_kind = SelKind::None;
|
||||
const ImVec2 mp = ImGui::GetMousePos();
|
||||
const float node_r_issue = kNodeRadius * g_cam_zoom;
|
||||
const float node_r_flow = kNodeRadius * kFlowRadiusMul * g_cam_zoom;
|
||||
const float node_r_draft = kNodeRadius * 1.25f * g_cam_zoom;
|
||||
|
||||
auto draw_one = [&](int i, bool flow_pass) {
|
||||
const auto& n = g_scan.nodes[i];
|
||||
@@ -635,7 +668,7 @@ static void draw_canvas() {
|
||||
|
||||
float dx = mp.x - sp.x, dy = mp.y - sp.y;
|
||||
bool over = hovered && (dx * dx + dy * dy) < r * r;
|
||||
if (over) g_hover = i;
|
||||
if (over) { g_hover = i; g_hover_kind = SelKind::Node; }
|
||||
|
||||
ImU32 col = bucket_color(status_bucket(n.status_eff));
|
||||
if (is_flow) {
|
||||
@@ -647,16 +680,16 @@ static void draw_canvas() {
|
||||
{ sp.x - r, sp.y },
|
||||
};
|
||||
dl->AddConvexPolyFilled(pts, 4, col);
|
||||
ImU32 outline = (g_selected == i) ? IM_COL32(255, 255, 255, 255)
|
||||
ImU32 outline = ((g_sel_kind == SelKind::Node && g_sel_index == i)) ? IM_COL32(255, 255, 255, 255)
|
||||
: IM_COL32(34, 211, 238, 255); // cyan-400 always
|
||||
dl->AddPolyline(pts, 4, outline, ImDrawFlags_Closed,
|
||||
(g_selected == i) ? 3.0f : 2.0f);
|
||||
((g_sel_kind == SelKind::Node && g_sel_index == i)) ? 3.0f : 2.0f);
|
||||
} else {
|
||||
dl->AddCircleFilled(sp, r, col, 20);
|
||||
ImU32 outline = (g_selected == i) ? IM_COL32(255, 255, 255, 255)
|
||||
ImU32 outline = ((g_sel_kind == SelKind::Node && g_sel_index == i)) ? IM_COL32(255, 255, 255, 255)
|
||||
: (over ? IM_COL32(255, 255, 255, 220)
|
||||
: IM_COL32(255, 255, 255, 90));
|
||||
dl->AddCircle(sp, r, outline, 20, g_selected == i ? 2.5f : 1.0f);
|
||||
dl->AddCircle(sp, r, outline, 20, (g_sel_kind == SelKind::Node && g_sel_index == i) ? 2.5f : 1.0f);
|
||||
}
|
||||
|
||||
// Text only when zoomed in or this is a flow (always show flow labels).
|
||||
@@ -690,9 +723,66 @@ static void draw_canvas() {
|
||||
for (int i = 0; i < (int)g_scan.nodes.size(); ++i) draw_one(i, false);
|
||||
for (int i = 0; i < (int)g_scan.nodes.size(); ++i) draw_one(i, true);
|
||||
|
||||
// Pass 3: drafts (ghost-nodes). Emerge desde source y se animan al target.
|
||||
const double now_draft = now_seconds();
|
||||
for (int i = 0; i < (int)g_drafts.size(); ++i) {
|
||||
DraftNode& d = g_drafts[i];
|
||||
float t = float((now_draft - d.spawn_t) / 1.4);
|
||||
if (t < 0) continue;
|
||||
if (t > 1.0f) { t = 1.0f; d.anim_done = true; }
|
||||
float e = t * t * (3.0f - 2.0f * t); // ease-in-out
|
||||
float wx = d.src_x + (d.tgt_x - d.src_x) * e;
|
||||
float wy = d.src_y + (d.tgt_y - d.src_y) * e;
|
||||
ImVec2 sp(origin_with_pan.x + wx * g_cam_zoom,
|
||||
origin_with_pan.y + wy * g_cam_zoom);
|
||||
|
||||
if (sp.x < p0.x - node_r_draft || sp.x > p1.x + node_r_draft) continue;
|
||||
if (sp.y < p0.y - node_r_draft || sp.y > p1.y + node_r_draft) continue;
|
||||
|
||||
float ddx = mp.x - sp.x, ddy = mp.y - sp.y;
|
||||
bool over = hovered && (ddx * ddx + ddy * ddy) < node_r_draft * node_r_draft;
|
||||
if (over) { g_hover = i; g_hover_kind = SelKind::Draft; }
|
||||
|
||||
// Color tipo: issue=azul, flow=cyan. Pulsing alpha.
|
||||
float pulse = 0.55f + 0.35f * std::sin(float(now_draft) * 4.5f);
|
||||
ImU32 fill = (d.proposed_type == "flow")
|
||||
? IM_COL32( 14, 165, 233, int(pulse * 200)) // sky-500
|
||||
: IM_COL32( 59, 130, 246, int(pulse * 200)); // blue-500
|
||||
ImU32 ring = IM_COL32(255, 255, 255, int(pulse * 230));
|
||||
|
||||
// Outline pulse (mas grande que el fill) — dashes via segmented circle.
|
||||
dl->AddCircle(sp, node_r_draft * 1.45f, ring, 32, 1.0f);
|
||||
dl->AddCircleFilled(sp, node_r_draft, fill, 24);
|
||||
bool is_sel = (g_sel_kind == SelKind::Draft && g_sel_index == i);
|
||||
dl->AddCircle(sp, node_r_draft, is_sel ? IM_COL32(255, 255, 255, 255) : IM_COL32(255, 255, 255, 200),
|
||||
24, is_sel ? 2.5f : 1.4f);
|
||||
|
||||
// Label: TMP + short title prefix.
|
||||
if (g_cam_zoom > 0.55f) {
|
||||
std::string lbl = std::string("? ") + d.title.substr(0, 18);
|
||||
ImVec2 ts = ImGui::CalcTextSize(lbl.c_str());
|
||||
ImVec2 tp(sp.x - ts.x * 0.5f, sp.y + node_r_draft + 2.0f);
|
||||
dl->AddText(ImVec2(tp.x + 1, tp.y + 1), IM_COL32(0, 0, 0, 220), lbl.c_str());
|
||||
dl->AddText(tp, IM_COL32(255, 255, 255, 245), lbl.c_str());
|
||||
}
|
||||
|
||||
if (over) {
|
||||
const float kTipW = 360.0f;
|
||||
ImGui::SetNextWindowSize(ImVec2(kTipW, 0.0f), ImGuiCond_Always);
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushTextWrapPos(kTipW - 16.0f);
|
||||
ImGui::Text("[DRAFT] %s", d.proposed_type.c_str());
|
||||
ImGui::TextWrapped("%s", d.title.c_str());
|
||||
ImGui::TextDisabled("origen: %s · domain: %s", d.source_id.c_str(), d.proposed_domain.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
// Click: select.
|
||||
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && g_hover >= 0) {
|
||||
g_selected = g_hover;
|
||||
g_sel_kind = g_hover_kind;
|
||||
g_sel_index = g_hover;
|
||||
}
|
||||
|
||||
// Sector labels at outermost ring.
|
||||
@@ -713,6 +803,217 @@ static void draw_canvas() {
|
||||
dl->PopClipRect();
|
||||
}
|
||||
|
||||
// ---- Draft helpers ------------------------------------------------------
|
||||
|
||||
static std::string slugify(const std::string& s) {
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (char c : s) {
|
||||
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) out.push_back(c);
|
||||
else if (c >= 'A' && c <= 'Z') out.push_back(char(c - 'A' + 'a'));
|
||||
else if (c == ' ' || c == '-' || c == '_' || c == '/') out.push_back('-');
|
||||
}
|
||||
// collapse multiple dashes
|
||||
std::string c2;
|
||||
bool prev_dash = false;
|
||||
for (char c : out) {
|
||||
if (c == '-') { if (!prev_dash) c2.push_back(c); prev_dash = true; }
|
||||
else { c2.push_back(c); prev_dash = false; }
|
||||
}
|
||||
while (!c2.empty() && c2.front() == '-') c2.erase(0, 1);
|
||||
while (!c2.empty() && c2.back() == '-') c2.pop_back();
|
||||
if (c2.size() > 48) c2.resize(48);
|
||||
return c2.empty() ? std::string("untitled") : c2;
|
||||
}
|
||||
|
||||
// Devuelve el primer NNNN libre escaneando dev/issues/ y dev/issues/completed/.
|
||||
static std::string next_issue_id(const fs::path& root) {
|
||||
int max_n = 0;
|
||||
auto scan = [&](const fs::path& dir) {
|
||||
if (!fs::exists(dir)) return;
|
||||
for (const auto& e : fs::directory_iterator(dir)) {
|
||||
if (!e.is_regular_file()) continue;
|
||||
auto stem = e.path().stem().string();
|
||||
// Match 4-digit prefix followed by - or letter.
|
||||
if (stem.size() >= 4) {
|
||||
bool all_digit = true;
|
||||
for (int i = 0; i < 4; ++i) if (stem[i] < '0' || stem[i] > '9') { all_digit = false; break; }
|
||||
if (all_digit) {
|
||||
int n = std::atoi(stem.substr(0, 4).c_str());
|
||||
if (n > max_n) max_n = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
scan(root / "dev" / "issues");
|
||||
scan(root / "dev" / "issues" / "completed");
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "%04d", max_n + 1);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static std::string next_flow_id(const fs::path& root) {
|
||||
int max_n = 0;
|
||||
auto scan = [&](const fs::path& dir) {
|
||||
if (!fs::exists(dir)) return;
|
||||
for (const auto& e : fs::directory_iterator(dir)) {
|
||||
if (!e.is_regular_file()) continue;
|
||||
auto stem = e.path().stem().string();
|
||||
if (stem.size() >= 4) {
|
||||
bool all_digit = true;
|
||||
for (int i = 0; i < 4; ++i) if (stem[i] < '0' || stem[i] > '9') { all_digit = false; break; }
|
||||
if (all_digit) {
|
||||
int n = std::atoi(stem.substr(0, 4).c_str());
|
||||
if (n > max_n) max_n = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
scan(root / "dev" / "flows");
|
||||
scan(root / "dev" / "flows" / "completed");
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "%04d", max_n + 1);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static std::string today_iso() {
|
||||
using namespace std::chrono;
|
||||
auto tt = system_clock::to_time_t(system_clock::now());
|
||||
std::tm tmv{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&tmv, &tt);
|
||||
#else
|
||||
localtime_r(&tt, &tmv);
|
||||
#endif
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d",
|
||||
tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Encuentra la posicion objetivo para un draft segun su proposed_domain.
|
||||
// Coloca el draft en la mitad del ring 2 (unlocked) sector de su domain.
|
||||
static void compute_draft_target(DraftNode& d) {
|
||||
std::unordered_set<std::string> domain_set(kDomainOrder.begin(), kDomainOrder.end());
|
||||
std::string dom = d.proposed_domain;
|
||||
if (dom.empty() || domain_set.find(dom) == domain_set.end()) {
|
||||
dom = kDomainOrder[fnv1a(d.id) % kDomainOrder.size()];
|
||||
}
|
||||
int sector = (int)kDomainOrder.size() - 1;
|
||||
for (int i = 0; i < (int)kDomainOrder.size(); ++i) {
|
||||
if (kDomainOrder[i] == dom) { sector = i; break; }
|
||||
}
|
||||
const float r_lo = kRingRadii[2] + 30.0f;
|
||||
const float r_hi = kRingRadii[3] - 30.0f;
|
||||
const float r = 0.5f * (r_lo + r_hi);
|
||||
const float theta = -1.5708f + (sector + 0.5f) * (2.0f * 3.14159265f / 18.0f);
|
||||
d.tgt_x = std::cos(theta) * r;
|
||||
d.tgt_y = std::sin(theta) * r;
|
||||
}
|
||||
|
||||
// Mock LLM: genera 3-5 ideas plausibles para un nodo source.
|
||||
static void mock_generate_ideas(const Node& source) {
|
||||
static const char* kVerbsIssue[] = {
|
||||
"Anadir", "Refactor", "Test golden", "Documentar", "Audit",
|
||||
"Migrar", "Limpiar", "Validar", "Profiling",
|
||||
};
|
||||
static const char* kVerbsFlow[] = {
|
||||
"Smoke flow", "Use-case end-to-end", "Demo escenario",
|
||||
};
|
||||
int N = 3 + (int)(fnv1a(source.id) % 3); // 3..5
|
||||
double now = now_seconds();
|
||||
for (int i = 0; i < N; ++i) {
|
||||
DraftNode d;
|
||||
d.id = "tmp_" + std::to_string(g_next_draft_n++);
|
||||
d.source_id = source.id;
|
||||
bool as_flow = (i == N - 1); // last one as flow
|
||||
d.proposed_type = as_flow ? "flow" : "issue";
|
||||
const char* verb = as_flow
|
||||
? kVerbsFlow[fnv1a(d.id) % (sizeof(kVerbsFlow)/sizeof(kVerbsFlow[0]))]
|
||||
: kVerbsIssue[fnv1a(d.id) % (sizeof(kVerbsIssue)/sizeof(kVerbsIssue[0]))];
|
||||
d.title = std::string(verb) + " " + source.title.substr(0, 40);
|
||||
d.description = "Idea generada (mock) a partir de " + source.id +
|
||||
". Sustituye este texto con LLM real (claude -p) en 0109h2.";
|
||||
d.proposed_domain = source.domain.empty() ? "meta" : source.domain.front();
|
||||
d.proposed_priority = "media";
|
||||
d.proposed_dod = { "DoD item 1", "DoD item 2", "DoD item 3" };
|
||||
d.src_x = source.x;
|
||||
d.src_y = source.y;
|
||||
compute_draft_target(d);
|
||||
d.spawn_t = now + i * 0.15f; // stagger
|
||||
g_drafts.push_back(std::move(d));
|
||||
}
|
||||
fn_log::log_info("skill_tree: generated %d mock drafts from %s", N, source.id.c_str());
|
||||
}
|
||||
|
||||
// Promote draft → archivo .md en dev/issues/ o dev/flows/.
|
||||
static bool promote_draft(int draft_idx, bool as_flow) {
|
||||
if (draft_idx < 0 || draft_idx >= (int)g_drafts.size()) return false;
|
||||
const auto& d = g_drafts[draft_idx];
|
||||
std::string id = as_flow ? next_flow_id(g_root) : next_issue_id(g_root);
|
||||
std::string slug = slugify(d.title);
|
||||
fs::path out_dir = g_root / "dev" / (as_flow ? "flows" : "issues");
|
||||
fs::create_directories(out_dir);
|
||||
fs::path out_file = out_dir / (id + "-" + slug + ".md");
|
||||
|
||||
std::ostringstream ss;
|
||||
ss << "---\n";
|
||||
if (as_flow) {
|
||||
ss << "name: " << slug << "\n";
|
||||
ss << "id: " << id << "\n";
|
||||
ss << "status: pending\n";
|
||||
} else {
|
||||
ss << "id: \"" << id << "\"\n";
|
||||
ss << "title: \"" << d.title << "\"\n";
|
||||
ss << "status: pendiente\n";
|
||||
ss << "type: feature\n";
|
||||
}
|
||||
ss << "domain:\n - " << d.proposed_domain << "\n";
|
||||
ss << "priority: " << d.proposed_priority << "\n";
|
||||
if (as_flow) {
|
||||
ss << "related_issues: []\n";
|
||||
ss << "apps: []\n";
|
||||
ss << "trigger: manual\n";
|
||||
} else {
|
||||
ss << "depends: []\n";
|
||||
ss << "blocks: []\n";
|
||||
ss << "related:\n - \"" << d.source_id << "\"\n";
|
||||
}
|
||||
ss << "created: " << today_iso() << "\n";
|
||||
ss << "updated: " << today_iso() << "\n";
|
||||
ss << "tags: [skill-tree-draft]\n";
|
||||
ss << "---\n\n";
|
||||
ss << "# " << id << " — " << d.title << "\n\n";
|
||||
ss << "Origen: idea generada desde nodo " << d.source_id << " via skill_tree.\n\n";
|
||||
ss << d.description << "\n\n";
|
||||
if (!d.proposed_dod.empty()) {
|
||||
ss << "## DoD\n\n";
|
||||
for (const auto& item : d.proposed_dod) ss << "- [ ] " << item << "\n";
|
||||
ss << "\n";
|
||||
}
|
||||
|
||||
std::ofstream f(out_file);
|
||||
if (!f) { fn_log::log_warn("skill_tree: failed to open %s for write", out_file.string().c_str()); return false; }
|
||||
f << ss.str();
|
||||
f.close();
|
||||
fn_log::log_info("skill_tree: promoted draft %s -> %s", d.id.c_str(), out_file.string().c_str());
|
||||
|
||||
// Remove draft from buffer.
|
||||
g_drafts.erase(g_drafts.begin() + draft_idx);
|
||||
if (g_sel_kind == SelKind::Draft && g_sel_index == draft_idx) {
|
||||
g_sel_kind = SelKind::None;
|
||||
g_sel_index = -1;
|
||||
} else if (g_sel_kind == SelKind::Draft && g_sel_index > draft_idx) {
|
||||
--g_sel_index;
|
||||
}
|
||||
// Trigger reload to pick up the new file.
|
||||
reload_scan();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void draw_inspector_draft();
|
||||
static void draw_dashboard();
|
||||
|
||||
// ---- Panels -------------------------------------------------------------
|
||||
|
||||
static void draw_tree() {
|
||||
@@ -748,12 +1049,18 @@ static void draw_inspector() {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
if (g_selected < 0 || g_selected >= (int)g_scan.nodes.size()) {
|
||||
if (g_sel_kind == SelKind::Draft) {
|
||||
draw_inspector_draft();
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
if (g_sel_kind != SelKind::Node ||
|
||||
g_sel_index < 0 || g_sel_index >= (int)g_scan.nodes.size()) {
|
||||
ImGui::TextDisabled("Click en un nodo del Tree para inspeccionar.");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
const auto& n = g_scan.nodes[g_selected];
|
||||
const auto& n = g_scan.nodes[g_sel_index];
|
||||
ImGui::Text("%s %s", n.kind == NodeKind::Issue ? "[ISSUE]" : "[FLOW]", n.id.c_str());
|
||||
ImGui::TextWrapped("%s", n.title.c_str());
|
||||
ImGui::Separator();
|
||||
@@ -812,13 +1119,229 @@ static void draw_inspector() {
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Botones [Generate ideas] / [Run autonomous-task] llegan en 0109e/f.");
|
||||
|
||||
// === Generate ideas (mock LLM until 0109h2) ===
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 16, 185, 129, 200));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 52, 211, 153, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 6, 148, 110, 255));
|
||||
if (ImGui::Button(TI_PLUS " Generate ideas")) {
|
||||
mock_generate_ideas(n);
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("emergen ghost-nodes con ideas. Click ghost para promover. (LLM real en 0109h2 — hoy mock)");
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Run autonomous-task llega en 0109f.");
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ---- Inspector: draft branch -------------------------------------------
|
||||
|
||||
static void draw_inspector_draft() {
|
||||
if (g_sel_index < 0 || g_sel_index >= (int)g_drafts.size()) {
|
||||
g_sel_kind = SelKind::None; g_sel_index = -1; return;
|
||||
}
|
||||
const DraftNode& d = g_drafts[g_sel_index];
|
||||
ImGui::Text(TI_BULB " DRAFT (idea) · origen: %s", d.source_id.c_str());
|
||||
ImGui::TextWrapped("%s", d.title.c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Text("tipo propuesto: %s", d.proposed_type.c_str());
|
||||
ImGui::Text("domain propuesto:%s", d.proposed_domain.c_str());
|
||||
ImGui::Text("priority: %s", d.proposed_priority.c_str());
|
||||
|
||||
ImGui::SeparatorText("descripcion");
|
||||
ImGui::TextWrapped("%s", d.description.c_str());
|
||||
|
||||
if (!d.proposed_dod.empty()) {
|
||||
ImGui::SeparatorText("DoD propuesto");
|
||||
for (const auto& it : d.proposed_dod) ImGui::BulletText("%s", it.c_str());
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Promote buttons.
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 59, 130, 246, 200));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 96, 165, 250, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 37, 99, 235, 255));
|
||||
if (ImGui::Button(TI_GIT_BRANCH " Generate issue")) {
|
||||
promote_draft(g_sel_index, /*as_flow=*/false);
|
||||
ImGui::PopStyleColor(3);
|
||||
return;
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 14, 165, 233, 200));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 56, 189, 248, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 2, 132, 199, 255));
|
||||
if (ImGui::Button(TI_PLAYLIST_ADD " Generate flow")) {
|
||||
promote_draft(g_sel_index, /*as_flow=*/true);
|
||||
ImGui::PopStyleColor(3);
|
||||
return;
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(115, 115, 115, 200));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(150, 150, 150, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 90, 90, 90, 255));
|
||||
if (ImGui::Button(TI_X " Discard")) {
|
||||
g_drafts.erase(g_drafts.begin() + g_sel_index);
|
||||
g_sel_kind = SelKind::None; g_sel_index = -1;
|
||||
ImGui::PopStyleColor(3);
|
||||
return;
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
// ---- Dashboard ----------------------------------------------------------
|
||||
|
||||
static int xp_for_type(const std::string& type, NodeKind kind) {
|
||||
if (kind == NodeKind::Flow) return 5;
|
||||
if (type == "epic") return 10;
|
||||
if (type == "feature") return 3;
|
||||
if (type == "infra") return 4;
|
||||
if (type == "refactor") return 2;
|
||||
if (type == "bugfix") return 1;
|
||||
if (type == "chore") return 1;
|
||||
if (type == "docs") return 1;
|
||||
if (type == "spike") return 2;
|
||||
if (type == "planning") return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void draw_dashboard() {
|
||||
if (!ImGui::Begin(TI_CHART_BAR " Dashboard", &g_show_dashboard)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Per-domain accumulators.
|
||||
struct DomStat {
|
||||
int done = 0, planned = 0, todo = 0, total = 0;
|
||||
int xp = 0;
|
||||
};
|
||||
std::unordered_map<std::string, DomStat> per_dom;
|
||||
int xp_total = 0;
|
||||
int xp_by_type_done[16] = {0}; // index by enum-ish ordering, but we keep simple
|
||||
int n_done = 0, n_planned = 0, n_todo = 0;
|
||||
|
||||
for (const auto& n : g_scan.nodes) {
|
||||
int xp = xp_for_type(n.type, n.kind);
|
||||
Bucket b = status_bucket(n.status_eff);
|
||||
if (b == Bucket::Done) ++n_done;
|
||||
else if (b == Bucket::Planned) ++n_planned;
|
||||
else ++n_todo;
|
||||
|
||||
if (b == Bucket::Done) xp_total += xp;
|
||||
|
||||
// Per domain (cuenta una vez en cada dominio listado).
|
||||
std::vector<std::string> doms = n.domain;
|
||||
if (doms.empty()) doms.push_back("(unknown)");
|
||||
for (const auto& d : doms) {
|
||||
auto& s = per_dom[d];
|
||||
++s.total;
|
||||
if (b == Bucket::Done) { ++s.done; s.xp += xp; }
|
||||
else if (b == Bucket::Planned) { ++s.planned; }
|
||||
else { ++s.todo; }
|
||||
}
|
||||
}
|
||||
|
||||
int level = (int)std::floor(std::sqrt((float)xp_total));
|
||||
int xp_next_level = (level + 1) * (level + 1);
|
||||
|
||||
// HUD top.
|
||||
ImGui::Text(TI_TROPHY " LV %d", level);
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("· XP %d · next LV at %d", xp_total, xp_next_level);
|
||||
|
||||
float xp_frac = (xp_next_level > 0) ? float(xp_total - level*level) / float(xp_next_level - level*level) : 0.0f;
|
||||
ImGui::ProgressBar(xp_frac, ImVec2(-FLT_MIN, 0.0f),
|
||||
(std::to_string(xp_total - level*level) + " / " +
|
||||
std::to_string(xp_next_level - level*level) + " XP").c_str());
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("done: %d · planned: %d · todo: %d · drafts: %d",
|
||||
n_done, n_planned, n_todo, (int)g_drafts.size());
|
||||
|
||||
ImGui::SeparatorText("Habilidades por dominio");
|
||||
|
||||
if (ImGui::BeginTable("doms", 6, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_Sortable)) {
|
||||
ImGui::TableSetupColumn("Dominio");
|
||||
ImGui::TableSetupColumn("Done", ImGuiTableColumnFlags_WidthFixed, 50.0f);
|
||||
ImGui::TableSetupColumn("Planned", ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
||||
ImGui::TableSetupColumn("Todo", ImGuiTableColumnFlags_WidthFixed, 50.0f);
|
||||
ImGui::TableSetupColumn("Progreso", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("LV", ImGuiTableColumnFlags_WidthFixed, 50.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
// Ordenar por % completado desc.
|
||||
std::vector<std::pair<std::string, DomStat>> ord(per_dom.begin(), per_dom.end());
|
||||
std::sort(ord.begin(), ord.end(), [](auto& a, auto& b) {
|
||||
float pa = a.second.total > 0 ? float(a.second.done) / a.second.total : 0;
|
||||
float pb = b.second.total > 0 ? float(b.second.done) / b.second.total : 0;
|
||||
if (pa != pb) return pa > pb;
|
||||
return a.second.done > b.second.done;
|
||||
});
|
||||
for (const auto& [name, s] : ord) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn(); ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::TableNextColumn(); ImGui::Text("%d", s.done);
|
||||
ImGui::TableNextColumn(); ImGui::Text("%d", s.planned);
|
||||
ImGui::TableNextColumn(); ImGui::Text("%d", s.todo);
|
||||
float frac = s.total > 0 ? float(s.done) / float(s.total) : 0.0f;
|
||||
ImGui::TableNextColumn();
|
||||
char ovl[24]; std::snprintf(ovl, sizeof(ovl), "%d/%d (%d%%)", s.done, s.total, int(frac*100));
|
||||
ImGui::ProgressBar(frac, ImVec2(-FLT_MIN, 0), ovl);
|
||||
int lv = (int)std::floor(std::sqrt((float)s.xp));
|
||||
ImGui::TableNextColumn(); ImGui::Text("LV %d", lv);
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Top 3 dominios mas masterizados");
|
||||
{
|
||||
std::vector<std::pair<std::string, DomStat>> top(per_dom.begin(), per_dom.end());
|
||||
std::sort(top.begin(), top.end(), [](auto& a, auto& b) {
|
||||
float pa = a.second.total > 0 ? float(a.second.done) / a.second.total : 0;
|
||||
float pb = b.second.total > 0 ? float(b.second.done) / b.second.total : 0;
|
||||
return pa > pb;
|
||||
});
|
||||
int shown = 0;
|
||||
for (const auto& [name, s] : top) {
|
||||
if (s.total < 2) continue; // skip noise
|
||||
if (shown >= 3) break;
|
||||
float p = float(s.done) / float(s.total);
|
||||
ImGui::BulletText("%s — %d/%d (%.0f%%)", name.c_str(), s.done, s.total, p*100);
|
||||
++shown;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Proximos a desbloquearse (mas locked)");
|
||||
{
|
||||
std::vector<std::pair<std::string, DomStat>> top(per_dom.begin(), per_dom.end());
|
||||
std::sort(top.begin(), top.end(), [](auto& a, auto& b) {
|
||||
return a.second.todo > b.second.todo;
|
||||
});
|
||||
int shown = 0;
|
||||
for (const auto& [name, s] : top) {
|
||||
if (s.todo < 1) break;
|
||||
if (shown >= 3) break;
|
||||
ImGui::BulletText("%s — %d todo (%d done)", name.c_str(), s.todo, s.done);
|
||||
++shown;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
static void render() {
|
||||
if (g_show_tree) draw_tree();
|
||||
if (g_show_inspector) draw_inspector();
|
||||
if (g_show_dashboard) draw_dashboard();
|
||||
}
|
||||
|
||||
// ---- Self-test ----------------------------------------------------------
|
||||
@@ -871,6 +1394,7 @@ int main(int argc, char** argv) {
|
||||
static fn_ui::PanelToggle panels[] = {
|
||||
{ "Tree", nullptr, &g_show_tree },
|
||||
{ "Inspector", nullptr, &g_show_inspector },
|
||||
{ "Dashboard", nullptr, &g_show_dashboard },
|
||||
};
|
||||
|
||||
fn::AppConfig cfg;
|
||||
|
||||
Reference in New Issue
Block a user