mirror of
https://github.com/computerim/impactflow-discovery.git
synced 2026-08-27 08:40:37 +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>
285 lines
9.6 KiB
HTML
285 lines
9.6 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</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 Direction</div>
|
||
<div id="content">
|
||
<p>Loading your profile…</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Plain-language description of each enneagram triad. We never show the
|
||
// type number on this page — only the pattern.
|
||
const TRIAD_INFO = {
|
||
gut: {
|
||
label: "You lead with instinct and will",
|
||
text:
|
||
"You move through the world from your gut. You sense what's right before you can fully explain it, and you're driven to act, to protect what matters, and to set things right. Autonomy and integrity are non-negotiable for you, and you'd rather meet a problem head-on than wait for permission.",
|
||
},
|
||
heart: {
|
||
label: "You lead with feeling and connection",
|
||
text:
|
||
"You navigate through emotion and relationship. You're finely attuned to how things land — for you and for the people around you — and you care deeply about being genuinely seen for who you are. Your energy comes alive in connection, recognition, and making others feel that they matter.",
|
||
},
|
||
head: {
|
||
label: "You lead with thought and perception",
|
||
text:
|
||
"You meet the world through your mind. You like to understand before you commit — gathering information, mapping possibilities, and anticipating what's ahead so you can feel prepared and secure. Your gift is seeing patterns and thinking your way toward clarity others miss.",
|
||
},
|
||
};
|
||
|
||
const IKIGAI = [
|
||
{ key: "love_summary", title: "What you love", icon: "♥" },
|
||
{ key: "strength_summary", title: "What you're good at", icon: "★" },
|
||
{ key: "mission_summary", title: "What the world needs", icon: "◆" },
|
||
{ key: "vocation_summary", title: "What you can be paid for", icon: "$" },
|
||
];
|
||
|
||
function confClass(level) {
|
||
if (level === "high") return "high";
|
||
if (level === "medium") return "medium";
|
||
return "low";
|
||
}
|
||
|
||
function dot(level) {
|
||
const c = confClass(level);
|
||
const title = `Confidence: ${level || "low"}`;
|
||
return `<span class="dot ${c}" title="${title}"></span>`;
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
if (s == null) return "";
|
||
return String(s)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">");
|
||
}
|
||
|
||
// The editable prose. Order/labels mirror the read view. The AI's
|
||
// structural read (triad/type/wing/variant) is shown but not edited here.
|
||
const GOAL_FIELDS = [
|
||
{ key: "short_term_goals", title: "Next 6–12 months" },
|
||
{ key: "long_term_goals", title: "Next 3–5 years" },
|
||
];
|
||
|
||
const content = document.getElementById("content");
|
||
let currentProfile = null;
|
||
|
||
async function load() {
|
||
try {
|
||
const res = await authedFetch("/discovery/profile/me");
|
||
if (!res.ok) throw new Error("Profile not found");
|
||
currentProfile = await res.json();
|
||
} catch (err) {
|
||
content.innerHTML = `<div class="error-box">Could not load your profile: ${escapeHtml(
|
||
err.message
|
||
)}</div>`;
|
||
return;
|
||
}
|
||
renderRead();
|
||
}
|
||
|
||
function renderRead() {
|
||
const profile = currentProfile;
|
||
const conf = profile.confidence || {};
|
||
const triad = TRIAD_INFO[profile.triad] || TRIAD_INFO.gut;
|
||
|
||
const ikigaiCards = IKIGAI.map(
|
||
(item) => `
|
||
<div class="card">
|
||
<h3>${dot(conf.ikigai)} ${item.icon} ${item.title}</h3>
|
||
<p>${escapeHtml(profile[item.key]) || "—"}</p>
|
||
</div>`
|
||
).join("");
|
||
|
||
const locked = profile.locked;
|
||
|
||
// Goals are the person's own articulated direction. Only show the
|
||
// section if at least one horizon has content.
|
||
const goals = GOAL_FIELDS.filter(
|
||
(g) => profile[g.key] && profile[g.key].trim()
|
||
);
|
||
const goalsBlock = goals.length
|
||
? `
|
||
<p class="section-label">Where you're headed</p>
|
||
<div class="ikigai-grid">${goals
|
||
.map(
|
||
(g) => `
|
||
<div class="card">
|
||
<h3>→ ${g.title}</h3>
|
||
<p>${escapeHtml(profile[g.key])}</p>
|
||
</div>`
|
||
)
|
||
.join("")}</div>
|
||
`
|
||
: "";
|
||
|
||
// Unlocked profiles can be edited and then affirmed; locked profiles are
|
||
// final and show neither control as actionable.
|
||
const actions = locked
|
||
? `<div class="confirm-row">
|
||
<button class="btn-primary confirmed" disabled>Profile confirmed ✓</button>
|
||
</div>`
|
||
: `<div class="nav">
|
||
<button class="btn-ghost" id="editBtn">Edit my words</button>
|
||
<button class="btn-primary" id="confirmBtn">This is me</button>
|
||
</div>
|
||
<p class="back-link" style="text-align:center;margin-top:18px">
|
||
<a href="/static/reflect.html">Not quite right? Talk it through with your coach →</a>
|
||
</p>`;
|
||
|
||
const coachingLink =
|
||
`<p class="back-link" style="text-align:center;margin-top:10px">
|
||
<a href="/static/coaching.html">Coaching preferences & check-ins →</a>
|
||
</p>
|
||
<p class="back-link" style="text-align:center;margin-top:10px">
|
||
<a href="/static/dashboard.html">Where your time goes →</a>
|
||
</p>
|
||
<p class="back-link" style="text-align:center;margin-top:10px">
|
||
<a href="/static/visuals.html">See your profile visualized →</a>
|
||
</p>`;
|
||
|
||
content.innerHTML = `
|
||
<div class="profile-narrative">${escapeHtml(
|
||
profile.overlap_narrative
|
||
)}</div>
|
||
|
||
<p class="section-label">Where your four circles meet</p>
|
||
<div class="ikigai-grid">${ikigaiCards}</div>
|
||
|
||
<div class="triad-block">
|
||
<h2>${dot(conf.triad)} ${triad.label}</h2>
|
||
<p>${triad.text}</p>
|
||
</div>
|
||
|
||
${goalsBlock}
|
||
${actions}
|
||
${coachingLink}
|
||
`;
|
||
|
||
if (locked) return;
|
||
|
||
document
|
||
.getElementById("editBtn")
|
||
.addEventListener("click", renderEdit);
|
||
|
||
const btn = document.getElementById("confirmBtn");
|
||
btn.addEventListener("click", async () => {
|
||
btn.disabled = true;
|
||
try {
|
||
const res = await authedFetch("/discovery/profile/me/confirm", {
|
||
method: "PUT",
|
||
});
|
||
if (!res.ok) throw new Error("confirm failed");
|
||
currentProfile.locked = true;
|
||
renderRead();
|
||
} catch (err) {
|
||
btn.disabled = false;
|
||
btn.textContent = "Try again";
|
||
}
|
||
});
|
||
}
|
||
|
||
function editField(key, label, value) {
|
||
return `
|
||
<div class="edit-field">
|
||
<label for="edit_${key}">${label}</label>
|
||
<textarea class="edit" id="edit_${key}">${escapeHtml(
|
||
value
|
||
)}</textarea>
|
||
</div>`;
|
||
}
|
||
|
||
function renderEdit() {
|
||
const profile = currentProfile;
|
||
|
||
const ikigaiFields = IKIGAI.map((item) =>
|
||
editField(item.key, `${item.icon} ${item.title}`, profile[item.key])
|
||
).join("");
|
||
const goalFields = GOAL_FIELDS.map((g) =>
|
||
editField(g.key, `→ ${g.title}`, profile[g.key])
|
||
).join("");
|
||
|
||
content.innerHTML = `
|
||
<p class="section-label">Edit your words</p>
|
||
<p class="edit-help">These are your words to own. Revise anything that
|
||
doesn't sound like you, then save.</p>
|
||
|
||
${editField(
|
||
"overlap_narrative",
|
||
"Where it all comes together",
|
||
profile.overlap_narrative
|
||
)}
|
||
|
||
<div class="ikigai-grid">${ikigaiFields}</div>
|
||
<div class="ikigai-grid">${goalFields}</div>
|
||
|
||
<div id="editError"></div>
|
||
<div class="nav">
|
||
<button class="btn-ghost" id="cancelBtn">Cancel</button>
|
||
<button class="btn-primary" id="saveBtn">Save changes</button>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById("cancelBtn").addEventListener("click", renderRead);
|
||
document.getElementById("saveBtn").addEventListener("click", saveEdits);
|
||
}
|
||
|
||
const EDITABLE_KEYS = [
|
||
"overlap_narrative",
|
||
...IKIGAI.map((i) => i.key),
|
||
...GOAL_FIELDS.map((g) => g.key),
|
||
];
|
||
|
||
async function saveEdits() {
|
||
const saveBtn = document.getElementById("saveBtn");
|
||
saveBtn.disabled = true;
|
||
|
||
const payload = {};
|
||
EDITABLE_KEYS.forEach((key) => {
|
||
payload[key] = document.getElementById(`edit_${key}`).value;
|
||
});
|
||
|
||
try {
|
||
const res = await authedFetch("/discovery/profile/me", {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!res.ok) {
|
||
const detail = await res
|
||
.json()
|
||
.catch(() => ({ detail: "Could not save changes" }));
|
||
throw new Error(detail.detail || "Could not save changes");
|
||
}
|
||
currentProfile = await res.json();
|
||
renderRead();
|
||
} catch (err) {
|
||
saveBtn.disabled = false;
|
||
document.getElementById(
|
||
"editError"
|
||
).innerHTML = `<div class="error-box">${escapeHtml(
|
||
err.message
|
||
)}</div>`;
|
||
}
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html>
|