@holostaff/sdk 0.4.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -11
- package/dist/index.js.map +1 -1
- package/dist/presence/chip.d.ts +37 -0
- package/dist/presence/chip.d.ts.map +1 -0
- package/dist/presence/chip.js +348 -0
- package/dist/presence/chip.js.map +1 -0
- package/dist/presence/index.d.ts +35 -0
- package/dist/presence/index.d.ts.map +1 -0
- package/dist/presence/index.js +81 -0
- package/dist/presence/index.js.map +1 -0
- package/dist/presence/note.d.ts +47 -0
- package/dist/presence/note.d.ts.map +1 -0
- package/dist/presence/note.js +501 -0
- package/dist/presence/note.js.map +1 -0
- package/dist/presence/types.d.ts +69 -0
- package/dist/presence/types.d.ts.map +1 -0
- package/dist/presence/types.js +8 -0
- package/dist/presence/types.js.map +1 -0
- package/dist/stage/anamFace.d.ts +32 -0
- package/dist/stage/anamFace.d.ts.map +1 -0
- package/dist/stage/anamFace.js +95 -0
- package/dist/stage/anamFace.js.map +1 -0
- package/dist/stage/index.d.ts +39 -0
- package/dist/stage/index.d.ts.map +1 -0
- package/dist/stage/index.js +182 -0
- package/dist/stage/index.js.map +1 -0
- package/dist/stage/realtime.d.ts +50 -0
- package/dist/stage/realtime.d.ts.map +1 -0
- package/dist/stage/realtime.js +165 -0
- package/dist/stage/realtime.js.map +1 -0
- package/dist/stage/ui.d.ts +27 -0
- package/dist/stage/ui.d.ts.map +1 -0
- package/dist/stage/ui.js +176 -0
- package/dist/stage/ui.js.map +1 -0
- package/dist/theater/index.d.ts +46 -0
- package/dist/theater/index.d.ts.map +1 -0
- package/dist/theater/index.js +222 -0
- package/dist/theater/index.js.map +1 -0
- package/dist/transport.d.ts +6 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +26 -0
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/dist/widget.d.ts +0 -38
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -182
- package/dist/widget.js.map +0 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence binding — P2. Subscribes to the `presence` SSE event and
|
|
3
|
+
* drives the living-portrait chip's identity. Holds the current copilot
|
|
4
|
+
* + design tokens as shared state the note (P3), handoff ritual (P4),
|
|
5
|
+
* and Stage (P6) all read.
|
|
6
|
+
*
|
|
7
|
+
* The chip is the resting magnitude of the copilot; everything else
|
|
8
|
+
* (note, Stage, Theater) grows from it, so this binding is the anchor
|
|
9
|
+
* the other surfaces attach to.
|
|
10
|
+
*/
|
|
11
|
+
import type { TransportConfig } from '../transport.js';
|
|
12
|
+
import { type ChipInstance } from './chip.js';
|
|
13
|
+
import type { DesignTokens, PresenceCopilot, PresenceOptions } from './types.js';
|
|
14
|
+
export interface PresenceDeps {
|
|
15
|
+
cfg: TransportConfig;
|
|
16
|
+
getSessionId: () => string;
|
|
17
|
+
buildBody: () => Record<string, unknown>;
|
|
18
|
+
/** The SDK's already-open SSE channel. */
|
|
19
|
+
source: EventSource | null;
|
|
20
|
+
options?: PresenceOptions;
|
|
21
|
+
}
|
|
22
|
+
export interface PresenceBinding {
|
|
23
|
+
dispose(): void;
|
|
24
|
+
/** The live chip instance (note/Stage anchor + gesture control). */
|
|
25
|
+
chip: ChipInstance | null;
|
|
26
|
+
/** Current copilot identity, or null when none covers the stage. */
|
|
27
|
+
getCopilot(): PresenceCopilot | null;
|
|
28
|
+
/** Host design tokens from the last presence event (Theater theming). */
|
|
29
|
+
getDesignTokens(): DesignTokens | null;
|
|
30
|
+
/** Register a click→activate handler (the Stage, wired in P6). */
|
|
31
|
+
onActivate(handler: (copilot: PresenceCopilot) => void): void;
|
|
32
|
+
}
|
|
33
|
+
export declare function startPresence(deps: PresenceDeps): PresenceBinding;
|
|
34
|
+
export type { PresenceCopilot, DesignTokens, PresenceOptions } from './types.js';
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/presence/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACtD,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,WAAW,CAAA;AACxD,OAAO,KAAK,EACW,YAAY,EAAE,eAAe,EAAiB,eAAe,EACnF,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,eAAe,CAAA;IACpB,YAAY,EAAE,MAAM,MAAM,CAAA;IAC1B,SAAS,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,0CAA0C;IAC1C,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;IAC1B,OAAO,CAAC,EAAE,eAAe,CAAA;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,IAAI,IAAI,CAAA;IACf,oEAAoE;IACpE,IAAI,EAAE,YAAY,GAAG,IAAI,CAAA;IACzB,oEAAoE;IACpE,UAAU,IAAI,eAAe,GAAG,IAAI,CAAA;IACpC,yEAAyE;IACzE,eAAe,IAAI,YAAY,GAAG,IAAI,CAAA;IACtC,kEAAkE;IAClE,UAAU,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,IAAI,CAAA;CAC9D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,CA6DjE;AAYD,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence binding — P2. Subscribes to the `presence` SSE event and
|
|
3
|
+
* drives the living-portrait chip's identity. Holds the current copilot
|
|
4
|
+
* + design tokens as shared state the note (P3), handoff ritual (P4),
|
|
5
|
+
* and Stage (P6) all read.
|
|
6
|
+
*
|
|
7
|
+
* The chip is the resting magnitude of the copilot; everything else
|
|
8
|
+
* (note, Stage, Theater) grows from it, so this binding is the anchor
|
|
9
|
+
* the other surfaces attach to.
|
|
10
|
+
*/
|
|
11
|
+
import { startChip } from './chip.js';
|
|
12
|
+
export function startPresence(deps) {
|
|
13
|
+
const opts = deps.options ?? {};
|
|
14
|
+
if (opts.enabled === false || typeof window === 'undefined' || !deps.source) {
|
|
15
|
+
return inertBinding();
|
|
16
|
+
}
|
|
17
|
+
let copilot = null;
|
|
18
|
+
let tokens = null;
|
|
19
|
+
let activateHandler = null;
|
|
20
|
+
const chip = startChip({
|
|
21
|
+
dock: opts.dock ?? 'bottom-right',
|
|
22
|
+
offset: opts.offset ?? 16,
|
|
23
|
+
onActivate: (c) => activateHandler?.(c),
|
|
24
|
+
});
|
|
25
|
+
// `presence` — set identity, no ritual (connect / reconnect).
|
|
26
|
+
const onPresence = (raw) => {
|
|
27
|
+
let event = null;
|
|
28
|
+
try {
|
|
29
|
+
event = JSON.parse(raw.data);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!event)
|
|
35
|
+
return;
|
|
36
|
+
copilot = event.copilot ?? null;
|
|
37
|
+
tokens = event.designTokens ?? null;
|
|
38
|
+
chip.setCopilot(copilot, tokens);
|
|
39
|
+
};
|
|
40
|
+
// `copilot_changed` — the handoff ritual (P4): crossfade to the new
|
|
41
|
+
// copilot + greeting gesture. First meeting waves; otherwise nods.
|
|
42
|
+
// (The intro note is rendered by the note binding off the same event.)
|
|
43
|
+
const onCopilotChanged = (raw) => {
|
|
44
|
+
let event = null;
|
|
45
|
+
try {
|
|
46
|
+
event = JSON.parse(raw.data);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!event || !event.copilot)
|
|
52
|
+
return;
|
|
53
|
+
copilot = event.copilot;
|
|
54
|
+
tokens = event.designTokens ?? tokens;
|
|
55
|
+
chip.setCopilot(copilot, tokens);
|
|
56
|
+
chip.playGesture(event.firstMeeting ? 'wave' : 'nod');
|
|
57
|
+
};
|
|
58
|
+
deps.source.addEventListener('presence', onPresence);
|
|
59
|
+
deps.source.addEventListener('copilot_changed', onCopilotChanged);
|
|
60
|
+
return {
|
|
61
|
+
chip,
|
|
62
|
+
dispose() {
|
|
63
|
+
deps.source?.removeEventListener('presence', onPresence);
|
|
64
|
+
deps.source?.removeEventListener('copilot_changed', onCopilotChanged);
|
|
65
|
+
chip.destroy();
|
|
66
|
+
},
|
|
67
|
+
getCopilot: () => copilot,
|
|
68
|
+
getDesignTokens: () => tokens,
|
|
69
|
+
onActivate(handler) { activateHandler = handler; },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function inertBinding() {
|
|
73
|
+
return {
|
|
74
|
+
chip: null,
|
|
75
|
+
dispose: () => { },
|
|
76
|
+
getCopilot: () => null,
|
|
77
|
+
getDesignTokens: () => null,
|
|
78
|
+
onActivate: () => { },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/presence/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The note — the initiative magnitude (copilot-presence-design.md §2.2).
|
|
3
|
+
*
|
|
4
|
+
* The written suggestion the portrait "leaves for you." Replaces the v1
|
|
5
|
+
* widget card (sdk/src/widget.ts). Grows from the chip, the chip glances
|
|
6
|
+
* at it on arrival, and its ignore timing is *attention-gated* — the
|
|
7
|
+
* countdown starts only once there's evidence the visitor is conscious of
|
|
8
|
+
* the page, so reading slowly or being deep in a task never reads as
|
|
9
|
+
* "ignored."
|
|
10
|
+
*
|
|
11
|
+
* ✕ → dismissed
|
|
12
|
+
* attention-gated timeout → ignored
|
|
13
|
+
* "Got it" / action verb → engaged
|
|
14
|
+
* "Show me" (P5 Theater) → engaged + opens the Theater
|
|
15
|
+
* "Talk" (P6 Stage) → engaged + opens the Stage
|
|
16
|
+
*
|
|
17
|
+
* Carried from v1: stale-route guard, one-note-at-a-time, animated out +
|
|
18
|
+
* removed from the DOM, aria-live=polite, reduced-motion variants.
|
|
19
|
+
*/
|
|
20
|
+
import { type TransportConfig } from '../transport.js';
|
|
21
|
+
import type { DockCorner } from '../types.js';
|
|
22
|
+
import type { PresenceBinding } from './index.js';
|
|
23
|
+
export interface NoteDeps {
|
|
24
|
+
cfg: TransportConfig;
|
|
25
|
+
getSessionId: () => string;
|
|
26
|
+
buildBody: () => Record<string, unknown>;
|
|
27
|
+
source: EventSource | null;
|
|
28
|
+
/** Chip anchor + gesture control + copilot identity. */
|
|
29
|
+
presence: PresenceBinding;
|
|
30
|
+
dock: DockCorner;
|
|
31
|
+
}
|
|
32
|
+
export interface NoteBinding {
|
|
33
|
+
dispose(): void;
|
|
34
|
+
/** Register the "Show me" → Theater handler (P5). */
|
|
35
|
+
onShow(handler: NoteActionHandler): void;
|
|
36
|
+
/** Register the "Talk" → Stage handler (P6). */
|
|
37
|
+
onTalk(handler: NoteActionHandler): void;
|
|
38
|
+
}
|
|
39
|
+
export type NoteActionHandler = (ctx: NoteActionContext) => void;
|
|
40
|
+
export interface NoteActionContext {
|
|
41
|
+
interventionId: string;
|
|
42
|
+
goal: string;
|
|
43
|
+
content: string;
|
|
44
|
+
stage: string | null;
|
|
45
|
+
}
|
|
46
|
+
export declare function startNote(deps: NoteDeps): NoteBinding;
|
|
47
|
+
//# sourceMappingURL=note.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"note.d.ts","sourceRoot":"","sources":["../../src/presence/note.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAY,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAChE,OAAO,KAAK,EAAuB,UAAU,EAAE,MAAM,aAAa,CAAA;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAcjD,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,eAAe,CAAA;IACpB,YAAY,EAAE,MAAM,MAAM,CAAA;IAC1B,SAAS,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;IAC1B,wDAAwD;IACxD,QAAQ,EAAE,eAAe,CAAA;IACzB,IAAI,EAAE,UAAU,CAAA;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,IAAI,IAAI,CAAA;IACf,qDAAqD;IACrD,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACxC,gDAAgD;IAChD,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAA;CACzC;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,CAAA;AAChE,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAYD,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,CAiFrD"}
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The note — the initiative magnitude (copilot-presence-design.md §2.2).
|
|
3
|
+
*
|
|
4
|
+
* The written suggestion the portrait "leaves for you." Replaces the v1
|
|
5
|
+
* widget card (sdk/src/widget.ts). Grows from the chip, the chip glances
|
|
6
|
+
* at it on arrival, and its ignore timing is *attention-gated* — the
|
|
7
|
+
* countdown starts only once there's evidence the visitor is conscious of
|
|
8
|
+
* the page, so reading slowly or being deep in a task never reads as
|
|
9
|
+
* "ignored."
|
|
10
|
+
*
|
|
11
|
+
* ✕ → dismissed
|
|
12
|
+
* attention-gated timeout → ignored
|
|
13
|
+
* "Got it" / action verb → engaged
|
|
14
|
+
* "Show me" (P5 Theater) → engaged + opens the Theater
|
|
15
|
+
* "Talk" (P6 Stage) → engaged + opens the Stage
|
|
16
|
+
*
|
|
17
|
+
* Carried from v1: stale-route guard, one-note-at-a-time, animated out +
|
|
18
|
+
* removed from the DOM, aria-live=polite, reduced-motion variants.
|
|
19
|
+
*/
|
|
20
|
+
import { postJson } from '../transport.js';
|
|
21
|
+
/** Attention-gated ignore budget (design §2.2): countdown of 45s, armed
|
|
22
|
+
* on the first interaction-after-render or a 15s grace, paused on
|
|
23
|
+
* hover/focus + hidden tab. */
|
|
24
|
+
const IGNORE_BUDGET_MS = 45000;
|
|
25
|
+
const ARM_GRACE_MS = 15000;
|
|
26
|
+
const ENTER_MS = 260;
|
|
27
|
+
const EXIT_MS = 200;
|
|
28
|
+
/** Intro note (P4 handoff) auto-dismiss — design §4. */
|
|
29
|
+
const INTRO_DISMISS_MS = 12000;
|
|
30
|
+
const CONTAINER_ID = 'holostaff-note-root';
|
|
31
|
+
export function startNote(deps) {
|
|
32
|
+
if (typeof window === 'undefined' || !deps.source) {
|
|
33
|
+
return { dispose: () => { }, onShow: () => { }, onTalk: () => { } };
|
|
34
|
+
}
|
|
35
|
+
let current = null;
|
|
36
|
+
let showHandler = null;
|
|
37
|
+
let talkHandler = null;
|
|
38
|
+
const reportOutcome = (interventionId, outcome) => {
|
|
39
|
+
void postJson(deps.cfg, `/api/runtime/sessions/${deps.getSessionId()}/outcome`, {
|
|
40
|
+
...deps.buildBody(),
|
|
41
|
+
interventionId,
|
|
42
|
+
outcome,
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
const onFire = (raw) => {
|
|
46
|
+
let parsed = null;
|
|
47
|
+
try {
|
|
48
|
+
const data = JSON.parse(raw.data);
|
|
49
|
+
if (!data || typeof data.interventionId !== 'string')
|
|
50
|
+
return;
|
|
51
|
+
parsed = data;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!parsed)
|
|
57
|
+
return;
|
|
58
|
+
// Modality gate — text renders a normal note; voice_presentation
|
|
59
|
+
// renders a note whose primary CTA is "Show me" → the Theater (P5).
|
|
60
|
+
if (parsed.modality !== 'text' && parsed.modality !== 'voice_presentation') {
|
|
61
|
+
deps.cfg.onError?.(new Error(`Modality '${parsed.modality}' is not supported in this SDK build`), { method: 'NOTE', path: '(fire_intervention)' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Stale-fire guard — decided for a route the visitor already left.
|
|
65
|
+
if (parsed.route) {
|
|
66
|
+
const decidedPath = parsed.route.split('?')[0];
|
|
67
|
+
if (decidedPath && decidedPath !== window.location.pathname) {
|
|
68
|
+
reportOutcome(parsed.interventionId, 'ignored');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// One note at a time — retire the old as 'ignored'.
|
|
73
|
+
if (current)
|
|
74
|
+
current.close('ignored');
|
|
75
|
+
current = renderNote(deps, parsed, { showHandler, talkHandler }, () => {
|
|
76
|
+
if (current && current.event.interventionId === parsed.interventionId)
|
|
77
|
+
current = null;
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
// Intro note on first meeting (P4). Never interrupts an open note
|
|
81
|
+
// (design §4: "never mid-card"); the intro is low-stakes + once-per-pair.
|
|
82
|
+
const onCopilotChanged = (raw) => {
|
|
83
|
+
let event = null;
|
|
84
|
+
try {
|
|
85
|
+
event = JSON.parse(raw.data);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!event || !event.firstMeeting || !event.copilot?.intro)
|
|
91
|
+
return;
|
|
92
|
+
if (current)
|
|
93
|
+
return; // a card is open — skip rather than queue
|
|
94
|
+
current = renderIntro(deps, event, () => { current = null; });
|
|
95
|
+
};
|
|
96
|
+
deps.source.addEventListener('fire_intervention', onFire);
|
|
97
|
+
deps.source.addEventListener('copilot_changed', onCopilotChanged);
|
|
98
|
+
return {
|
|
99
|
+
dispose() {
|
|
100
|
+
deps.source?.removeEventListener('fire_intervention', onFire);
|
|
101
|
+
deps.source?.removeEventListener('copilot_changed', onCopilotChanged);
|
|
102
|
+
current?.detach();
|
|
103
|
+
current = null;
|
|
104
|
+
},
|
|
105
|
+
onShow(handler) { showHandler = handler; },
|
|
106
|
+
onTalk(handler) { talkHandler = handler; },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function renderNote(deps, event, handlers, onClosed) {
|
|
110
|
+
const container = ensureContainer();
|
|
111
|
+
positionContainer(container, deps);
|
|
112
|
+
const root = container.shadowRoot;
|
|
113
|
+
root.innerHTML = '';
|
|
114
|
+
const reducedMotion = typeof window.matchMedia === 'function'
|
|
115
|
+
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
116
|
+
const copilotName = event.copilot?.name?.trim() || 'Copilot';
|
|
117
|
+
const roleLine = event.copilot?.roleLine?.trim() || '';
|
|
118
|
+
const avatarUrl = event.copilot?.avatar?.trim() || '';
|
|
119
|
+
const card = document.createElement('div');
|
|
120
|
+
card.className = `note enter ${deps.dock}`;
|
|
121
|
+
card.setAttribute('role', 'status');
|
|
122
|
+
card.setAttribute('aria-live', 'polite');
|
|
123
|
+
// Header: avatar + name·role + ✕
|
|
124
|
+
const head = document.createElement('div');
|
|
125
|
+
head.className = 'head';
|
|
126
|
+
if (avatarUrl) {
|
|
127
|
+
const img = document.createElement('img');
|
|
128
|
+
img.className = 'avatar';
|
|
129
|
+
img.src = avatarUrl;
|
|
130
|
+
img.alt = '';
|
|
131
|
+
img.referrerPolicy = 'no-referrer';
|
|
132
|
+
img.addEventListener('error', () => img.replaceWith(monogram(copilotName)));
|
|
133
|
+
head.appendChild(img);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
head.appendChild(monogram(copilotName));
|
|
137
|
+
}
|
|
138
|
+
const idBlock = document.createElement('div');
|
|
139
|
+
idBlock.className = 'idblock';
|
|
140
|
+
const nameEl = document.createElement('div');
|
|
141
|
+
nameEl.className = 'name';
|
|
142
|
+
nameEl.textContent = copilotName;
|
|
143
|
+
idBlock.appendChild(nameEl);
|
|
144
|
+
if (roleLine) {
|
|
145
|
+
const roleEl = document.createElement('div');
|
|
146
|
+
roleEl.className = 'role';
|
|
147
|
+
roleEl.textContent = roleLine;
|
|
148
|
+
idBlock.appendChild(roleEl);
|
|
149
|
+
}
|
|
150
|
+
head.appendChild(idBlock);
|
|
151
|
+
const x = document.createElement('button');
|
|
152
|
+
x.className = 'x';
|
|
153
|
+
x.type = 'button';
|
|
154
|
+
x.setAttribute('aria-label', 'Dismiss');
|
|
155
|
+
x.textContent = '✕';
|
|
156
|
+
head.appendChild(x);
|
|
157
|
+
// Body
|
|
158
|
+
const message = document.createElement('div');
|
|
159
|
+
message.className = 'message';
|
|
160
|
+
message.textContent = event.content || event.goal;
|
|
161
|
+
// Actions. A voice_presentation note leads with "Show me" (→ Theater)
|
|
162
|
+
// when the Theater is wired; otherwise it gracefully degrades to "Got
|
|
163
|
+
// it" (the copilot tells instead of shows — theater doc §4.1). "Talk"
|
|
164
|
+
// (→ Stage, P6) is offered whenever voice is wired.
|
|
165
|
+
const isPresent = event.modality === 'voice_presentation';
|
|
166
|
+
const actions = document.createElement('div');
|
|
167
|
+
actions.className = 'actions';
|
|
168
|
+
if (handlers.talkHandler) {
|
|
169
|
+
actions.appendChild(makeBtn('secondary', 'Talk', () => {
|
|
170
|
+
handlers.talkHandler?.(toActionCtx(event));
|
|
171
|
+
instance.close('engaged');
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
if (isPresent && handlers.showHandler) {
|
|
175
|
+
actions.appendChild(makeBtn('primary', 'Show me', () => {
|
|
176
|
+
handlers.showHandler?.(toActionCtx(event));
|
|
177
|
+
instance.close('engaged');
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
actions.appendChild(makeBtn('primary', 'Got it', () => instance.close('engaged')));
|
|
182
|
+
}
|
|
183
|
+
card.append(head, message, actions);
|
|
184
|
+
root.append(styleNode(), card);
|
|
185
|
+
// Entry — grow from the chip. The portrait glances at the note.
|
|
186
|
+
if (reducedMotion) {
|
|
187
|
+
card.classList.remove('enter');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
requestAnimationFrame(() => requestAnimationFrame(() => card.classList.remove('enter')));
|
|
191
|
+
}
|
|
192
|
+
deps.presence.chip?.playGesture(deps.presence.chip.innerGlance());
|
|
193
|
+
// -- Attention-gated ignore timer -----------------------------------
|
|
194
|
+
let closed = false;
|
|
195
|
+
let armed = false;
|
|
196
|
+
let remainingMs = IGNORE_BUDGET_MS;
|
|
197
|
+
let runTimer = null;
|
|
198
|
+
let startedAt = 0;
|
|
199
|
+
let paused = false;
|
|
200
|
+
const tick = () => {
|
|
201
|
+
if (closed || !armed || paused)
|
|
202
|
+
return;
|
|
203
|
+
startedAt = Date.now();
|
|
204
|
+
runTimer = setTimeout(() => instance.close('ignored'), remainingMs);
|
|
205
|
+
};
|
|
206
|
+
const pause = () => {
|
|
207
|
+
paused = true;
|
|
208
|
+
if (runTimer != null) {
|
|
209
|
+
clearTimeout(runTimer);
|
|
210
|
+
runTimer = null;
|
|
211
|
+
remainingMs = Math.max(1000, remainingMs - (Date.now() - startedAt));
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const resume = () => { if (paused) {
|
|
215
|
+
paused = false;
|
|
216
|
+
tick();
|
|
217
|
+
} };
|
|
218
|
+
const arm = () => {
|
|
219
|
+
if (armed || closed)
|
|
220
|
+
return;
|
|
221
|
+
armed = true;
|
|
222
|
+
window.removeEventListener('pointerdown', arm);
|
|
223
|
+
window.removeEventListener('scroll', arm);
|
|
224
|
+
window.removeEventListener('keydown', arm);
|
|
225
|
+
if (graceTimer != null) {
|
|
226
|
+
clearTimeout(graceTimer);
|
|
227
|
+
graceTimer = null;
|
|
228
|
+
}
|
|
229
|
+
if (!document.hidden)
|
|
230
|
+
tick();
|
|
231
|
+
};
|
|
232
|
+
// Arm on first conscious interaction, or after the grace window.
|
|
233
|
+
window.addEventListener('pointerdown', arm, { passive: true });
|
|
234
|
+
window.addEventListener('scroll', arm, { passive: true });
|
|
235
|
+
window.addEventListener('keydown', arm);
|
|
236
|
+
let graceTimer = setTimeout(arm, ARM_GRACE_MS);
|
|
237
|
+
// Pause while reading (hover/focus) or while the tab is hidden.
|
|
238
|
+
card.addEventListener('mouseenter', pause);
|
|
239
|
+
card.addEventListener('focusin', pause);
|
|
240
|
+
card.addEventListener('mouseleave', resume);
|
|
241
|
+
card.addEventListener('focusout', resume);
|
|
242
|
+
const onVis = () => { if (document.hidden)
|
|
243
|
+
pause();
|
|
244
|
+
else if (armed)
|
|
245
|
+
resume(); };
|
|
246
|
+
document.addEventListener('visibilitychange', onVis);
|
|
247
|
+
const sendOutcome = (outcome) => {
|
|
248
|
+
void postJson(deps.cfg, `/api/runtime/sessions/${deps.getSessionId()}/outcome`, {
|
|
249
|
+
...deps.buildBody(),
|
|
250
|
+
interventionId: event.interventionId,
|
|
251
|
+
outcome,
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
const teardown = () => {
|
|
255
|
+
if (runTimer != null)
|
|
256
|
+
clearTimeout(runTimer);
|
|
257
|
+
if (graceTimer != null)
|
|
258
|
+
clearTimeout(graceTimer);
|
|
259
|
+
window.removeEventListener('pointerdown', arm);
|
|
260
|
+
window.removeEventListener('scroll', arm);
|
|
261
|
+
window.removeEventListener('keydown', arm);
|
|
262
|
+
document.removeEventListener('visibilitychange', onVis);
|
|
263
|
+
};
|
|
264
|
+
const doClose = (outcome) => {
|
|
265
|
+
if (closed)
|
|
266
|
+
return;
|
|
267
|
+
closed = true;
|
|
268
|
+
teardown();
|
|
269
|
+
if (outcome)
|
|
270
|
+
sendOutcome(outcome);
|
|
271
|
+
const remove = () => {
|
|
272
|
+
try {
|
|
273
|
+
root.removeChild(card);
|
|
274
|
+
}
|
|
275
|
+
catch { /* gone */ }
|
|
276
|
+
onClosed();
|
|
277
|
+
};
|
|
278
|
+
if (reducedMotion) {
|
|
279
|
+
remove();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
card.classList.add('exit'); // collapse toward the chip
|
|
283
|
+
let removed = false;
|
|
284
|
+
const once = () => { if (!removed) {
|
|
285
|
+
removed = true;
|
|
286
|
+
remove();
|
|
287
|
+
} };
|
|
288
|
+
card.addEventListener('transitionend', once, { once: true });
|
|
289
|
+
setTimeout(once, EXIT_MS + 80);
|
|
290
|
+
};
|
|
291
|
+
x.addEventListener('click', () => doClose('dismissed'));
|
|
292
|
+
const instance = {
|
|
293
|
+
event,
|
|
294
|
+
close(outcome) { doClose(outcome); },
|
|
295
|
+
detach() { doClose(null); },
|
|
296
|
+
};
|
|
297
|
+
return instance;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Intro note (P4) — the first-meeting greeting card. ✕-only, auto-dismiss
|
|
301
|
+
* in 12s, no outcome reporting (it's a presence event, not an
|
|
302
|
+
* intervention — it bypasses budgets server-side).
|
|
303
|
+
*/
|
|
304
|
+
function renderIntro(deps, event, onClosed) {
|
|
305
|
+
const container = ensureContainer();
|
|
306
|
+
positionContainer(container, deps);
|
|
307
|
+
const root = container.shadowRoot;
|
|
308
|
+
root.innerHTML = '';
|
|
309
|
+
const reducedMotion = typeof window.matchMedia === 'function'
|
|
310
|
+
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
311
|
+
const name = event.copilot.name?.trim() || 'Copilot';
|
|
312
|
+
const roleLine = event.copilot.roleLine?.trim() || '';
|
|
313
|
+
const avatarUrl = event.copilot.avatar?.trim() || '';
|
|
314
|
+
const card = document.createElement('div');
|
|
315
|
+
card.className = `note enter ${deps.dock}`;
|
|
316
|
+
card.setAttribute('role', 'status');
|
|
317
|
+
card.setAttribute('aria-live', 'polite');
|
|
318
|
+
const head = document.createElement('div');
|
|
319
|
+
head.className = 'head';
|
|
320
|
+
if (avatarUrl) {
|
|
321
|
+
const img = document.createElement('img');
|
|
322
|
+
img.className = 'avatar';
|
|
323
|
+
img.src = avatarUrl;
|
|
324
|
+
img.alt = '';
|
|
325
|
+
img.referrerPolicy = 'no-referrer';
|
|
326
|
+
img.addEventListener('error', () => img.replaceWith(monogram(name)));
|
|
327
|
+
head.appendChild(img);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
head.appendChild(monogram(name));
|
|
331
|
+
}
|
|
332
|
+
const idBlock = document.createElement('div');
|
|
333
|
+
idBlock.className = 'idblock';
|
|
334
|
+
const nameEl = document.createElement('div');
|
|
335
|
+
nameEl.className = 'name';
|
|
336
|
+
nameEl.textContent = name;
|
|
337
|
+
idBlock.appendChild(nameEl);
|
|
338
|
+
if (roleLine) {
|
|
339
|
+
const roleEl = document.createElement('div');
|
|
340
|
+
roleEl.className = 'role';
|
|
341
|
+
roleEl.textContent = roleLine;
|
|
342
|
+
idBlock.appendChild(roleEl);
|
|
343
|
+
}
|
|
344
|
+
head.appendChild(idBlock);
|
|
345
|
+
const x = document.createElement('button');
|
|
346
|
+
x.className = 'x';
|
|
347
|
+
x.type = 'button';
|
|
348
|
+
x.setAttribute('aria-label', 'Dismiss');
|
|
349
|
+
x.textContent = '✕';
|
|
350
|
+
head.appendChild(x);
|
|
351
|
+
const message = document.createElement('div');
|
|
352
|
+
message.className = 'message';
|
|
353
|
+
message.textContent = event.copilot.intro;
|
|
354
|
+
card.append(head, message);
|
|
355
|
+
root.append(styleNode(), card);
|
|
356
|
+
if (reducedMotion)
|
|
357
|
+
card.classList.remove('enter');
|
|
358
|
+
else
|
|
359
|
+
requestAnimationFrame(() => requestAnimationFrame(() => card.classList.remove('enter')));
|
|
360
|
+
let closed = false;
|
|
361
|
+
let dismissTimer = setTimeout(() => instance.close(null), INTRO_DISMISS_MS);
|
|
362
|
+
const doClose = () => {
|
|
363
|
+
if (closed)
|
|
364
|
+
return;
|
|
365
|
+
closed = true;
|
|
366
|
+
if (dismissTimer != null) {
|
|
367
|
+
clearTimeout(dismissTimer);
|
|
368
|
+
dismissTimer = null;
|
|
369
|
+
}
|
|
370
|
+
const remove = () => {
|
|
371
|
+
try {
|
|
372
|
+
root.removeChild(card);
|
|
373
|
+
}
|
|
374
|
+
catch { /* gone */ }
|
|
375
|
+
onClosed();
|
|
376
|
+
};
|
|
377
|
+
if (reducedMotion) {
|
|
378
|
+
remove();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
card.classList.add('exit');
|
|
382
|
+
let removed = false;
|
|
383
|
+
const once = () => { if (!removed) {
|
|
384
|
+
removed = true;
|
|
385
|
+
remove();
|
|
386
|
+
} };
|
|
387
|
+
card.addEventListener('transitionend', once, { once: true });
|
|
388
|
+
setTimeout(once, EXIT_MS + 80);
|
|
389
|
+
};
|
|
390
|
+
x.addEventListener('click', doClose);
|
|
391
|
+
const instance = {
|
|
392
|
+
event: { interventionId: `intro_${event.copilot.copilotId}`, modality: 'text', content: event.copilot.intro, goal: '', stage: event.stage },
|
|
393
|
+
close() { doClose(); },
|
|
394
|
+
detach() { doClose(); },
|
|
395
|
+
};
|
|
396
|
+
return instance;
|
|
397
|
+
}
|
|
398
|
+
function toActionCtx(e) {
|
|
399
|
+
return { interventionId: e.interventionId, goal: e.goal, content: e.content, stage: e.stage };
|
|
400
|
+
}
|
|
401
|
+
function makeBtn(kind, label, onClick) {
|
|
402
|
+
const b = document.createElement('button');
|
|
403
|
+
b.className = `btn ${kind}`;
|
|
404
|
+
b.type = 'button';
|
|
405
|
+
b.textContent = label;
|
|
406
|
+
b.addEventListener('click', onClick);
|
|
407
|
+
return b;
|
|
408
|
+
}
|
|
409
|
+
function monogram(name) {
|
|
410
|
+
const el = document.createElement('div');
|
|
411
|
+
el.className = 'avatar mono';
|
|
412
|
+
el.textContent = (name.trim()[0] ?? '?').toUpperCase();
|
|
413
|
+
return el;
|
|
414
|
+
}
|
|
415
|
+
function ensureContainer() {
|
|
416
|
+
let el = document.getElementById(CONTAINER_ID);
|
|
417
|
+
if (el)
|
|
418
|
+
return el;
|
|
419
|
+
el = document.createElement('div');
|
|
420
|
+
el.id = CONTAINER_ID;
|
|
421
|
+
el.style.cssText = 'position:fixed;z-index:2147483646;pointer-events:none';
|
|
422
|
+
el.attachShadow({ mode: 'open' });
|
|
423
|
+
document.body.appendChild(el);
|
|
424
|
+
return el;
|
|
425
|
+
}
|
|
426
|
+
/** Anchor the note to the chip — grows from it, aligned to its outer
|
|
427
|
+
* edge, sitting just inside it toward viewport center. */
|
|
428
|
+
function positionContainer(el, deps) {
|
|
429
|
+
const gap = 12;
|
|
430
|
+
const rect = deps.presence.chip?.getAnchorRect() ?? null;
|
|
431
|
+
// Reset.
|
|
432
|
+
el.style.top = el.style.bottom = el.style.left = el.style.right = '';
|
|
433
|
+
if (rect) {
|
|
434
|
+
if (deps.dock.startsWith('bottom')) {
|
|
435
|
+
el.style.bottom = `${Math.max(8, window.innerHeight - rect.top + gap)}px`;
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
el.style.top = `${Math.max(8, rect.bottom + gap)}px`;
|
|
439
|
+
}
|
|
440
|
+
if (deps.dock.endsWith('right')) {
|
|
441
|
+
el.style.right = `${Math.max(8, window.innerWidth - rect.right)}px`;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
el.style.left = `${Math.max(8, rect.left)}px`;
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// Fallback: dock corner with room for the chip.
|
|
449
|
+
const off = 16;
|
|
450
|
+
if (deps.dock.startsWith('bottom'))
|
|
451
|
+
el.style.bottom = `${off + 64}px`;
|
|
452
|
+
else
|
|
453
|
+
el.style.top = `${off + 64}px`;
|
|
454
|
+
if (deps.dock.endsWith('right'))
|
|
455
|
+
el.style.right = `${off}px`;
|
|
456
|
+
else
|
|
457
|
+
el.style.left = `${off}px`;
|
|
458
|
+
}
|
|
459
|
+
function styleNode() {
|
|
460
|
+
const node = document.createElement('style');
|
|
461
|
+
node.textContent = `
|
|
462
|
+
:host { all: initial; }
|
|
463
|
+
.note {
|
|
464
|
+
pointer-events: auto;
|
|
465
|
+
width: 320px; max-width: calc(100vw - 32px);
|
|
466
|
+
background: #111; color: #fafafa;
|
|
467
|
+
border-radius: 16px;
|
|
468
|
+
box-shadow: 0 12px 32px rgba(0,0,0,0.32), 0 2px 8px rgba(0,0,0,0.12);
|
|
469
|
+
padding: 12px 14px;
|
|
470
|
+
font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
471
|
+
opacity: 1; transform: scale(1) translateY(0);
|
|
472
|
+
transition: opacity ${ENTER_MS}ms ease-out, transform ${ENTER_MS}ms cubic-bezier(0.16,1,0.3,1);
|
|
473
|
+
}
|
|
474
|
+
/* Grow from the chip: origin toward the docked corner. */
|
|
475
|
+
.note.bottom-right { transform-origin: bottom right; }
|
|
476
|
+
.note.bottom-left { transform-origin: bottom left; }
|
|
477
|
+
.note.top-right { transform-origin: top right; }
|
|
478
|
+
.note.top-left { transform-origin: top left; }
|
|
479
|
+
.note.enter { opacity: 0; transform: scale(0.82) translateY(8px); }
|
|
480
|
+
.note.exit { opacity: 0; transform: scale(0.82) translateY(8px); transition-duration: ${EXIT_MS}ms; }
|
|
481
|
+
.head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
482
|
+
.avatar { width: 34px; height: 34px; border-radius: 50%; object-fit: cover; flex: none; background: #2a2a2a; }
|
|
483
|
+
.avatar.mono { display: flex; align-items: center; justify-content: center; color: #9ab8ff; font-weight: 600; font-size: 15px; }
|
|
484
|
+
.idblock { flex: 1; min-width: 0; }
|
|
485
|
+
.name { font-weight: 600; font-size: 13px; color: #e8e8e8; }
|
|
486
|
+
.role { font-size: 11px; color: #9a9a9a; margin-top: 1px; }
|
|
487
|
+
.x { appearance: none; border: 0; background: transparent; color: #8a8a8a; font-size: 13px; line-height: 1; padding: 6px; margin: -6px -4px -6px 0; border-radius: 8px; cursor: pointer; flex: none; }
|
|
488
|
+
.x:hover { color: #fff; background: rgba(255,255,255,0.08); }
|
|
489
|
+
.message { margin: 0 2px 12px; color: #f2f2f2; }
|
|
490
|
+
.actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
491
|
+
.btn { appearance: none; border: 0; padding: 7px 14px; border-radius: 9px; font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; }
|
|
492
|
+
.btn.primary { background: #4f8bff; color: #fff; }
|
|
493
|
+
.btn.primary:hover { background: #6aa0ff; }
|
|
494
|
+
.btn.secondary { background: rgba(255,255,255,0.10); color: #e8e8e8; }
|
|
495
|
+
.btn.secondary:hover { background: rgba(255,255,255,0.16); }
|
|
496
|
+
.btn:focus-visible, .x:focus-visible { outline: 2px solid #9ab8ff; outline-offset: 2px; }
|
|
497
|
+
@media (prefers-reduced-motion: reduce) { .note { transition: none; } }
|
|
498
|
+
`;
|
|
499
|
+
return node;
|
|
500
|
+
}
|
|
501
|
+
//# sourceMappingURL=note.js.map
|