mirror of
https://github.com/computerim/impactflow-discovery.git
synced 2026-08-27 09:10:36 +00:00
b9d7b0e22b
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>
191 lines
7.5 KiB
HTML
191 lines
7.5 KiB
HTML
<!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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
}
|
||
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 (6–12 months)</p><div id="st"></div></div>
|
||
<div><p class="edit-help">Long-term (3–5 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>
|