@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,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime session — P6 (copilot-voice-stage-design.md §1, §2).
|
|
3
|
+
*
|
|
4
|
+
* The conversational brain + ears: GPT/Azure Realtime over WebRTC. It
|
|
5
|
+
* owns the mic (STT), runs the LLM, does turn-taking / VAD / barge-in,
|
|
6
|
+
* and — in **text mode** (modalities: ['text']) — emits TEXT deltas +
|
|
7
|
+
* tool calls over the data channel. NO audio output: the spoken voice is
|
|
8
|
+
* Anam's native TTS, fed the text (Path A). This is the de-Vue-ified
|
|
9
|
+
* port of tutorLM's useWebRTC + useToolDataChannel.
|
|
10
|
+
*/
|
|
11
|
+
export async function connectRealtime(opts, cb) {
|
|
12
|
+
const pc = new RTCPeerConnection();
|
|
13
|
+
// Mic in (STT). No remote audio track is consumed — text mode.
|
|
14
|
+
for (const track of opts.micStream.getAudioTracks())
|
|
15
|
+
pc.addTrack(track, opts.micStream);
|
|
16
|
+
const dc = pc.createDataChannel('oai-events');
|
|
17
|
+
let turnId = mintTurnId();
|
|
18
|
+
const send = (obj) => {
|
|
19
|
+
try {
|
|
20
|
+
if (dc.readyState === 'open')
|
|
21
|
+
dc.send(JSON.stringify(obj));
|
|
22
|
+
}
|
|
23
|
+
catch { /* closed */ }
|
|
24
|
+
};
|
|
25
|
+
dc.addEventListener('open', () => {
|
|
26
|
+
// Configure the session: text-only out, server VAD (barge-in),
|
|
27
|
+
// input transcription, and our tool surface.
|
|
28
|
+
send({
|
|
29
|
+
type: 'session.update',
|
|
30
|
+
session: {
|
|
31
|
+
modalities: ['text'],
|
|
32
|
+
...(opts.instructions ? { instructions: opts.instructions } : {}),
|
|
33
|
+
tools: opts.tools.map(t => ({
|
|
34
|
+
type: 'function',
|
|
35
|
+
name: t.name,
|
|
36
|
+
description: t.description,
|
|
37
|
+
parameters: t.parameters,
|
|
38
|
+
})),
|
|
39
|
+
tool_choice: 'auto',
|
|
40
|
+
input_audio_transcription: { model: 'whisper-1' },
|
|
41
|
+
turn_detection: { type: 'server_vad', threshold: 0.5, silence_duration_ms: 500 },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
// If opened from a suggestion, seed the opening turn with it.
|
|
45
|
+
if (opts.seedMessage) {
|
|
46
|
+
send({
|
|
47
|
+
type: 'conversation.item.create',
|
|
48
|
+
item: {
|
|
49
|
+
type: 'message',
|
|
50
|
+
role: 'user',
|
|
51
|
+
content: [{ type: 'input_text', text: opts.seedMessage }],
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Greet first — the copilot opens by speaking (instructions say how).
|
|
56
|
+
send({ type: 'response.create' });
|
|
57
|
+
});
|
|
58
|
+
dc.addEventListener('message', (ev) => {
|
|
59
|
+
let msg;
|
|
60
|
+
try {
|
|
61
|
+
msg = JSON.parse(ev.data);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
handleServerEvent(msg);
|
|
67
|
+
});
|
|
68
|
+
const handleServerEvent = (msg) => {
|
|
69
|
+
switch (msg.type) {
|
|
70
|
+
// Text deltas — naming varies across Realtime versions.
|
|
71
|
+
case 'response.text.delta':
|
|
72
|
+
case 'response.output_text.delta':
|
|
73
|
+
if (typeof msg.delta === 'string')
|
|
74
|
+
cb.onTextDelta(msg.delta, turnId);
|
|
75
|
+
break;
|
|
76
|
+
case 'response.text.done':
|
|
77
|
+
case 'response.output_text.done':
|
|
78
|
+
case 'response.done':
|
|
79
|
+
cb.onTurnDone(turnId);
|
|
80
|
+
turnId = mintTurnId();
|
|
81
|
+
break;
|
|
82
|
+
case 'input_audio_buffer.speech_started':
|
|
83
|
+
cb.onUserSpeechStarted();
|
|
84
|
+
break;
|
|
85
|
+
case 'response.function_call_arguments.done': {
|
|
86
|
+
const name = typeof msg.name === 'string' ? msg.name : '';
|
|
87
|
+
const callId = typeof msg.call_id === 'string' ? msg.call_id : '';
|
|
88
|
+
let args = {};
|
|
89
|
+
if (typeof msg.arguments === 'string') {
|
|
90
|
+
try {
|
|
91
|
+
args = JSON.parse(msg.arguments);
|
|
92
|
+
}
|
|
93
|
+
catch { /* keep {} */ }
|
|
94
|
+
}
|
|
95
|
+
if (name && callId)
|
|
96
|
+
cb.onToolCall({ name, args, callId });
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case 'error':
|
|
100
|
+
cb.onError(new Error(typeof msg.error === 'object' && msg.error ? JSON.stringify(msg.error) : 'realtime error'));
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
// SDP offer → answer exchange using the ephemeral key.
|
|
105
|
+
const offer = await pc.createOffer();
|
|
106
|
+
await pc.setLocalDescription(offer);
|
|
107
|
+
await waitForIceGathering(pc);
|
|
108
|
+
const sdpRes = await fetch(`${opts.realtimeUrl}?model=${encodeURIComponent(opts.realtimeModel)}`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${opts.ephemeralKey}`,
|
|
112
|
+
'Content-Type': 'application/sdp',
|
|
113
|
+
},
|
|
114
|
+
body: pc.localDescription?.sdp ?? offer.sdp ?? '',
|
|
115
|
+
});
|
|
116
|
+
if (!sdpRes.ok) {
|
|
117
|
+
pc.close();
|
|
118
|
+
throw new Error(`realtime SDP ${sdpRes.status}: ${(await sdpRes.text().catch(() => '')).slice(0, 200)}`);
|
|
119
|
+
}
|
|
120
|
+
const answer = await sdpRes.text();
|
|
121
|
+
await pc.setRemoteDescription({ type: 'answer', sdp: answer });
|
|
122
|
+
return {
|
|
123
|
+
sendToolResult(callId, output) {
|
|
124
|
+
send({
|
|
125
|
+
type: 'conversation.item.create',
|
|
126
|
+
item: {
|
|
127
|
+
type: 'function_call_output',
|
|
128
|
+
call_id: callId,
|
|
129
|
+
output: typeof output === 'string' ? output : JSON.stringify(output),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
send({ type: 'response.create' });
|
|
133
|
+
},
|
|
134
|
+
close() {
|
|
135
|
+
try {
|
|
136
|
+
dc.close();
|
|
137
|
+
}
|
|
138
|
+
catch { /* */ }
|
|
139
|
+
try {
|
|
140
|
+
pc.close();
|
|
141
|
+
}
|
|
142
|
+
catch { /* */ }
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function mintTurnId() {
|
|
147
|
+
return `turn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
148
|
+
}
|
|
149
|
+
/** Resolve once ICE gathering completes (or after a short cap) so the
|
|
150
|
+
* offer SDP carries candidates. */
|
|
151
|
+
function waitForIceGathering(pc) {
|
|
152
|
+
if (pc.iceGatheringState === 'complete')
|
|
153
|
+
return Promise.resolve();
|
|
154
|
+
return new Promise(resolve => {
|
|
155
|
+
const done = () => {
|
|
156
|
+
pc.removeEventListener('icegatheringstatechange', check);
|
|
157
|
+
resolve();
|
|
158
|
+
};
|
|
159
|
+
const check = () => { if (pc.iceGatheringState === 'complete')
|
|
160
|
+
done(); };
|
|
161
|
+
pc.addEventListener('icegatheringstatechange', check);
|
|
162
|
+
setTimeout(done, 1500); // cap — don't block on slow networks
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=realtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime.js","sourceRoot":"","sources":["../../src/stage/realtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage panel UI — P6 (copilot-presence-design.md §2.3).
|
|
3
|
+
*
|
|
4
|
+
* A bottom sheet that grows from the chip: the copilot's live Anam avatar
|
|
5
|
+
* fills it, with name + scope beneath; listening / thinking / speaking
|
|
6
|
+
* are expressed through state, not spinners. Captions (output text) are
|
|
7
|
+
* an accessibility toggle that never violates the no-text-INPUT rule.
|
|
8
|
+
* Shadow-DOM isolated, vanilla.
|
|
9
|
+
*/
|
|
10
|
+
import type { DockCorner } from '../types.js';
|
|
11
|
+
export type StageState = 'connecting' | 'listening' | 'thinking' | 'speaking';
|
|
12
|
+
export interface StageUiDeps {
|
|
13
|
+
dock: DockCorner;
|
|
14
|
+
copilotName: string;
|
|
15
|
+
scopeLine?: string;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
}
|
|
18
|
+
export interface StageUi {
|
|
19
|
+
/** The <video> Anam binds to. */
|
|
20
|
+
video: HTMLVideoElement;
|
|
21
|
+
setState(state: StageState): void;
|
|
22
|
+
appendCaption(text: string): void;
|
|
23
|
+
/** Toggle the visible state ring + label. */
|
|
24
|
+
destroy(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function startStageUi(deps: StageUiDeps): StageUi;
|
|
27
|
+
//# sourceMappingURL=ui.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/stage/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAAA;AAI7E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,UAAU,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,OAAO;IACtB,iCAAiC;IACjC,KAAK,EAAE,gBAAgB,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IACjC,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,6CAA6C;IAC7C,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAgHvD"}
|
package/dist/stage/ui.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage panel UI — P6 (copilot-presence-design.md §2.3).
|
|
3
|
+
*
|
|
4
|
+
* A bottom sheet that grows from the chip: the copilot's live Anam avatar
|
|
5
|
+
* fills it, with name + scope beneath; listening / thinking / speaking
|
|
6
|
+
* are expressed through state, not spinners. Captions (output text) are
|
|
7
|
+
* an accessibility toggle that never violates the no-text-INPUT rule.
|
|
8
|
+
* Shadow-DOM isolated, vanilla.
|
|
9
|
+
*/
|
|
10
|
+
const CONTAINER_ID = 'holostaff-stage-root';
|
|
11
|
+
export function startStageUi(deps) {
|
|
12
|
+
const container = ensureContainer();
|
|
13
|
+
const root = container.shadowRoot;
|
|
14
|
+
root.innerHTML = '';
|
|
15
|
+
root.appendChild(styleNode(deps.dock));
|
|
16
|
+
const reducedMotion = typeof window.matchMedia === 'function'
|
|
17
|
+
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
18
|
+
const sheet = document.createElement('div');
|
|
19
|
+
sheet.className = `sheet enter ${deps.dock}`;
|
|
20
|
+
sheet.setAttribute('role', 'dialog');
|
|
21
|
+
sheet.setAttribute('aria-label', `Conversation with ${deps.copilotName}`);
|
|
22
|
+
// Avatar
|
|
23
|
+
const stageArea = document.createElement('div');
|
|
24
|
+
stageArea.className = 'avatar-area';
|
|
25
|
+
const video = document.createElement('video');
|
|
26
|
+
video.className = 'avatar-video';
|
|
27
|
+
video.autoplay = true;
|
|
28
|
+
video.playsInline = true;
|
|
29
|
+
video.setAttribute('playsinline', '');
|
|
30
|
+
// Anam provides the audio; this element is NOT muted.
|
|
31
|
+
stageArea.appendChild(video);
|
|
32
|
+
const ring = document.createElement('div');
|
|
33
|
+
ring.className = 'state-ring';
|
|
34
|
+
stageArea.appendChild(ring);
|
|
35
|
+
// Header
|
|
36
|
+
const head = document.createElement('div');
|
|
37
|
+
head.className = 'head';
|
|
38
|
+
const idBlock = document.createElement('div');
|
|
39
|
+
idBlock.className = 'idblock';
|
|
40
|
+
const nameEl = document.createElement('div');
|
|
41
|
+
nameEl.className = 'name';
|
|
42
|
+
nameEl.textContent = deps.copilotName;
|
|
43
|
+
idBlock.appendChild(nameEl);
|
|
44
|
+
if (deps.scopeLine) {
|
|
45
|
+
const scope = document.createElement('div');
|
|
46
|
+
scope.className = 'scope';
|
|
47
|
+
scope.textContent = deps.scopeLine;
|
|
48
|
+
idBlock.appendChild(scope);
|
|
49
|
+
}
|
|
50
|
+
head.appendChild(idBlock);
|
|
51
|
+
const ccBtn = document.createElement('button');
|
|
52
|
+
ccBtn.className = 'cc';
|
|
53
|
+
ccBtn.type = 'button';
|
|
54
|
+
ccBtn.setAttribute('aria-pressed', 'false');
|
|
55
|
+
ccBtn.title = 'Captions';
|
|
56
|
+
ccBtn.textContent = 'CC';
|
|
57
|
+
head.appendChild(ccBtn);
|
|
58
|
+
const closeBtn = document.createElement('button');
|
|
59
|
+
closeBtn.className = 'x';
|
|
60
|
+
closeBtn.type = 'button';
|
|
61
|
+
closeBtn.setAttribute('aria-label', 'End conversation');
|
|
62
|
+
closeBtn.textContent = '✕';
|
|
63
|
+
closeBtn.addEventListener('click', () => deps.onClose());
|
|
64
|
+
head.appendChild(closeBtn);
|
|
65
|
+
// State label + captions
|
|
66
|
+
const stateLabel = document.createElement('div');
|
|
67
|
+
stateLabel.className = 'state-label';
|
|
68
|
+
stateLabel.textContent = 'Connecting…';
|
|
69
|
+
const captions = document.createElement('div');
|
|
70
|
+
captions.className = 'captions';
|
|
71
|
+
captions.setAttribute('aria-live', 'polite');
|
|
72
|
+
let ccOn = false;
|
|
73
|
+
ccBtn.addEventListener('click', () => {
|
|
74
|
+
ccOn = !ccOn;
|
|
75
|
+
ccBtn.setAttribute('aria-pressed', String(ccOn));
|
|
76
|
+
ccBtn.classList.toggle('on', ccOn);
|
|
77
|
+
captions.classList.toggle('show', ccOn);
|
|
78
|
+
});
|
|
79
|
+
const hint = document.createElement('div');
|
|
80
|
+
hint.className = 'hint';
|
|
81
|
+
hint.textContent = "Can't talk right now? Turn on captions, or use the suggestion buttons.";
|
|
82
|
+
sheet.append(head, stageArea, stateLabel, captions, hint);
|
|
83
|
+
root.appendChild(sheet);
|
|
84
|
+
if (reducedMotion)
|
|
85
|
+
sheet.classList.remove('enter');
|
|
86
|
+
else
|
|
87
|
+
requestAnimationFrame(() => requestAnimationFrame(() => sheet.classList.remove('enter')));
|
|
88
|
+
const setState = (state) => {
|
|
89
|
+
sheet.setAttribute('data-state', state);
|
|
90
|
+
stateLabel.textContent = ({
|
|
91
|
+
connecting: 'Connecting…',
|
|
92
|
+
listening: 'Listening…',
|
|
93
|
+
thinking: 'Thinking…',
|
|
94
|
+
speaking: deps.copilotName + ' is speaking',
|
|
95
|
+
})[state];
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
video,
|
|
99
|
+
setState,
|
|
100
|
+
appendCaption(text) {
|
|
101
|
+
// Append to the current line; sentence breaks create new lines.
|
|
102
|
+
captions.textContent = (captions.textContent ?? '') + text;
|
|
103
|
+
captions.scrollTop = captions.scrollHeight;
|
|
104
|
+
},
|
|
105
|
+
destroy() {
|
|
106
|
+
sheet.classList.add('exit');
|
|
107
|
+
setTimeout(() => { try {
|
|
108
|
+
container.remove();
|
|
109
|
+
}
|
|
110
|
+
catch { /* gone */ } }, reducedMotion ? 0 : 220);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function ensureContainer() {
|
|
115
|
+
let el = document.getElementById(CONTAINER_ID);
|
|
116
|
+
if (el)
|
|
117
|
+
return el;
|
|
118
|
+
el = document.createElement('div');
|
|
119
|
+
el.id = CONTAINER_ID;
|
|
120
|
+
el.style.cssText = 'position:fixed;inset:0;z-index:2147483646;pointer-events:none';
|
|
121
|
+
el.attachShadow({ mode: 'open' });
|
|
122
|
+
document.body.appendChild(el);
|
|
123
|
+
return el;
|
|
124
|
+
}
|
|
125
|
+
function styleNode(dock) {
|
|
126
|
+
const node = document.createElement('style');
|
|
127
|
+
const onRight = dock.endsWith('right');
|
|
128
|
+
const side = onRight ? 'right: 20px;' : 'left: 20px;';
|
|
129
|
+
const origin = `${dock.startsWith('bottom') ? 'bottom' : 'top'} ${onRight ? 'right' : 'left'}`;
|
|
130
|
+
node.textContent = `
|
|
131
|
+
:host { all: initial; }
|
|
132
|
+
.sheet {
|
|
133
|
+
pointer-events: auto;
|
|
134
|
+
position: fixed; ${side} bottom: 20px;
|
|
135
|
+
width: 380px; max-width: calc(100vw - 32px);
|
|
136
|
+
height: 560px; max-height: calc(100vh - 40px);
|
|
137
|
+
background: #0e0f12; color: #fafafa;
|
|
138
|
+
border-radius: 20px; overflow: hidden;
|
|
139
|
+
box-shadow: 0 24px 64px rgba(0,0,0,0.45);
|
|
140
|
+
display: flex; flex-direction: column;
|
|
141
|
+
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
142
|
+
transform-origin: ${origin};
|
|
143
|
+
opacity: 1; transition: opacity 220ms ease-out, transform 220ms cubic-bezier(0.16,1,0.3,1);
|
|
144
|
+
}
|
|
145
|
+
.sheet.enter { opacity: 0; transform: scale(0.9) translateY(12px); }
|
|
146
|
+
.sheet.exit { opacity: 0; transform: scale(0.92) translateY(12px); }
|
|
147
|
+
@media (max-width: 480px) {
|
|
148
|
+
.sheet { left: 0; right: 0; bottom: 0; width: 100%; height: 88vh; border-radius: 20px 20px 0 0; }
|
|
149
|
+
}
|
|
150
|
+
.head { display: flex; align-items: center; gap: 10px; padding: 14px 16px; }
|
|
151
|
+
.idblock { flex: 1; min-width: 0; }
|
|
152
|
+
.name { font-weight: 600; font-size: 15px; }
|
|
153
|
+
.scope { font-size: 12px; color: #9aa0aa; margin-top: 1px; }
|
|
154
|
+
.cc, .x { appearance: none; border: 0; background: rgba(255,255,255,0.08); color: #cfd2d8; cursor: pointer; border-radius: 8px; }
|
|
155
|
+
.cc { font-size: 11px; font-weight: 700; padding: 5px 8px; }
|
|
156
|
+
.cc.on { background: #4f8bff; color: #fff; }
|
|
157
|
+
.x { font-size: 14px; padding: 6px 9px; }
|
|
158
|
+
.cc:hover, .x:hover { background: rgba(255,255,255,0.16); }
|
|
159
|
+
.avatar-area { position: relative; flex: 1; min-height: 0; background: #16181d; display: flex; align-items: center; justify-content: center; }
|
|
160
|
+
.avatar-video { width: 100%; height: 100%; object-fit: cover; }
|
|
161
|
+
.state-ring {
|
|
162
|
+
position: absolute; inset: 0; pointer-events: none;
|
|
163
|
+
box-shadow: inset 0 0 0 0 rgba(79,139,255,0); transition: box-shadow 200ms ease-out;
|
|
164
|
+
}
|
|
165
|
+
.sheet[data-state="listening"] .state-ring { box-shadow: inset 0 0 0 3px rgba(79,139,255,0.7); }
|
|
166
|
+
.sheet[data-state="thinking"] .state-ring { box-shadow: inset 0 0 0 3px rgba(255,255,255,0.25); }
|
|
167
|
+
.sheet[data-state="speaking"] .state-ring { box-shadow: inset 0 0 0 3px rgba(120,220,160,0.6); }
|
|
168
|
+
.state-label { padding: 10px 16px 0; font-size: 12px; color: #9aa0aa; }
|
|
169
|
+
.captions { display: none; padding: 8px 16px; max-height: 96px; overflow-y: auto; color: #e8e8ea; font-size: 14px; }
|
|
170
|
+
.captions.show { display: block; }
|
|
171
|
+
.hint { padding: 8px 16px 14px; font-size: 11px; color: #6b7280; }
|
|
172
|
+
@media (prefers-reduced-motion: reduce) { .sheet { transition: none; } }
|
|
173
|
+
`;
|
|
174
|
+
return node;
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=ui.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/stage/ui.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theater overlay — P5 (copilot-theater-design.md §4).
|
|
3
|
+
*
|
|
4
|
+
* The fourth magnitude: a dimmed scrim + a sandboxed <iframe> hosting the
|
|
5
|
+
* Holostaff Theater micro-app, with the copilot's portrait composited on
|
|
6
|
+
* top as the presenter. Near-zero SDK weight — all the render/stream
|
|
7
|
+
* machinery lives in the iframe app we host; the SDK only injects the
|
|
8
|
+
* iframe and speaks the postMessage protocol:
|
|
9
|
+
*
|
|
10
|
+
* SDK → iframe : { type:'theme', tokens } · { type:'present', payload }
|
|
11
|
+
* { type:'highlight', elementId } · { type:'reset' }
|
|
12
|
+
* iframe → SDK : { type:'theater-ready' | 'theater-frame-done'
|
|
13
|
+
* | 'theater-error' | 'theater-close' }
|
|
14
|
+
*
|
|
15
|
+
* CSP degrade (§4.1): if the host's `frame-src` blocks our domain the
|
|
16
|
+
* iframe never reaches 'theater-ready'; we time out, mark the Theater
|
|
17
|
+
* unavailable, and the copilot falls back to telling (note / voice).
|
|
18
|
+
*/
|
|
19
|
+
import type { PresenceBinding } from '../presence/index.js';
|
|
20
|
+
export interface TheaterDeps {
|
|
21
|
+
/** Theater service origin, e.g. https://theater.holostaff.ai */
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
/** Resolves the runtime context for /present payloads. */
|
|
24
|
+
buildContext: () => {
|
|
25
|
+
tenantId: string;
|
|
26
|
+
sourceId: string;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
};
|
|
29
|
+
/** Presence — portrait composite + design tokens + copilot identity. */
|
|
30
|
+
presence: PresenceBinding;
|
|
31
|
+
onError?: (err: Error) => void;
|
|
32
|
+
}
|
|
33
|
+
export interface PresentRequest {
|
|
34
|
+
intent: string;
|
|
35
|
+
context?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface TheaterBinding {
|
|
38
|
+
present(req: PresentRequest): void;
|
|
39
|
+
highlight(elementId: string): void;
|
|
40
|
+
close(): void;
|
|
41
|
+
/** False once a CSP block has been detected for this host. */
|
|
42
|
+
isAvailable(): boolean;
|
|
43
|
+
dispose(): void;
|
|
44
|
+
}
|
|
45
|
+
export declare function startTheater(deps: TheaterDeps): TheaterBinding;
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theater/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAK3D,MAAM,WAAW,WAAW;IAC1B,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAA;IACf,0DAA0D;IAC1D,YAAY,EAAE,MAAM;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7E,wEAAwE;IACxE,QAAQ,EAAE,eAAe,CAAA;IACzB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,KAAK,IAAI,IAAI,CAAA;IACb,8DAA8D;IAC9D,WAAW,IAAI,OAAO,CAAA;IACtB,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,cAAc,CAmK9D"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theater overlay — P5 (copilot-theater-design.md §4).
|
|
3
|
+
*
|
|
4
|
+
* The fourth magnitude: a dimmed scrim + a sandboxed <iframe> hosting the
|
|
5
|
+
* Holostaff Theater micro-app, with the copilot's portrait composited on
|
|
6
|
+
* top as the presenter. Near-zero SDK weight — all the render/stream
|
|
7
|
+
* machinery lives in the iframe app we host; the SDK only injects the
|
|
8
|
+
* iframe and speaks the postMessage protocol:
|
|
9
|
+
*
|
|
10
|
+
* SDK → iframe : { type:'theme', tokens } · { type:'present', payload }
|
|
11
|
+
* { type:'highlight', elementId } · { type:'reset' }
|
|
12
|
+
* iframe → SDK : { type:'theater-ready' | 'theater-frame-done'
|
|
13
|
+
* | 'theater-error' | 'theater-close' }
|
|
14
|
+
*
|
|
15
|
+
* CSP degrade (§4.1): if the host's `frame-src` blocks our domain the
|
|
16
|
+
* iframe never reaches 'theater-ready'; we time out, mark the Theater
|
|
17
|
+
* unavailable, and the copilot falls back to telling (note / voice).
|
|
18
|
+
*/
|
|
19
|
+
const READY_TIMEOUT_MS = 6000;
|
|
20
|
+
const SCRIM_ID = 'holostaff-theater-root';
|
|
21
|
+
export function startTheater(deps) {
|
|
22
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
23
|
+
return inert();
|
|
24
|
+
}
|
|
25
|
+
const origin = safeOrigin(deps.baseUrl);
|
|
26
|
+
let scrim = null;
|
|
27
|
+
let iframe = null;
|
|
28
|
+
let ready = false;
|
|
29
|
+
let available = true;
|
|
30
|
+
let readyTimer = null;
|
|
31
|
+
let pending = null;
|
|
32
|
+
const onMessage = (ev) => {
|
|
33
|
+
if (origin && ev.origin !== origin)
|
|
34
|
+
return;
|
|
35
|
+
const m = ev.data;
|
|
36
|
+
if (!m || typeof m.type !== 'string')
|
|
37
|
+
return;
|
|
38
|
+
switch (m.type) {
|
|
39
|
+
case 'theater-ready':
|
|
40
|
+
ready = true;
|
|
41
|
+
if (readyTimer) {
|
|
42
|
+
clearTimeout(readyTimer);
|
|
43
|
+
readyTimer = null;
|
|
44
|
+
}
|
|
45
|
+
sendTheme();
|
|
46
|
+
if (pending) {
|
|
47
|
+
sendPresent(pending);
|
|
48
|
+
pending = null;
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
case 'theater-close':
|
|
52
|
+
close();
|
|
53
|
+
break;
|
|
54
|
+
case 'theater-error':
|
|
55
|
+
deps.onError?.(new Error(`Theater: ${m.error ?? 'render error'}`));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
window.addEventListener('message', onMessage);
|
|
60
|
+
const ensureOverlay = () => {
|
|
61
|
+
if (scrim)
|
|
62
|
+
return;
|
|
63
|
+
scrim = document.createElement('div');
|
|
64
|
+
scrim.id = SCRIM_ID;
|
|
65
|
+
scrim.style.cssText = [
|
|
66
|
+
'position:fixed', 'inset:0', 'z-index:2147483647',
|
|
67
|
+
'background:rgba(8,9,12,0.62)', 'backdrop-filter:blur(2px)',
|
|
68
|
+
'opacity:0', 'transition:opacity 200ms ease-out',
|
|
69
|
+
].join(';');
|
|
70
|
+
iframe = document.createElement('iframe');
|
|
71
|
+
iframe.title = 'Copilot presentation';
|
|
72
|
+
// Run scripts + talk to the Theater backend (same origin as the
|
|
73
|
+
// iframe = our service), but no access to the host page or top nav.
|
|
74
|
+
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
|
|
75
|
+
iframe.style.cssText = [
|
|
76
|
+
'position:absolute', 'inset:0', 'width:100%', 'height:100%',
|
|
77
|
+
'border:0', 'background:transparent',
|
|
78
|
+
].join(';');
|
|
79
|
+
iframe.addEventListener('error', () => markUnavailable());
|
|
80
|
+
// Close affordance (always visible — design §2.4).
|
|
81
|
+
const closeBtn = document.createElement('button');
|
|
82
|
+
closeBtn.type = 'button';
|
|
83
|
+
closeBtn.setAttribute('aria-label', 'Close presentation');
|
|
84
|
+
closeBtn.textContent = '✕';
|
|
85
|
+
closeBtn.style.cssText = [
|
|
86
|
+
'position:absolute', 'top:18px', 'right:20px', 'z-index:2',
|
|
87
|
+
'width:40px', 'height:40px', 'border-radius:50%', 'border:0',
|
|
88
|
+
'background:rgba(255,255,255,0.14)', 'color:#fff', 'font-size:16px',
|
|
89
|
+
'cursor:pointer',
|
|
90
|
+
].join(';');
|
|
91
|
+
closeBtn.addEventListener('click', () => close());
|
|
92
|
+
scrim.addEventListener('click', (e) => { if (e.target === scrim)
|
|
93
|
+
close(); });
|
|
94
|
+
scrim.append(iframe, closeBtn, presenterPortrait());
|
|
95
|
+
document.body.appendChild(scrim);
|
|
96
|
+
requestAnimationFrame(() => { if (scrim)
|
|
97
|
+
scrim.style.opacity = '1'; });
|
|
98
|
+
// Load the app shell + arm the CSP-block timeout.
|
|
99
|
+
ready = false;
|
|
100
|
+
iframe.src = `${deps.baseUrl.replace(/\/$/, '')}/theater/app`;
|
|
101
|
+
readyTimer = setTimeout(() => { if (!ready)
|
|
102
|
+
markUnavailable(); }, READY_TIMEOUT_MS);
|
|
103
|
+
};
|
|
104
|
+
const sendTheme = () => {
|
|
105
|
+
const tokens = deps.presence.getDesignTokens();
|
|
106
|
+
post({ type: 'theme', tokens });
|
|
107
|
+
};
|
|
108
|
+
const sendPresent = (req) => {
|
|
109
|
+
const ctx = deps.buildContext();
|
|
110
|
+
const copilot = deps.presence.getCopilot();
|
|
111
|
+
post({
|
|
112
|
+
type: 'present',
|
|
113
|
+
payload: {
|
|
114
|
+
...ctx,
|
|
115
|
+
copilotId: copilot?.copilotId,
|
|
116
|
+
copilotName: copilot?.name,
|
|
117
|
+
intent: req.intent,
|
|
118
|
+
context: req.context,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
const post = (msg) => {
|
|
123
|
+
try {
|
|
124
|
+
iframe?.contentWindow?.postMessage(msg, origin || '*');
|
|
125
|
+
}
|
|
126
|
+
catch { /* gone */ }
|
|
127
|
+
};
|
|
128
|
+
const presenterPortrait = () => {
|
|
129
|
+
// Pin the copilot as the presenter (design §2.4). Static here; the
|
|
130
|
+
// Stage (P6) upgrades this to the live Anam avatar narrating.
|
|
131
|
+
const wrap = document.createElement('div');
|
|
132
|
+
wrap.style.cssText = [
|
|
133
|
+
'position:absolute', 'left:24px', 'bottom:24px', 'z-index:2',
|
|
134
|
+
'width:72px', 'height:72px', 'border-radius:50%', 'overflow:hidden',
|
|
135
|
+
'box-shadow:0 8px 24px rgba(0,0,0,0.4)', 'background:#1a1a1a',
|
|
136
|
+
].join(';');
|
|
137
|
+
const c = deps.presence.getCopilot();
|
|
138
|
+
const portrait = c?.portrait;
|
|
139
|
+
if (portrait?.idle || portrait?.idleMp4 || portrait?.poster) {
|
|
140
|
+
const v = document.createElement('video');
|
|
141
|
+
v.muted = true;
|
|
142
|
+
v.loop = true;
|
|
143
|
+
v.autoplay = true;
|
|
144
|
+
v.playsInline = true;
|
|
145
|
+
v.setAttribute('playsinline', '');
|
|
146
|
+
v.style.cssText = 'width:100%;height:100%;object-fit:cover';
|
|
147
|
+
if (portrait.poster)
|
|
148
|
+
v.poster = portrait.poster;
|
|
149
|
+
const src = portrait.idle || portrait.idleMp4;
|
|
150
|
+
if (src) {
|
|
151
|
+
v.src = src;
|
|
152
|
+
void v.play().catch(() => { });
|
|
153
|
+
}
|
|
154
|
+
wrap.appendChild(v);
|
|
155
|
+
}
|
|
156
|
+
else if (c?.avatar) {
|
|
157
|
+
const img = document.createElement('img');
|
|
158
|
+
img.src = c.avatar;
|
|
159
|
+
img.alt = '';
|
|
160
|
+
img.referrerPolicy = 'no-referrer';
|
|
161
|
+
img.style.cssText = 'width:100%;height:100%;object-fit:cover';
|
|
162
|
+
wrap.appendChild(img);
|
|
163
|
+
}
|
|
164
|
+
return wrap;
|
|
165
|
+
};
|
|
166
|
+
const markUnavailable = () => {
|
|
167
|
+
available = false;
|
|
168
|
+
deps.onError?.(new Error('Theater unavailable (likely host frame-src CSP). Falling back to telling.'));
|
|
169
|
+
close();
|
|
170
|
+
};
|
|
171
|
+
const close = () => {
|
|
172
|
+
if (readyTimer) {
|
|
173
|
+
clearTimeout(readyTimer);
|
|
174
|
+
readyTimer = null;
|
|
175
|
+
}
|
|
176
|
+
if (!scrim)
|
|
177
|
+
return;
|
|
178
|
+
const node = scrim;
|
|
179
|
+
scrim = null;
|
|
180
|
+
iframe = null;
|
|
181
|
+
ready = false;
|
|
182
|
+
pending = null;
|
|
183
|
+
node.style.opacity = '0';
|
|
184
|
+
setTimeout(() => { try {
|
|
185
|
+
node.remove();
|
|
186
|
+
}
|
|
187
|
+
catch { /* gone */ } }, 200);
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
present(req) {
|
|
191
|
+
if (!available || !req?.intent)
|
|
192
|
+
return;
|
|
193
|
+
ensureOverlay();
|
|
194
|
+
if (ready)
|
|
195
|
+
sendPresent(req);
|
|
196
|
+
else
|
|
197
|
+
pending = req; // sent on theater-ready
|
|
198
|
+
},
|
|
199
|
+
highlight(elementId) { post({ type: 'highlight', elementId }); },
|
|
200
|
+
close,
|
|
201
|
+
isAvailable: () => available,
|
|
202
|
+
dispose() {
|
|
203
|
+
window.removeEventListener('message', onMessage);
|
|
204
|
+
close();
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function safeOrigin(url) {
|
|
209
|
+
try {
|
|
210
|
+
return new URL(url).origin;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return '';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function inert() {
|
|
217
|
+
return {
|
|
218
|
+
present: () => { }, highlight: () => { }, close: () => { },
|
|
219
|
+
isAvailable: () => false, dispose: () => { },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=index.js.map
|