@simon-says-build/ssb-overlay 0.1.0 → 0.5.1
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/assets/ssb-logo.png +0 -0
- package/index.d.ts +6 -0
- package/index.js +1 -1
- package/install/cli.mjs +26 -0
- package/install/install.mjs +343 -0
- package/package.json +18 -3
- package/src/SsbHost.js +376 -167
- package/src/core.js +124 -18
package/src/core.js
CHANGED
|
@@ -42,33 +42,88 @@ export function inAppApi(config, getToken, fetchImpl = globalThis.fetch) {
|
|
|
42
42
|
chat: (id) => call(`/chats/${encodeURIComponent(id)}`),
|
|
43
43
|
decide: (chatId, proposalId, action, body) => post(`/chats/${encodeURIComponent(chatId)}/proposals/${encodeURIComponent(proposalId)}/${action}`, body),
|
|
44
44
|
pushToken: (token, platform) => post('/push-token', { token, platform }),
|
|
45
|
+
voice: (body) => post('/voice', body),
|
|
46
|
+
work: () => call('/work'),
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
50
|
+
// ---- the pencil: scribbles on the live screen --------------------------------
|
|
51
|
+
const MIN_STEP = 3; // a point closer than this to the last one adds nothing
|
|
52
|
+
|
|
53
|
+
/** A stroke with `p` added, unless it is too close to the last point to matter. */
|
|
54
|
+
export function addPoint(stroke, p) {
|
|
55
|
+
const last = stroke[stroke.length - 1];
|
|
56
|
+
if (last && Math.hypot(p.x - last.x, p.y - last.y) < MIN_STEP) return stroke;
|
|
57
|
+
return [...stroke, { x: p.x, y: p.y }];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A stroke as straight segments a View can draw: each is a bar `width` long,
|
|
62
|
+
* centred on its midpoint and turned by `angle` (radians). A single point is a dot.
|
|
63
|
+
*/
|
|
64
|
+
export function segmentsOf(stroke, thickness = 4) {
|
|
65
|
+
if (stroke.length === 1) return [{ left: stroke[0].x - thickness / 2, top: stroke[0].y - thickness / 2, width: thickness, angle: 0 }];
|
|
66
|
+
const out = [];
|
|
67
|
+
for (let i = 1; i < stroke.length; i++) {
|
|
68
|
+
const a = stroke[i - 1], b = stroke[i];
|
|
69
|
+
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
70
|
+
const width = len + thickness; // overlap the joints so a curve has no gaps
|
|
71
|
+
out.push({ left: (a.x + b.x) / 2 - width / 2, top: (a.y + b.y) / 2 - thickness / 2, width, angle: Math.atan2(b.y - a.y, b.x - a.x) });
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A drawing is a list of items, each a pencil stroke or a box:
|
|
77
|
+
// { kind: 'stroke', points: [{x, y}] } | { kind: 'box', x0, y0, x1, y1 } (screen points)
|
|
78
|
+
const MIN_BOX = 8; // a box smaller than this was a slip, not a box
|
|
79
|
+
|
|
80
|
+
/** A box item from its drag, or null when it is too small to mean anything. */
|
|
81
|
+
export function boxItem(d) {
|
|
82
|
+
if (Math.abs(d.x1 - d.x0) < MIN_BOX || Math.abs(d.y1 - d.y0) < MIN_BOX) return null;
|
|
83
|
+
return { kind: 'box', x0: d.x0, y0: d.y0, x1: d.x1, y1: d.y1 };
|
|
55
84
|
}
|
|
56
85
|
|
|
57
|
-
/**
|
|
58
|
-
export function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
/** One item's box, in points. */
|
|
87
|
+
export function itemRect(it) {
|
|
88
|
+
if (it.kind === 'box') {
|
|
89
|
+
const left = Math.min(it.x0, it.x1), top = Math.min(it.y0, it.y1);
|
|
90
|
+
return { left, top, width: Math.abs(it.x1 - it.x0), height: Math.abs(it.y1 - it.y0) };
|
|
91
|
+
}
|
|
92
|
+
const xs = it.points.map((p) => p.x), ys = it.points.map((p) => p.y);
|
|
93
|
+
const left = Math.min(...xs), top = Math.min(...ys);
|
|
94
|
+
return { left, top, width: Math.max(...xs) - left, height: Math.max(...ys) - top };
|
|
62
95
|
}
|
|
63
96
|
|
|
64
|
-
/** The
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
97
|
+
/** The box round everything drawn, or null with nothing drawn. */
|
|
98
|
+
export function drawingBox(items) {
|
|
99
|
+
const rs = items.filter((it) => it.kind === 'box' || it.points?.length).map(itemRect);
|
|
100
|
+
if (!rs.length) return null;
|
|
101
|
+
const left = Math.min(...rs.map((r) => r.left)), top = Math.min(...rs.map((r) => r.top));
|
|
102
|
+
return { left, top, width: Math.max(...rs.map((r) => r.left + r.width)) - left, height: Math.max(...rs.map((r) => r.top + r.height)) - top };
|
|
103
|
+
}
|
|
68
104
|
|
|
69
105
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
106
|
+
* What was drawn, as the ONE mark a send carries (annotate.mjs cleanMarks):
|
|
107
|
+
* a `box` when only boxes were drawn, else a `pen`, over the drawing's box,
|
|
108
|
+
* in fractions of the screen.
|
|
109
|
+
*/
|
|
110
|
+
export function drawingMark(items, screen) {
|
|
111
|
+
const r = drawingBox(items);
|
|
112
|
+
if (!r) return null;
|
|
113
|
+
const f = (v, d) => Math.min(1, Math.max(0, v / d));
|
|
114
|
+
return { kind: items.every((it) => it.kind === 'box') ? 'box' : 'pen',
|
|
115
|
+
x: f(r.left, screen.width), y: f(r.top, screen.height), w: f(r.width, screen.width), h: f(r.height, screen.height) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The point the drawing is about (what the app has under it is named): the middle of its box. */
|
|
119
|
+
export const drawingPoint = (items) => {
|
|
120
|
+
const r = drawingBox(items);
|
|
121
|
+
return r ? { x: r.left + r.width / 2, y: r.top + r.height / 2 } : null;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Where the note field goes: just above the keyboard when that is clear of
|
|
126
|
+
* the drawing, else under the status bar. `keyboardTop` is the
|
|
72
127
|
* keyboard's top edge in screen points (the screen height when it is down).
|
|
73
128
|
*/
|
|
74
129
|
export function fieldTop({ rect, screen, keyboardTop, insets, fieldH = 56 }) {
|
|
@@ -202,3 +257,54 @@ export function demoBanner(chat) {
|
|
|
202
257
|
if (d.status === 'failed') return { text: `Done: ${what}. The demo couldn't be prepared${d.error ? `: ${d.error}` : '.'}` };
|
|
203
258
|
return null;
|
|
204
259
|
}
|
|
260
|
+
|
|
261
|
+
// ---- the button: wherever it is put --------------------------------------------
|
|
262
|
+
export const FAB = 52;
|
|
263
|
+
const TAP_SLOP = 8; // a press that moved less than this is a tap, not a drag
|
|
264
|
+
|
|
265
|
+
/** Where the button sits by default: bottom-right, clear of the insets. */
|
|
266
|
+
export const fabHome = (screen, insets) => ({ x: screen.width - FAB - 16, y: screen.height - FAB - 16 - insets.bottom });
|
|
267
|
+
|
|
268
|
+
/** A position kept on screen: the whole button visible, off the status bar. */
|
|
269
|
+
export function clampFab(pos, screen, insets) {
|
|
270
|
+
return {
|
|
271
|
+
x: Math.min(screen.width - FAB - 4, Math.max(4, pos.x)),
|
|
272
|
+
y: Math.min(screen.height - FAB - 4, Math.max(insets.top, pos.y)),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Did the finger travel far enough to be a drag? */
|
|
277
|
+
export const isDrag = (dx, dy) => Math.hypot(dx, dy) >= TAP_SLOP;
|
|
278
|
+
|
|
279
|
+
// ---- 🎙 Talk (functions/voice-chat.mjs, voice-agent/src/chat.mjs) --------------
|
|
280
|
+
export const SNAPSHOT_TOPIC = 'ssb.guide.snapshot'; // the agent's (voice-agent/src/guide.mjs TOPIC.snapshot)
|
|
281
|
+
|
|
282
|
+
/** What the agent is sent about the screen: the route, the picture when there is one, and whether this is the opening look. */
|
|
283
|
+
export const snapshotOf = ({ route, screenshot, first = false }) => JSON.stringify({ route: route || '/', ...(screenshot ? { screenshot } : {}), ...(first ? { first: true } : {}) });
|
|
284
|
+
|
|
285
|
+
/** The talk bar's words. */
|
|
286
|
+
export function talkLine(talk) {
|
|
287
|
+
if (!talk) return '';
|
|
288
|
+
if (talk.status === 'connecting') return 'Connecting to SSB…';
|
|
289
|
+
if (talk.status === 'waiting') return 'Waiting for SSB to join…';
|
|
290
|
+
return talk.muted ? 'Talking with SSB · muted' : 'Talking with SSB';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---- the panel's top: the work (2026-10-20, the product owner: "the web
|
|
294
|
+
// extension has jobs and in-progress things at the top, with chat down the
|
|
295
|
+
// bottom … maybe we can move to that pattern") ------------------------------
|
|
296
|
+
/** The extension's words for a job in flight (extension/overlay.js inFlightHtml). */
|
|
297
|
+
export const STATE_WORD = Object.freeze({ ready: 'Ready', queued: 'Queued', building: 'Building', checking: 'Checking', blocked: 'Blocked' });
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* What the top of the panel shows: the app's work in flight (GET
|
|
301
|
+
* /api/in-app/work, functions/sessions.mjs inFlightOf — whatever chat it came
|
|
302
|
+
* from), this chat's drafts waiting to start, and what this chat started that
|
|
303
|
+
* is not in flight any more (the row says so, with its job).
|
|
304
|
+
*/
|
|
305
|
+
export function workView({ inFlight = [], chat = null } = {}) {
|
|
306
|
+
const flying = Array.isArray(inFlight) ? inFlight : [];
|
|
307
|
+
const flyingIds = new Set(flying.map((x) => x.id));
|
|
308
|
+
const { waiting, sent } = draftsOf(chat);
|
|
309
|
+
return { flying, waiting, sent: sent.filter((p) => !p.jobId || !flyingIds.has(p.jobId)) };
|
|
310
|
+
}
|