// Phase 00 · Style Lock — host picks conversation style, guest co-signs.
// Sets `shared.style = { voice, asymmetry, directness, signedA, signedB, signedAt }`.
// Once both sign, style is immutable for the session.
// Depends on: CG, Icons
const STYLE_OPTIONS = {
voice: [
{ v: 'warm-informal', label: 'warm · informal', hint: 'wise friend · plain, lowercase-ish · "what\'s actually going on here?"' },
{ v: 'warm-clinical', label: 'warm · clinical', hint: 'steady therapist · measured, named feelings · "it sounds like there\'s a pattern."' },
{ v: 'dry-neutral', label: 'dry · neutral', hint: 'court reporter with opinions · short, no flourish · "the asymmetry is on record."' },
],
asymmetry: [
{ v: 'truth-seeking', label: 'truth-seeking', hint: 'if one party is more responsible, the mediator will name it. no forced balance.' },
{ v: 'symmetric', label: 'symmetric', hint: 'both sides treated equally in tone & framing. fault is not weighed.' },
{ v: 'hybrid', label: 'hybrid', hint: 'asymmetry may be named — but only when both of you would co-sign that observation.' },
],
directness: [
{ v: 'soften', label: 'soften the edges', hint: 'hard truths only after gentle setup. the coach will never bluntly contradict you.' },
{ v: 'plain', label: 'plain', hint: 'the coach says what it sees without padding. no escalation either.' },
{ v: 'challenge', label: 'challenge me', hint: 'the coach will push back on characterizations and kitchen-sinking directly.' },
],
};
const STYLE_DEFAULTS = { voice: 'warm-informal', asymmetry: 'hybrid', directness: 'plain' };
function StyleLockPhase({ seat, onPhase }) {
const [, forceTick] = React.useState(0);
React.useEffect(() => CG.session.onState(() => forceTick(t => t + 1)), []);
React.useEffect(() => CG.onStatus(() => forceTick(t => t + 1)), []);
const shared = CG.session.shared || {};
const names = (CG.session.getNames && CG.session.getNames()) || {};
const mp = CG.session.isMultiplayer && CG.session.isMultiplayer();
const isHost = !mp || seat === 'A';
const isGuest = mp && seat === 'B';
const nameA = names.A || (mp ? 'host' : 'host');
const nameB = names.B || (mp ? 'guest' : 'guest');
// Host-side draft (pre-sign). Seeded from shared.style if partially set, else defaults.
const [draft, setDraft] = React.useState(() => {
if (shared.style) return { voice: shared.style.voice, asymmetry: shared.style.asymmetry, directness: shared.style.directness };
return { ...STYLE_DEFAULTS };
});
// The locked/signed style lives in shared.style. A null style means nothing proposed yet.
const proposed = shared.style; // { voice, asymmetry, directness, signedA, signedB, ... } | null
const hostSigned = !!(proposed && proposed.signedA);
const guestSigned = !!(proposed && proposed.signedB);
const bothSigned = hostSigned && guestSigned;
// Host · propose & sign (in solo, also auto-co-sign so user can proceed)
const hostPropose = () => {
const style = {
voice: draft.voice,
asymmetry: draft.asymmetry,
directness: draft.directness,
signedA: true,
signedB: !mp, // solo: auto-co-sign as "both parties"
signedAt: new Date().toISOString(),
};
CG.session.setShared({ style });
};
// Guest · co-sign what host proposed
const guestCosign = () => {
if (!proposed) return;
// Guest sends a patch up to host; host-authoritative setShared also runs via data channel.
CG.session.setShared({
style: { ...proposed, signedB: true, signedAt: proposed.signedAt },
});
};
// Guest · request a change (wipes host signature so host can re-propose)
const guestRequestChange = () => {
if (!proposed) return;
CG.session.setShared({ style: null });
};
const continueToTopic = () => { onPhase && onPhase('topic'); };
// Presence: broadcast what this user is doing within the style phase
React.useEffect(() => {
let activity = 'drafting style';
if (bothSigned) activity = 'reviewing · locked';
else if (isHost && hostSigned && !guestSigned) activity = `waiting on ${nameB}`;
else if (isGuest && hostSigned && !guestSigned) activity = 'reviewing proposal';
else if (isGuest && !hostSigned) activity = `waiting on ${nameA}`;
else if (isHost && !hostSigned) activity = 'drafting style';
try { CG.session.setCursor && CG.session.setCursor({ activity }); } catch (e) {}
}, [bothSigned, hostSigned, guestSigned, isHost, isGuest, nameA, nameB]);
// ---- VIEWS ----
if (bothSigned) {
return (
three choices shape how the agents in this session talk. pick them now, together, once.{' '} once co-signed, the style is locked for the whole session — no quiet mid-argument edits.
they see your proposal below. if they request a change, you can re-propose.
before the room opens, the host picks three knobs — voice, asymmetry policy, directness — and proposes them to you. you co-sign, or request a change. neither of you can edit mid-session.
read what this will actually do to the agents (below), and either co-sign as {nameB} or send it back for a rework.
{text.trim() || '(pick all three to preview)'}
);
}
Object.assign(window, { StyleLockPhase });
console.log('[phases-style] loaded');