// Phase 07 — Apology on-ramp. Private editable draft, live coach flags, real Claude feedback.
// Quiet, narrow, ritual.
console.log('[phases-apology] loading');
function ApologyPhase({ seat, godView, onPhase }) {
const S = window.SCENARIO;
const isJordan = seat === 'B';
const isMaya = seat === 'A';
const isGod = seat === 'god';
// Receiver view: maya doesn't see jordan's draft. We show a waiting panel.
if (isMaya) return ;
// Jordan (seat B) or God view — show the drafting runway.
return ;
}
// ————————————————————————————————————————————————————————————————————————————
// Receiver (Maya) — private, "something is happening, you'll see it if/when it's sent"
// ————————————————————————————————————————————————————————————————————————————
function ApologyReceiverView({ onPhase }) {
const S = window.SCENARIO;
const [dots, setDots] = React.useState(1);
React.useEffect(() => {
const t = setInterval(() => setDots(d => (d % 3) + 1), 700);
return () => clearInterval(t);
}, []);
return (
phase 07 · apology on-ramp · your view · maya
the runway is private. this is what that looks like from your side.
jordan's coach noticed a softening signal and offered to help them draft something. the content is firewalled. you'll see it only if jordan chooses to send it into the room — on their time, not yours.
firewalled · content private
jordan is drafting something{'.'.repeat(dots)}
you will not see what they're writing, how long they take, or how many drafts. this runway belongs to jordan. if and when they hit send, the final version lands in the center and becomes public record.
your options while you wait
·
keep reviewing the map / established truths on your side
·
talk to your coach privately — it's ok not to wait
·
park the room entirely — come back tomorrow
your coach · private
"you're not owed this. if it lands, you can take any piece of it into your acknowledgment column — or none of it. the apology is theirs to write; what you do with it is yours."
want to see what's happening on jordan's side right now? switch to their seat — or see both at once in god view.
no apologies with a gun to the head. the runway is yours.
write freely. your coach reads along and flags the patterns that turn apologies into their opposite. nothing lands until you hit send — and then it's permanent.
{hasProgress && (
)}
);
}
// —— drafter (main column) ——
function ApologyDrafter(props) {
const { stage } = props;
if (stage === 'offer') return ;
if (stage === 'landed') return ;
// 'editing' | 'sending'
return ;
}
// —— stage: offer (before user accepts to draft) ——
function ApologyOfferCard({ setStage, setDraft }) {
const S = window.SCENARIO;
return (
softening signal · your coach noticed
you · chat c9 · earlier
"i can sit with the pattern read. the recent three — yeah. they've been closer together than usual."
private · only you see this
you just did something a lot of people can't do. you named a pattern you're inside of.
if you want, i can help you draft something specific — not a performance, not a sandwich. just the thing that's true. you'd keep full edit control. maya sees nothing until you decide.
the "check on me later" option is real. if you're not ready, the coach backs off and re-surfaces the offer at a softer moment — never in front of maya, never with a timer.
this becomes public record. maya sees it. it stays in the room's archive with your name on it.
"{draft}"
· maya can accept, partially acknowledge, or let it sit — her call
· a redaction request is possible, but creates its own record
· you can't un-send; you can follow up
);
}
// —— stage: landed ——
function ApologyLandedCard({ draft, landedAt, onPhase }) {
const S = window.SCENARIO;
return (
sent · landed in center · on the record
on record · {landedAt || 'apr 15 · 10:14pm'}
jordan · apology
{draft}
what this triggers
proposed as a new established truth · awaiting maya's response
maya can drag sub-pieces into her acknowledgment column · or none at all
if an action-commitment is in the text, it lands as a draft action item for phase 08
the weight shifts. not away — distributed.
);
}
// —— Maya-side view (only shown in god view, alongside drafter) ——
function ApologyMayaSide({ landedAt, draft }) {
const S = window.SCENARIO;
const [dots, setDots] = React.useState(1);
React.useEffect(() => {
if (landedAt) return;
const t = setInterval(() => setDots(d => (d % 3) + 1), 700);
return () => clearInterval(t);
}, [landedAt]);
return (
meanwhile · maya's seat
{!landedAt ? (
<>
firewalled · content hidden
jordan is drafting something{'.'.repeat(dots)}
maya sees nothing of the content, the edits, or the time taken. only the status.
>
) : (
<>
✓ landed at {landedAt}
maya sees the apology in the center. she can acknowledge, let it sit, or request a redaction. pressure to reciprocate is explicitly off.
no "read at" timestamp on her side. no reply countdown.
>
)}
);
}
// —— Deep feedback panel ——
function DeepFeedbackPanel({ feedback }) {
if (feedback.error) {
return
{feedback.error}
;
}
return (
{feedback.summary &&
{feedback.summary}
}
{feedback.strengths?.length > 0 && (
what's working
{feedback.strengths.map((s, i) => (
✓{s}
))}
)}
{feedback.suggestions?.length > 0 && (
things to consider
{feedback.suggestions.map((s, i) => (
→{s}
))}
)}
);
}
// ————————————————————————————————————————————————————————————————————————————
// Claude calls
// ————————————————————————————————————————————————————————————————————————————
async function callCoachFlags(draft) {
try {
const prompt = `You are a communication coach reviewing a draft apology. Identify specific patterns that weaken apologies. Be concise and concrete.
Draft:
"""
${draft}
"""
Return ONLY a JSON array of flag objects. Each object has:
- "label": short label (2-4 words, lowercase), e.g. "non-apology", "sandwich", "vague", "mind-reading", "over-qualifying"
- "severity": "red" (strongly undermines) | "yellow" (weakens)
- "note": one short sentence explaining the flag
- "spans": array of exact quoted substrings from the draft that triggered this flag (1-3 phrases)
Only flag real patterns. If the draft is clean, return [].
Limit to 4 flags max. Focus on the most important.
Return ONLY valid JSON, no prose.`;
const text = await window.claude.complete(prompt);
return parseJsonArray(text);
} catch (e) {
console.warn('coach flags failed', e);
return [];
}
}
async function callDeepFeedback(draft) {
try {
const prompt = `You are a communication coach giving warm, specific feedback on a draft apology. The person is drafting privately. They want honest, concrete guidance — not validation.
Draft:
"""
${draft}
"""
Return ONLY a JSON object with these keys:
- "summary": 1-2 sentences, your overall read. Human, not clinical. Lowercase ok.
- "strengths": array of 0-3 specific things that work (short phrases).
- "suggestions": array of 0-3 specific things to consider — concrete, not generic. Not commands; invitations.
Return ONLY valid JSON.`;
const text = await window.claude.complete(prompt);
return parseJsonObject(text) || { error: 'coach went quiet. try again?' };
} catch (e) {
console.warn('deep feedback failed', e);
return { error: 'coach went quiet. try again?' };
}
}
function parseJsonArray(text) {
if (!text) return [];
const match = text.match(/\[[\s\S]*\]/);
if (!match) return [];
try {
const arr = JSON.parse(match[0]);
return Array.isArray(arr) ? arr.filter(x => x && x.label) : [];
} catch { return []; }
}
function parseJsonObject(text) {
if (!text) return null;
const match = text.match(/\{[\s\S]*\}/);
if (!match) return null;
try { return JSON.parse(match[0]); } catch { return null; }
}
// Expose globally so the component file imports work across script scopes
Object.assign(window, { ApologyPhase });
console.log('[phases-apology] loaded, ApologyPhase on window:', typeof window.ApologyPhase);