Files
Joel Salmon b9d7b0e22b Phase 5: iteration & polish (visualizations, goal history, smart tagging)
Final roadmap phase. No DB migration — it reads data already captured.

- Goal-evolution history: GET /discovery/profile/me/goal-history derives a
  per-goal timeline from the profile_revision snapshots (pure aggregator in
  app/services/profile_history.py).
- Smart tagging: POST /discovery/integration/suggest-foundation suggests which
  foundation a task builds toward + rationale/confidence (FoundationTagger,
  app/services/tagging.py). Suggestion only; the person confirms by posting the
  task mapping.
- Deeper goal-refinement: the reflect loop accepts an optional focus ("goals")
  that steers the coach toward sharpening goals — still mirror, not compass.
- Visualizations: visuals.html renders an Ikigai Venn and an Enneagram diagram
  (plain-language callouts, not the raw type number) plus the goal-evolution
  timeline; linked from profile.html.

Tests: 99 passing (added pure goal-history tests, goal-history + suggest
endpoint tests, reflect-focus passthrough; run in-container). README updated.

This completes the ImpactFlow Vision roadmap (Phases 1-5) on the Discovery side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:37:33 -05:00

191 lines
7.5 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ImpactFlow — Your Profile, Visualized</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Playfair+Display:wght@500;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/static/style.css" />
<script src="/static/auth.js"></script>
</head>
<body>
<div class="wrap">
<div class="brand">ImpactFlow · Your Profile, Visualized</div>
<div id="content"><p>Loading…</p></div>
<p class="back-link"><a href="/static/profile.html">← Back to your profile</a></p>
</div>
<script>
const SVGNS = "http://www.w3.org/2000/svg";
function escapeHtml(s) {
if (s == null) return "";
return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function el(tag, attrs, text) {
const n = document.createElementNS(SVGNS, tag);
for (const k in attrs) n.setAttribute(k, attrs[k]);
if (text != null) n.textContent = text;
return n;
}
// ---- Ikigai: four overlapping circles ----
function ikigaiSvg(p) {
const W = 460, H = 460, cx = W / 2, cy = H / 2, r = 130, off = 78;
const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, class: "viz" });
const circles = [
{ dx: 0, dy: -off, fill: "#c9a84c", label: "What you love", key: "love_summary" },
{ dx: off, dy: 0, fill: "#2e7d32", label: "Good at", key: "strength_summary" },
{ dx: 0, dy: off, fill: "#1c3049", label: "World needs", key: "mission_summary" },
{ dx: -off, dy: 0, fill: "#a0522d", label: "Paid for", key: "vocation_summary" },
];
circles.forEach((c) => {
svg.appendChild(
el("circle", {
cx: cx + c.dx, cy: cy + c.dy, r,
fill: c.fill, "fill-opacity": "0.22",
stroke: c.fill, "stroke-opacity": "0.7", "stroke-width": "1.5",
})
);
});
circles.forEach((c) => {
const lx = cx + c.dx * 1.7;
const ly = cy + c.dy * 1.7 + (c.dy === 0 ? 4 : 0);
svg.appendChild(
el("text", {
x: lx, y: ly, "text-anchor": "middle",
"font-size": "14", "font-weight": "600", fill: "#0d1b2a",
}, c.label)
);
});
svg.appendChild(
el("text", { x: cx, y: cy + 4, "text-anchor": "middle", "font-size": "15", "font-weight": "700", fill: "#0d1b2a" }, "Ikigai")
);
return svg;
}
// ---- Enneagram: 9 points, inner triangle + hexad, type/wing highlighted.
// We keep callouts in plain language rather than headlining a type number.
function enneagramSvg(p) {
const W = 420, H = 420, cx = W / 2, cy = H / 2, R = 160;
const deg = { 9: 0, 1: 40, 2: 80, 3: 120, 4: 160, 5: 200, 6: 240, 7: 280, 8: 320 };
const pt = (n) => {
const t = (deg[n] * Math.PI) / 180;
return [cx + R * Math.sin(t), cy - R * Math.cos(t)];
};
const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, class: "viz" });
svg.appendChild(el("circle", { cx, cy, r: R, fill: "none", stroke: "#1c3049", "stroke-opacity": "0.3", "stroke-width": "1.5" }));
const line = (a, b) => {
const [x1, y1] = pt(a), [x2, y2] = pt(b);
svg.appendChild(el("line", { x1, y1, x2, y2, stroke: "#1c3049", "stroke-opacity": "0.25", "stroke-width": "1.2" }));
};
[[9, 3], [3, 6], [6, 9]].forEach(([a, b]) => line(a, b));
[[1, 4], [4, 2], [2, 8], [8, 5], [5, 7], [7, 1]].forEach(([a, b]) => line(a, b));
const type = p.probable_type, wing = p.wing;
for (let n = 1; n <= 9; n++) {
const [x, y] = pt(n);
const isType = n === type, isWing = n === wing;
svg.appendChild(
el("circle", {
cx: x, cy: y, r: isType ? 17 : 12,
fill: isType ? "#c9a84c" : "#fff",
stroke: isWing ? "#c9a84c" : "#1c3049",
"stroke-width": isWing ? "3" : "1.5",
})
);
svg.appendChild(
el("text", {
x, y: y + 4, "text-anchor": "middle", "font-size": "13",
"font-weight": isType ? "700" : "500",
fill: isType ? "#0d1b2a" : "#1c3049",
}, String(n))
);
}
return svg;
}
const TRIAD_LABEL = {
gut: "You lead with instinct and will",
heart: "You lead with feeling and connection",
head: "You lead with thought and perception",
};
async function loadGoalHistory() {
try {
const res = await authedFetch("/discovery/profile/me/goal-history");
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
function timelineHtml(entries) {
if (!entries || !entries.length)
return `<p class="edit-help">No changes recorded yet.</p>`;
return entries
.map((e) => {
const when = e.at ? new Date(e.at).toLocaleDateString() : "";
return `<div class="card" style="margin-bottom:12px">
<p style="margin:0 0 6px"><span class="dot ${
e.source === "extraction" ? "medium" : "high"
}"></span> <strong>${escapeHtml(e.source || "")}</strong> · ${when}</p>
<p style="margin:0">${escapeHtml(e.value) || "—"}</p>
</div>`;
})
.join("");
}
async function load() {
const content = document.getElementById("content");
let p;
try {
const res = await authedFetch("/discovery/profile/me");
if (!res.ok) throw new Error("Profile not found");
p = await res.json();
} catch (e) {
content.innerHTML = `<div class="error-box">${escapeHtml(e.message)}</div>`;
return;
}
content.innerHTML = `
<p class="section-label">Your Ikigai</p>
<div id="ikigai" class="viz-wrap"></div>
<div class="ikigai-grid">
<div class="card"><h3>♥ What you love</h3><p>${escapeHtml(p.love_summary) || "—"}</p></div>
<div class="card"><h3>★ What you're good at</h3><p>${escapeHtml(p.strength_summary) || "—"}</p></div>
<div class="card"><h3>◆ What the world needs</h3><p>${escapeHtml(p.mission_summary) || "—"}</p></div>
<div class="card"><h3>$ What you can be paid for</h3><p>${escapeHtml(p.vocation_summary) || "—"}</p></div>
</div>
<p class="section-label" style="margin-top:30px">Your centre of gravity</p>
<div id="enneagram" class="viz-wrap"></div>
<div class="triad-block"><p>${escapeHtml(
TRIAD_LABEL[p.triad] || "Your pattern"
)}. This map highlights where your energy sits and the point it leans toward.</p></div>
<p class="section-label" style="margin-top:30px">How your goals have evolved</p>
<div class="goal-cols">
<div><p class="edit-help">Near-term (612 months)</p><div id="st"></div></div>
<div><p class="edit-help">Long-term (35 years)</p><div id="lt"></div></div>
</div>
`;
document.getElementById("ikigai").appendChild(ikigaiSvg(p));
document.getElementById("enneagram").appendChild(enneagramSvg(p));
const hist = await loadGoalHistory();
document.getElementById("st").innerHTML = timelineHtml(hist && hist.short_term_goals);
document.getElementById("lt").innerHTML = timelineHtml(hist && hist.long_term_goals);
}
load();
</script>
</body>
</html>